| /** | |
| * Bayan Chrome Extension — Patch Application Engine | |
| * | |
| * Algorithms reused from: src/js/editor.js | |
| * Core logic: reverse-order offset replacement (editor.js L674-710) | |
| * | |
| * This module applies suggestion corrections to plain text | |
| * without requiring DOM manipulation. | |
| * | |
| * ═══════════════════════════════════════════════════════════ | |
| * HIGH-1 FIX — Offset Rebasing Strategy | |
| * ═══════════════════════════════════════════════════════════ | |
| * | |
| * When applying a single correction, the text length may change. | |
| * All remaining suggestions whose offsets come AFTER the applied | |
| * patch must be shifted by the length difference (delta). | |
| * | |
| * Strategy: LINEAR SHIFT | |
| * delta = replacementText.length - (suggestion.end - suggestion.start) | |
| * For every remaining suggestion S where S.start >= applied.end: | |
| * S.start += delta | |
| * S.end += delta | |
| * | |
| * Edge cases: | |
| * - Shorter replacement (delta < 0): offsets shift left | |
| * - Longer replacement (delta > 0): offsets shift right | |
| * - Same-length (delta = 0): offsets unchanged | |
| * - Overlapping suggestions: handled by PatchSet.resolve_overlaps() | |
| * on the backend — we never receive overlapping suggestions | |
| * | |
| * This is O(n) per apply, which is fine for ≤50 suggestions. | |
| * ═══════════════════════════════════════════════════════════ | |
| */ | |
| /** | |
| * Apply a single suggestion patch to text. | |
| * Source: src/js/editor.js L506-510 (offset-based replacement) | |
| * | |
| * @param {string} text - Original text | |
| * @param {Object} suggestion - { start, end, correction } | |
| * @returns {string} Text with correction applied | |
| */ | |
| function applyPatch(text, suggestion) { | |
| const before = text.substring(0, suggestion.start); | |
| const after = text.substring(suggestion.end); | |
| return before + suggestion.correction + after; | |
| } | |
| /** | |
| * Apply all suggestion patches to text. | |
| * CRITICAL: Sort in REVERSE order (highest start offset first) | |
| * to prevent offset shifts from invalidating subsequent patches. | |
| * Source: src/js/editor.js L674-710 (reverse-order algorithm) | |
| * | |
| * @param {string} text - Original text | |
| * @param {Array} suggestions - Array of { start, end, correction } | |
| * @returns {string} Fully corrected text | |
| */ | |
| function applyAllPatches(text, suggestions) { | |
| if (!suggestions || suggestions.length === 0) return text; | |
| // CRITICAL: Sort in REVERSE order (highest start offset first). | |
| // This is the exact same algorithm from editor.js L676. | |
| const sorted = [...suggestions].sort((a, b) => b.start - a.start); | |
| let result = text; | |
| sorted.forEach((s) => { | |
| result = result.substring(0, s.start) + s.correction + result.substring(s.end); | |
| }); | |
| return result; | |
| } | |
| /** | |
| * Apply a specific alternative correction to text. | |
| * Source: src/js/editor.js L547-600 (alternative correction pattern) | |
| * | |
| * @param {string} text - Original text | |
| * @param {Object} suggestion - The suggestion being corrected | |
| * @param {string} alternativeText - The alternative correction to apply | |
| * @returns {string} Text with alternative correction applied | |
| */ | |
| function applyAlternativePatch(text, suggestion, alternativeText) { | |
| const before = text.substring(0, suggestion.start); | |
| const after = text.substring(suggestion.end); | |
| return before + alternativeText + after; | |
| } | |
| /** | |
| * Apply ONE correction and rebase ALL remaining suggestions. | |
| * This is the primary function for individual suggestion applies. | |
| * | |
| * HIGH-1 FIX: Atomic apply + rebase in a single call. | |
| * | |
| * @param {string} text - Current text (must match suggestion offsets) | |
| * @param {Object} appliedSuggestion - The suggestion being applied | |
| * @param {string} replacementText - The text to insert (correction or alternative) | |
| * @param {Array} allSuggestions - All current suggestions (including the one being applied) | |
| * @returns {{ text: string, suggestions: Array }} Updated text and rebased suggestions | |
| */ | |
| function applyAndRebase(text, appliedSuggestion, replacementText, allSuggestions) { | |
| // 1. Apply the patch to text | |
| const newText = text.substring(0, appliedSuggestion.start) | |
| + replacementText | |
| + text.substring(appliedSuggestion.end); | |
| // 2. Calculate delta: how much the text length changed | |
| const originalSpanLength = appliedSuggestion.end - appliedSuggestion.start; | |
| const delta = replacementText.length - originalSpanLength; | |
| // 3. Remove the applied suggestion and rebase remaining offsets | |
| const rebased = allSuggestions | |
| .filter((s) => s.id !== appliedSuggestion.id) | |
| .map((s) => { | |
| // Only shift suggestions that start AT or AFTER the applied patch's end. | |
| // Suggestions entirely before the patch are unaffected. | |
| // (Backend guarantees no overlapping suggestions via PatchSet.resolve_overlaps()) | |
| if (s.start >= appliedSuggestion.end) { | |
| return { ...s, start: s.start + delta, end: s.end + delta }; | |
| } | |
| return s; | |
| }); | |
| return { text: newText, suggestions: rebased }; | |
| } | |
| /** | |
| * Remove a suggestion from the list (after applying or dismissing). | |
| * Source: src/js/editor.js L520-523 (UUID-based filter) | |
| * | |
| * @param {Array} suggestions - Current suggestions array | |
| * @param {string} suggestionId - ID of suggestion to remove | |
| * @returns {Array} Filtered suggestions | |
| */ | |
| function removeSuggestion(suggestions, suggestionId) { | |
| return suggestions.filter((s) => s.id !== suggestionId); | |
| } | |
| /** | |
| * Count suggestions by type. | |
| * Source: src/js/editor.js L525-527 (type counting) | |
| * | |
| * @param {Array} suggestions | |
| * @returns {{ spelling: number, grammar: number, punctuation: number }} | |
| */ | |
| function countByType(suggestions) { | |
| return { | |
| spelling: suggestions.filter((s) => s.type === 'spelling').length, | |
| grammar: suggestions.filter((s) => s.type === 'grammar').length, | |
| punctuation: suggestions.filter((s) => s.type === 'punctuation').length, | |
| }; | |
| } | |