@inshapardaz/likhari-react 0.1.27 → 0.1.28-dev.254

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export { DEFAULT_FONT_OPTIONS, URDU_WEB_FONT_OPTIONS, FONT_SIZES_PX } from './fo
8
8
  export type { FontOption } from './fonts/index.js';
9
9
  export { STRINGS, getStrings, useStrings, useUiStrings, UiStringsContext } from './i18n/index.js';
10
10
  export type { Locale, Strings } from './i18n/index.js';
11
- export { registerSpellDictionary, hasSpellDictionary, type HunspellFiles, type SpellLanguage } from './spellcheck/spellDictionaries.js';
11
+ export { registerSpellDictionary, hasSpellDictionary, setEnglishDictionaryBaseUrl, type HunspellFiles, type SpellLanguage } from './spellcheck/spellDictionaries.js';
12
12
  export { localStorageAutoCorrectStore, apiAutoCorrectStore, fileAutoCorrectStore } from './autocorrect/autoCorrectStores.js';
13
13
  export type { AutoCorrectStore, AutoCorrectEntry } from './autocorrect/autoCorrectStores.js';
14
14
  export { normalizeUrdu, normalizeUrduCharacters, removeUrduDiacritics, replaceUrduDigits } from './normalization/urduNormalize.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAC1G,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC9E,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC1F,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrF,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AACzF,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,KAAK,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,gCAAgC,CAAC;AACrI,OAAO,EAAE,4BAA4B,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAC1H,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAC1F,OAAO,EAAE,aAAa,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAChI,YAAY,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAC9E,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAC1F,OAAO,EAAE,yBAAyB,EAAE,gBAAgB,EAAE,UAAU,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAClI,YAAY,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAChG,YAAY,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAC1G,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC9E,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC1F,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrF,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AACzF,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,2BAA2B,EAAE,KAAK,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAClK,OAAO,EAAE,4BAA4B,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAC1H,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAC1F,OAAO,EAAE,aAAa,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAChI,YAAY,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAC9E,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAC1F,OAAO,EAAE,yBAAyB,EAAE,gBAAgB,EAAE,UAAU,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAClI,YAAY,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAChG,YAAY,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC"}
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ export { defaultMantineTheme } from './theme/mantineTheme.js';
3
3
  export { ImageNode, $createImageNode, $isImageNode } from './image/ImageNode.js';
4
4
  export { DEFAULT_FONT_OPTIONS, URDU_WEB_FONT_OPTIONS, FONT_SIZES_PX } from './fonts/index.js';
5
5
  export { STRINGS, getStrings, useStrings, useUiStrings, UiStringsContext } from './i18n/index.js';
6
- export { registerSpellDictionary, hasSpellDictionary } from './spellcheck/spellDictionaries.js';
6
+ export { registerSpellDictionary, hasSpellDictionary, setEnglishDictionaryBaseUrl } from './spellcheck/spellDictionaries.js';
7
7
  export { localStorageAutoCorrectStore, apiAutoCorrectStore, fileAutoCorrectStore } from './autocorrect/autoCorrectStores.js';
8
8
  export { normalizeUrdu, normalizeUrduCharacters, removeUrduDiacritics, replaceUrduDigits } from './normalization/urduNormalize.js';
9
9
  export { localStorageUserWordStore, apiUserWordStore, ignoreWord, addUserWord, onSpellWordsChange } from './spellcheck/userWords.js';
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Underlines misspelled words the way the browser does, without changing the
3
- * document: ranges are set on the rendered text through the CSS Custom
4
- * Highlight API. Checking is debounced so typing stays smooth. In browsers
5
- * without the API, nothing is underlined.
3
+ * document. Only blocks that are in view are checked, a block a few seconds after
4
+ * it scrolls into view, and an edit is rechecked after typing pauses. Results are
5
+ * kept per block, so a change re-checks only the block it is in. Underlines are
6
+ * ranges set through the CSS Custom Highlight API. In browsers without it, nothing
7
+ * is underlined.
6
8
  */
7
9
  export declare function SpellHighlightPlugin(): null;
8
10
  //# sourceMappingURL=SpellHighlightPlugin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SpellHighlightPlugin.d.ts","sourceRoot":"","sources":["../../src/spellcheck/SpellHighlightPlugin.tsx"],"names":[],"mappings":"AAUA;;;;;GAKG;AACH,wBAAgB,oBAAoB,SAkDnC"}
