Add setting for customizable emoji support in ratings

Added InteractiveRatingsSettings interface, settings tab, and dynamic symbol pattern updates. Users can now customize supported emojis through plugin settings.

Default: 🎥🏆💎🔥🎯🚀💰🎖️

Fix regex detection bug for low ratings (1-2 stars)

Modified regex to detect 1+ symbols instead of 3+, and updated editor logic to intelligently handle short patterns with rating text or full-only symbols. Fixes issue where reducing ratings to 1-2 stars made them uneditable.

Simplify pattern detection logic - remove complex special cases

Replaced complex logic with simple rule: if no rating text, require 3+ symbols to avoid false positives. If rating text exists, any symbol count is valid. This generic approach handles all cases without special full-only symbol logic.

Remove hardcoded configurable emojis from constants.ts since they're now managed through settings
This commit is contained in:
Filip Noetzel 2025-06-09 23:04:50 +02:00
parent 3c0cf4029a
commit 2ea7af7bbd
7 changed files with 134 additions and 25 deletions

View file

@ -1,16 +1,30 @@
import { Plugin } from 'obsidian';
import { LOGGING_ENABLED } from './constants';
import { ratingEditorExtension } from './editor-extension';
import { InteractiveRatingsSettings } from './types';
import { DEFAULT_SETTINGS, InteractiveRatingsSettingTab } from './settings';
import { updateSymbolPatternsFromSettings } from './utils';
export class InteractiveRatingsPlugin extends Plugin {
settings: InteractiveRatingsSettings;
async onload(): Promise<void> {
if (LOGGING_ENABLED) {
console.info('[InteractiveRatings] Plugin loading - edit mode only');
}
// Load settings
await this.loadSettings();
// Update symbol patterns based on settings
this.updateSymbolPatterns();
// Register editor extension for interactive ratings in editing mode only
this.registerEditorExtension(ratingEditorExtension);
// Add settings tab
this.addSettingTab(new InteractiveRatingsSettingTab(this.app, this));
if (LOGGING_ENABLED) {
console.info('[InteractiveRatings] Plugin loaded successfully');
}
@ -21,4 +35,16 @@ export class InteractiveRatingsPlugin extends Plugin {
console.info('[InteractiveRatings] Plugin unloaded');
}
}
async loadSettings(): Promise<void> {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings(): Promise<void> {
await this.saveData(this.settings);
}
updateSymbolPatterns(): void {
updateSymbolPatternsFromSettings(this.settings);
}
}

View file

@ -3,8 +3,8 @@ import { SymbolSet } from './types';
// Global logging control - set to false for production, true for debugging
export const LOGGING_ENABLED = false;
// Define symbol patterns as a global constant
export const SYMBOL_PATTERNS: SymbolSet[] = [
// Base symbol patterns (excluding user-configurable emojis)
export const BASE_SYMBOL_PATTERNS: SymbolSet[] = [
{ full: '★', empty: '☆', half: null }, // Symbols
{ full: '✦', empty: '✧', half: null }, // Star symbols
{ full: '🌕', empty: '🌑', half: '🌗' }, // Moon phases
@ -33,20 +33,22 @@ export const SYMBOL_PATTERNS: SymbolSet[] = [
{ full: '⬤', empty: '◯', half: null }, // Bold circles
{ full: '⚫', empty: '⚪', half: null }, // Black/white circles
{ full: '█', empty: '░', half: null }, // Block/light shade
// Full-only symbols (same symbol for full and empty, no half)
{ full: '🎥', empty: '🎥', half: null }, // Movie cameras
{ full: '🏆', empty: '🏆', half: null }, // Trophies
{ full: '⭐', empty: '⭐', half: null }, // Gold stars
{ full: '💎', empty: '💎', half: null }, // Diamonds
{ full: '🔥', empty: '🔥', half: null }, // Fire
{ full: '⚡', empty: '⚡', half: null }, // Lightning
{ full: '🎯', empty: '🎯', half: null }, // Target/bullseye
{ full: '🚀', empty: '🚀', half: null }, // Rockets
{ full: '💰', empty: '💰', half: null }, // Money bags
{ full: '🎖️', empty: '🎖️', half: null }, // Military medals
];
// Mutable symbol patterns - starts with base patterns and user-configurable emojis are added dynamically
export let SYMBOL_PATTERNS: SymbolSet[] = [
...BASE_SYMBOL_PATTERNS,
// User-configurable emojis are added dynamically from settings
];
/**
* Update the global symbol patterns array
*/
export function updateSymbolPatterns(newPatterns: SymbolSet[]): void {
SYMBOL_PATTERNS.length = 0; // Clear the array
SYMBOL_PATTERNS.push(...newPatterns); // Add new patterns
}
// Interaction constants
export const INTERACTION_BUFFER = 5; // Buffer for interaction detection
export const OVERLAY_VERTICAL_ADJUSTMENT = 2.1; // Vertical alignment for overlay

View file

@ -416,10 +416,6 @@ const ratingViewPlugin = ViewPlugin.fromClass(
const start = match.index;
const end = start + pattern.length;
// Skip if too short (minimum 3 symbols) - use Unicode length
const unicodeLength = getUnicodeCharLength(pattern);
if (unicodeLength < 3) continue;
const symbolSet = getSymbolSetForPattern(pattern);
if (!symbolSet) continue;
@ -432,8 +428,11 @@ const ratingViewPlugin = ViewPlugin.fromClass(
// Check for rating text after the symbols - pass UTF-16 positions
const ratingText = parseRatingText(text, start, end);
// For full-only symbols without rating text, they can still be interactive
// (rating text will be auto-added on first interaction)
// Simple rule: if no rating text, require minimum 3 symbols to avoid false positives
const unicodeLength = getUnicodeCharLength(pattern);
if (!ratingText && unicodeLength < 3) {
continue;
}
const actualEnd = ratingText ? ratingText.endPosition : end;
@ -515,6 +514,7 @@ const ratingViewPlugin = ViewPlugin.fromClass(
const fullOnlyCount = filteredMatches.filter(m => isFullOnlySymbol(m.symbolSet)).length;
const commentCount = filteredMatches.filter(m => m.ratingText && m.ratingText.format === 'comment-fraction').length;
const autoAddCandidates = filteredMatches.filter(m => isFullOnlySymbol(m.symbolSet) && !m.ratingText).length;
const shortPatternCount = filteredMatches.filter(m => getUnicodeCharLength(m.pattern) < 3).length;
console.debug(`[InteractiveRatings] Built ${filteredMatches.length - skippedCount}/${filteredMatches.length} rating decorations (${skippedCount} skipped due to cursor proximity)`, {
cursorPos,
withRatingText: filteredMatches.filter(m => m.ratingText).length,
@ -522,7 +522,8 @@ const ratingViewPlugin = ViewPlugin.fromClass(
withHalfSymbols: filteredMatches.filter(m => m.symbolSet.half).length,
fullOnlySymbols: fullOnlyCount,
commentFormatSymbols: commentCount,
autoAddCandidates
autoAddCandidates,
shortPatterns: shortPatternCount
});
}

View file

@ -167,6 +167,7 @@ function escapeRegexChar(char: string): string {
/**
* Generate regex patterns for detecting rating symbols in text
* Fixed to handle multibyte Unicode characters (emojis) properly
* Now supports 1+ symbols to detect low ratings
*/
export function generateSymbolRegexPatterns(): RegExp[] {
return SYMBOL_PATTERNS.map(symbols => {
@ -178,13 +179,15 @@ export function generateSymbolRegexPatterns(): RegExp[] {
// Escape each symbol for regex and create alternation
const escapedSymbols = symbolChars.map(escapeRegexChar);
const pattern = `(?:${escapedSymbols.join('|')}){3,}`;
// Changed from {3,} to {1,} to detect single symbols (fixes low rating bug)
const pattern = `(?:${escapedSymbols.join('|')})+`;
if (LOGGING_ENABLED) {
console.debug(`[InteractiveRatings] Generated regex pattern for symbol set`, {
symbolSet: symbols,
pattern,
escapedSymbols
escapedSymbols,
allowsSingleSymbol: true
});
}

39
src/settings.ts Normal file
View file

@ -0,0 +1,39 @@
import { App, PluginSettingTab, Setting } from 'obsidian';
import { InteractiveRatingsPlugin } from './InteractiveRatingsPlugin';
import { InteractiveRatingsSettings } from './types';
const DEFAULT_SUPPORTED_EMOJIS = '🎥🏆⭐💎🔥⚡🎯🚀💰🎖️';
export const DEFAULT_SETTINGS: InteractiveRatingsSettings = {
supportedEmojis: DEFAULT_SUPPORTED_EMOJIS
};
export class InteractiveRatingsSettingTab extends PluginSettingTab {
plugin: InteractiveRatingsPlugin;
constructor(app: App, plugin: InteractiveRatingsPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'Interactive Ratings Settings' });
new Setting(containerEl)
.setName('Emojis to support in ratings')
.setDesc('Emojis to use for ratings.')
.addTextArea(text => text
.setPlaceholder(DEFAULT_SUPPORTED_EMOJIS)
.setValue(this.plugin.settings.supportedEmojis)
.onChange(async (value) => {
this.plugin.settings.supportedEmojis = value;
await this.plugin.saveSettings();
// Update the symbol patterns with new emojis
this.plugin.updateSymbolPatterns();
}));
}
}

View file

@ -11,3 +11,7 @@ export interface RatingText {
text: string;
endPosition: number;
}
export interface InteractiveRatingsSettings {
supportedEmojis: string;
}

View file

@ -1,5 +1,5 @@
import { LOGGING_ENABLED } from './constants';
import { SymbolSet } from './types';
import { LOGGING_ENABLED, BASE_SYMBOL_PATTERNS, updateSymbolPatterns } from './constants';
import { SymbolSet, InteractiveRatingsSettings } from './types';
/**
* Get the length of a string in Unicode characters
@ -32,6 +32,40 @@ export function isFullOnlySymbol(symbolSet: SymbolSet): boolean {
return symbolSet.full === symbolSet.empty && !symbolSet.half;
}
/**
* Update symbol patterns based on user settings
*/
export function updateSymbolPatternsFromSettings(settings: InteractiveRatingsSettings): void {
// Start with base patterns (all the existing ones except the customizable emojis)
const newPatterns = [...BASE_SYMBOL_PATTERNS];
// Add emoji patterns from settings
if (settings.supportedEmojis) {
// Split the emoji string into individual characters (handling Unicode properly)
const emojis = [...settings.supportedEmojis];
for (const emoji of emojis) {
if (emoji.trim()) { // Skip empty characters
newPatterns.push({
full: emoji,
empty: emoji,
half: null
});
}
}
}
// Update the global symbol patterns
updateSymbolPatterns(newPatterns);
if (LOGGING_ENABLED) {
console.debug('[InteractiveRatings] Updated symbol patterns from settings', {
supportedEmojis: settings.supportedEmojis,
totalPatterns: newPatterns.length
});
}
}
/**
* Calculate the new rating based on cursor position
*/