diff --git a/src/InteractiveRatingsPlugin.ts b/src/InteractiveRatingsPlugin.ts index 1125d6f..5a65541 100644 --- a/src/InteractiveRatingsPlugin.ts +++ b/src/InteractiveRatingsPlugin.ts @@ -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 { 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 { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + async saveSettings(): Promise { + await this.saveData(this.settings); + } + + updateSymbolPatterns(): void { + updateSymbolPatternsFromSettings(this.settings); + } } diff --git a/src/constants.ts b/src/constants.ts index ff4889e..b532148 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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 \ No newline at end of file diff --git a/src/editor-extension.ts b/src/editor-extension.ts index 8e81457..714095e 100644 --- a/src/editor-extension.ts +++ b/src/editor-extension.ts @@ -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 }); } diff --git a/src/ratings-parser.ts b/src/ratings-parser.ts index b6f2dc5..a61fddc 100644 --- a/src/ratings-parser.ts +++ b/src/ratings-parser.ts @@ -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 }); } diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..6121d13 --- /dev/null +++ b/src/settings.ts @@ -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(); + })); + } +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 7cd9e99..5bf2e9c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,3 +11,7 @@ export interface RatingText { text: string; endPosition: number; } + +export interface InteractiveRatingsSettings { + supportedEmojis: string; +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index aec7fe0..3aae1fc 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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 */