1
+ {"version":3,"file":"SpellHighlightPlugin.d.ts","sourceRoot":"","sources":["../../src/spellcheck/SpellHighlightPlugin.tsx"],"names":[],"mappings":"AAsBA;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,SAoJnC"}
@@ -1,60 +1,196 @@
1
1
  import { useEffect } from 'react';
2
2
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
3
- import { $collectMisspellings } from './spellcheckActions.js';
3
+ import { $getNodeByKey, $getRoot, $isElementNode } from 'lexical';
4
+ import { $misspellingsInBlock } from './spellcheckActions.js';
4
5
  import { getSpeller } from './spellDictionaries.js';
5
6
  import { onSpellWordsChange } from './userWords.js';
6
7
  /** The CSS Custom Highlight name; editor.css styles it as a red wavy underline. */
7
8
  const HIGHLIGHT = 'likhari-spelling';
8
- const DEBOUNCE_MS = 300;
9
+ /** A block that has just come into view is checked after this long, so scrolling past it costs nothing. */
10
+ const VIEW_DELAY_MS = 2000;
11
+ /** An edit inside a visible block is checked after the typing pauses for this long. */
12
+ const TYPING_DELAY_MS = 300;
13
+ /** The top-level block (paragraph, table, list...) that holds a node, or null. */
14
+ function $topLevelKey(node) {
15
+ const root = $getRoot();
16
+ let current = node;
17
+ while (current.getParent() && !current.getParent().is(root))
18
+ current = current.getParent();
19
+ return current.getParent() ? current.getKey() : null;
20
+ }
9
21
  /**
10
22
  * Underlines misspelled words the way the browser does, without changing the
11
- * document: ranges are set on the rendered text through the CSS Custom
12
- * Highlight API. Checking is debounced so typing stays smooth. In browsers
13
- * without the API, nothing is underlined.
23
+ * document. Only blocks that are in view are checked, a block a few seconds after
24
+ * it scrolls into view, and an edit is rechecked after typing pauses. Results are
25
+ * kept per block, so a change re-checks only the block it is in. Underlines are
26
+ * ranges set through the CSS Custom Highlight API. In browsers without it, nothing
27
+ * is underlined.
14
28
  */
