File size: 6,126 Bytes
74411f9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/**
 * 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,
  };
}