15
29
  export function SpellHighlightPlugin() {
16
30
  const [editor] = useLexicalComposerContext();
17
31
  useEffect(() => {
18
- if (typeof CSS === 'undefined' || !('highlights' in CSS))
32
+ if (typeof CSS === 'undefined' || !('highlights' in CSS) || typeof IntersectionObserver === 'undefined')
19
33
  return;
20
34
  let checkers = null;
21
- let timer;
22
35
  let active = true;
36
+ const cache = new Map();
37
+ const visible = new Set();
38
+ const elements = new Map();
39
+ const keyOf = new Map();
40
+ const timers = new Map();
23
41
  const paint = () => {
24
- if (!active || !checkers)
25
- return;
26
- const found = editor.getEditorState().read(() => $collectMisspellings(checkers));
27
42
  const ranges = [];
28
- for (const misspelling of found) {
29
- for (const segment of misspelling.segments) {
30
- const text = editor.getElementByKey(segment.key)?.firstChild;
31
- if (!text || text.nodeType !== Node.TEXT_NODE)
32
- continue;
33
- const range = document.createRange();
34
- range.setStart(text, segment.start);
35
- range.setEnd(text, segment.end);
36
- ranges.push(range);
43
+ for (const [key, words] of cache) {
44
+ const block = editor.getElementByKey(key);
45
+ if (!block || words.length === 0)
46
+ continue;
47
+ const pieces = textPieces(block);
48
+ for (const word of words) {
49
+ for (const piece of pieces) {
50
+ const start = Math.max(word.start, piece.start);
51
+ const end = Math.min(word.end, piece.end);
52
+ if (end <= start)
53
+ continue;
54
+ const range = document.createRange();
55
+ range.setStart(piece.node, start - piece.start);
56
+ range.setEnd(piece.node, end - piece.start);
57
+ ranges.push(range);
58
+ }
37
59
  }
38
60
  }
39
61
  CSS.highlights.set(HIGHLIGHT, new Highlight(...ranges));
40
62
  };
41
- const schedule = () => {
42
- clearTimeout(timer);
43
- timer = setTimeout(paint, DEBOUNCE_MS);
63
+ const checkBlock = (key) => {
64
+ if (!active || !checkers)
65
+ return;
66
+ const words = editor.getEditorState().read(() => {
67
+ const node = $getNodeByKey(key);
68
+ return $isElementNode(node) ? $misspellingsInBlock(node, checkers) : null;
69
+ });
70
+ if (words === null)
71
+ cache.delete(key);
72
+ else
73
+ cache.set(key, words);
74
+ paint();
75
+ };
76
+ const schedule = (key, delay) => {
77
+ clearTimeout(timers.get(key));
78
+ timers.set(key, setTimeout(() => {
79
+ timers.delete(key);
80
+ if (visible.has(key))
81
+ checkBlock(key);
82
+ }, delay));
83
+ };
84
+ const observer = new IntersectionObserver((entries) => {
85
+ for (const entry of entries) {
86
+ const key = keyOf.get(entry.target);
87
+ if (key === undefined)
88
+ continue;
89
+ if (entry.isIntersecting) {
90
+ visible.add(key);
91
+ if (!cache.has(key))
92
+ schedule(key, VIEW_DELAY_MS);
93
+ }
94
+ else {
95
+ visible.delete(key);
96
+ clearTimeout(timers.get(key));
97
+ timers.delete(key);
98
+ }
99
+ }
100
+ });
101
+ // Keeps the observer on exactly the current top-level blocks.
102
+ const syncBlocks = (editorState = editor.getEditorState()) => {
103
+ const current = new Map();
104
+ editorState.read(() => {
105
+ for (const block of $getRoot().getChildren()) {
106
+ const element = editor.getElementByKey(block.getKey());
107
+ if (element)
108
+ current.set(block.getKey(), element);
109
+ }
110
+ });
111
+ for (const [key, element] of elements) {
112
+ if (current.get(key) !== element) {
113
+ observer.unobserve(element);
114
+ keyOf.delete(element);
115
+ elements.delete(key);
116
+ visible.delete(key);
117
+ cache.delete(key);
118
+ clearTimeout(timers.get(key));
119
+ timers.delete(key);
120
+ }
121
+ }
122
+ for (const [key, element] of current) {
123
+ if (!elements.has(key)) {
124
+ elements.set(key, element);
125
+ keyOf.set(element, key);
126
+ observer.observe(element);
127
+ }
128
+ }
44
129
  };
45
- const unregister = editor.registerUpdateListener(schedule);
46
- const unsubscribe = onSpellWordsChange(schedule);
130
+ const unregisterUpdate = editor.registerUpdateListener(({ editorState, dirtyElements, dirtyLeaves }) => {
131
+ if (!checkers)
132
+ return;
133
+ const dirty = new Set();
134
+ editorState.read(() => {
135
+ for (const key of dirtyLeaves) {
136
+ const node = $getNodeByKey(key);
137
+ const top = node ? $topLevelKey(node) : null;
138
+ if (top)
139
+ dirty.add(top);
140
+ }
141
+ for (const key of dirtyElements.keys()) {
142
+ const node = $getNodeByKey(key);
143
+ const top = node ? $topLevelKey(node) : null;
144
+ if (top)
145
+ dirty.add(top);
146
+ }
147
+ });
148
+ syncBlocks(editorState);
149
+ for (const key of dirty) {
150
+ if (visible.has(key))
151
+ schedule(key, TYPING_DELAY_MS);
152
+ else
153
+ cache.delete(key);
154
+ }
155
+ if (dirty.size > 0)
156
+ paint();
157
+ });
158
+ const unsubscribeWords = onSpellWordsChange(() => {
159
+ cache.clear();
160
+ for (const key of visible)
161
+ schedule(key, 0);
162
+ paint();
163
+ });
47
164
  Promise.all([getSpeller('en'), getSpeller('ur')]).then(async ([en, ur]) => {
165
+ if (!active)
166
+ return;
48
167
  checkers = { ltr: en ? await en : undefined, rtl: ur ? await ur : undefined };
49
- schedule();
168
+ syncBlocks();
169
+ for (const key of visible)
170
+ if (!cache.has(key))
171
+ schedule(key, 0);
50
172
  });
51
173
  return () => {
52
174
  active = false;
53
- clearTimeout(timer);
54
- unregister();
55
- unsubscribe();
175
+ observer.disconnect();
176
+ unregisterUpdate();
177
+ unsubscribeWords();
178
+ for (const timer of timers.values())
179
+ clearTimeout(timer);
56
180
  CSS.highlights.delete(HIGHLIGHT);
57
181
  };
58
182
  }, [editor]);
59
183
  return null;
60
184
  }
185
+ /** The text nodes of a rendered block, with where each one starts in the block's text. */
186
+ function textPieces(block) {
187
+ const pieces = [];
188
+ const walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT);
189
+ let offset = 0;
190
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
191
+ const text = node;
192
+ pieces.push({ node: text, start: offset, end: offset + text.length });
193
+ offset += text.length;
194
+ }
195
+ return pieces;
196
+ }
@@ -12,6 +12,8 @@ export interface Speller {
12
12
  suggest(word: string): string[];
13
13
  }
14
14
  type Loader = () => Promise<HunspellFiles>;
15
+ /** Sets where the English dictionary files are served from. Takes effect on the next load. */
16
+ export declare function setEnglishDictionaryBaseUrl(url: string): void;
15
17
  /**
16
18
  * Registers a dictionary for a language. Call once at startup with files you
17
19
  * have licence to ship — e.g. the Urdu `.aff`/`.dic` pair — either as strings
@@ -1 +1 @@
1
- {"version":3,"file":"spellDictionaries.d.ts","sourceRoot":"","sources":["../../src/spellcheck/spellDictionaries.ts"],"names":[],"mappings":"AAGA,4DAA4D;AAC5D,MAAM,MAAM,aAAa,GAAG,IAAI,GAAG,IAAI,GAAG,cAAc,CAAC;AAEzD,wFAAwF;AACxF,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ,qEAAqE;IACrE,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,OAAO;IACtB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CACjC;AAED,KAAK,MAAM,GAAG,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;AAoB3C;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAIpG;AAED,2DAA2D;AAC3D,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAEnE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAc3E;AAYD,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAOrD;AAED,iFAAiF;AACjF,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,CAGpF;AAED,gEAAgE;AAChE,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AASD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,CAMhD;AAED,0DAA0D;AAC1D,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,QAAQ,EAAE,CAEzE"}
1
+ {"version":3,"file":"spellDictionaries.d.ts","sourceRoot":"","sources":["../../src/spellcheck/spellDictionaries.ts"],"names":[],"mappings":"AAGA,4DAA4D;AAC5D,MAAM,MAAM,aAAa,GAAG,IAAI,GAAG,IAAI,GAAG,cAAc,CAAC;AAEzD,wFAAwF;AACxF,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ,qEAAqE;IACrE,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,OAAO;IACtB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CACjC;AAED,KAAK,MAAM,GAAG,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;AAiB3C,8FAA8F;AAC9F,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAI7D;AAUD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAIpG;AAED,2DAA2D;AAC3D,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAEnE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAc3E;AAYD,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAOrD;AAED,iFAAiF;AACjF,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,CAGpF;AAED,gEAAgE;AAChE,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AASD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,CAMhD;AAED,0DAA0D;AAC1D,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,QAAQ,EAAE,CAEzE"}
@@ -9,10 +9,20 @@ async function fetchText(url) {
9
9
  throw new Error(`Spellcheck dictionary failed to load: ${url}`);
10
10
  return response.text();
11
11
  }
12
+ // Where the English dictionary files are. Built from this module's own URL, so the
13
+ // files shipped next to it are found; a host that bundles the editor can point this
14
+ // elsewhere with setEnglishDictionaryBaseUrl.
15
+ let englishBaseUrl = import.meta.url.replace(/[^/]*$/, 'dictionaries/en/');
16
+ /** Sets where the English dictionary files are served from. Takes effect on the next load. */
17
+ export function setEnglishDictionaryBaseUrl(url) {
18
+ englishBaseUrl = url.endsWith('/') ? url : `${url}/`;
19
+ loadedFiles.delete('en');
20
+ spellers.delete('en');
21
+ }
12
22
  /** English ships with the editor: its files are fetched on first use. */
13
23
  const englishLoader = async () => ({
14
- aff: await fetchText(new URL('./dictionaries/en/index.aff', import.meta.url).href),
15
- dic: await fetchText(new URL('./dictionaries/en/index.dic', import.meta.url).href),
24
+ aff: await fetchText(`${englishBaseUrl}index.aff`),
25
+ dic: await fetchText(`${englishBaseUrl}index.dic`),
16
26
  });
17
27
  loaders.set('en', englishLoader);
18
28
  /**
@@ -1,3 +1,4 @@
1
+ import type { ElementNode } from 'lexical';
1
2
  import { type FindMatch } from '../find/findReplaceActions.js';
2
3
  import { type Speller } from './spellDictionaries.js';
3
4
  /** The checker for each text direction: English for LTR blocks, Urdu for RTL. */
@@ -18,4 +19,16 @@ export interface Misspelling {
18
19
  }
19
20
  /** Every word in the document its block's checker does not recognise. */
20
21
  export declare function $collectMisspellings(checkers: Checkers): Misspelling[];
22
+ /** A misspelled word inside a block, as offsets into the block's text. */
23
+ export interface BlockMisspelling {
24
+ start: number;
25
+ end: number;
26
+ word: string;
27
+ }
28
+ /**
29
+ * The misspelled words inside one top-level block (a paragraph, a table, a list...),
30
+ * as offsets into the block's text. Words are found per parent (paragraph or cell),
31
+ * so text in different cells never forms one word. Must run inside an editor read.
32
+ */
33
+ export declare function $misspellingsInBlock(block: ElementNode, checkers: Checkers): BlockMisspelling[];
21
34
  //# sourceMappingURL=spellcheckActions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"spellcheckActions.d.ts","sourceRoot":"","sources":["../../src/spellcheck/spellcheckActions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAClF,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAE5D,iFAAiF;AACjF,MAAM,WAAW,QAAQ;IACvB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,qEAAqE;AACrE,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,yEAAyE;AACzE,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW,EAAE,CAmBtE"}
1
+ {"version":3,"file":"spellcheckActions.d.ts","sourceRoot":"","sources":["../../src/spellcheck/spellcheckActions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAwB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAClF,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAE5D,iFAAiF;AACjF,MAAM,WAAW,QAAQ;IACvB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,qEAAqE;AACrE,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,yEAAyE;AACzE,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW,EAAE,CAmBtE;AAeD,0EAA0E;AAC1E,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,GAAG,gBAAgB,EAAE,CA8B/F"}
@@ -36,3 +36,38 @@ function segmentsOf(group, start, end) {
36
36
  }
37
37
  return segments;
38
38
  }
39
+ /**
40
+ * The misspelled words inside one top-level block (a paragraph, a table, a list...),
41
+ * as offsets into the block's text. Words are found per parent (paragraph or cell),
42
+ * so text in different cells never forms one word. Must run inside an editor read.
43
+ */
44
+ export function $misspellingsInBlock(block, checkers) {
45
+ const nodes = block.getAllTextNodes();
46
+ const starts = [];
47
+ let offset = 0;
48
+ for (const node of nodes) {
49
+ starts.push(offset);
50
+ offset += node.getTextContent().length;
51
+ }
52
+ const found = [];
53
+ let i = 0;
54
+ while (i < nodes.length) {
55
+ const parent = nodes[i].getParentOrThrow();
56
+ let j = i;
57
+ let text = '';
58
+ while (j < nodes.length && nodes[j].getParentOrThrow().getKey() === parent.getKey()) {
59
+ text += nodes[j].getTextContent();
60
+ j += 1;
61
+ }
62
+ const speller = parent.getDirection() === 'rtl' ? checkers.rtl : checkers.ltr;
63
+ if (speller) {
64
+ for (const span of wordsIn(text)) {
65
+ if (!speller.correct(span.word)) {
66
+ found.push({ start: starts[i] + span.start, end: starts[i] + span.end, word: span.word });
67
+ }
68
+ }
69
+ }
70
+ i = j;
71
+ }
72
+ return found;
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inshapardaz/likhari-react",
3
- "version": "0.1.27",
3
+ "version": "0.1.28-dev.254",
4
4
  "description": "React implementation of the Likhari editor, built on @lexical/react.",
5
5
  "license": "MIT",
6
6
  "repository": {