@inshapardaz/likhari-react 0.1.28 → 0.1.29-dev.265

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.
@@ -46,13 +46,14 @@ const en = {
46
46
  autocorrect: 'Auto-correct',
47
47
  textCleanup: 'Text cleanup',
48
48
  spellChecker: 'Spell-checker',
49
+ joinLines: 'Join lines',
49
50
  moreFormatting: 'More formatting',
50
51
  strikethrough: 'Strikethrough',
51
52
  superscript: 'Superscript',
52
53
  subscript: 'Subscript',
53
54
  uppercase: 'UPPERCASE',
54
55
  lowercase: 'lowercase',
55
- capitalize: 'Capitalize',
56
+ capitalize: 'InitCap',
56
57
  clearFormatting: 'Clear formatting',
57
58
  outdent: 'Outdent',
58
59
  indent: 'Indent',
@@ -178,6 +179,7 @@ const en = {
178
179
  },
179
180
  contextMenu: {
180
181
  cut: 'Cut',
182
+ joinLines: 'Join lines',
181
183
  copy: 'Copy',
182
184
  paste: 'Paste',
183
185
  selectAll: 'Select all',
@@ -322,6 +324,7 @@ const ur = {
322
324
  autocorrect: 'خودکار تصحیح',
323
325
  textCleanup: 'متن کی صفائی',
324
326
  spellChecker: 'ہجے چیک کرنے والا',
327
+ joinLines: 'سطریں ملائیں',
325
328
  moreFormatting: 'مزید فارمیٹنگ',
326
329
  strikethrough: 'خط زدہ',
327
330
  superscript: 'بالا نویس',
@@ -454,6 +457,7 @@ const ur = {
454
457
  },
455
458
  contextMenu: {
456
459
  cut: 'کاٹیں',
460
+ joinLines: 'سطریں ملائیں',
457
461
  copy: 'کاپی کریں',
458
462
  paste: 'چسپاں کریں',
459
463
  selectAll: 'سب منتخب کریں',
@@ -598,6 +602,7 @@ const paShahmukhi = {
598
602
  autocorrect: 'خودکار درستی',
599
603
  textCleanup: 'متن دی صفائی',
600
604
  spellChecker: 'ہجے چیکر',
605
+ joinLines: 'سطراں ملاؤ',
601
606
  moreFormatting: 'ہور فارمیٹنگ',
602
607
  strikethrough: 'کٹی لکیر',
603
608
  superscript: 'اُتلا لکھت',
@@ -730,6 +735,7 @@ const paShahmukhi = {
730
735
  },
731
736
  contextMenu: {
732
737
  cut: 'کٹو',
738
+ joinLines: 'سطراں ملاؤ',
733
739
  copy: 'کاپی کرو',
734
740
  paste: 'چسپاں کرو',
735
741
  selectAll: 'سارا چنو',
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { EditorRoot } from './EditorRoot.js';
2
2
  export type { EditorRootProps, EditorRef, EditorInitialContent, NavigationGuardMode } from './EditorRoot.js';
3
+ export type { ToolbarStyle } from './components/Toolbar.js';
3
4
  export type { DraftRestoreMode } from './components/DraftRestore.js';
4
5
  export { defaultMantineTheme } from './theme/mantineTheme.js';
5
6
  export { ImageNode, $createImageNode, $isImageNode } from './image/ImageNode.js';
@@ -8,7 +9,7 @@ export { DEFAULT_FONT_OPTIONS, URDU_WEB_FONT_OPTIONS, FONT_SIZES_PX } from './fo
8
9
  export type { FontOption } from './fonts/index.js';
9
10
  export { STRINGS, getStrings, useStrings, useUiStrings, UiStringsContext } from './i18n/index.js';
10
11
  export type { Locale, Strings } from './i18n/index.js';
11
- export { registerSpellDictionary, hasSpellDictionary, type HunspellFiles, type SpellLanguage } from './spellcheck/spellDictionaries.js';
12
+ export { registerSpellDictionary, hasSpellDictionary, setEnglishDictionaryBaseUrl, type HunspellFiles, type SpellLanguage } from './spellcheck/spellDictionaries.js';
12
13
  export { localStorageAutoCorrectStore, apiAutoCorrectStore, fileAutoCorrectStore } from './autocorrect/autoCorrectStores.js';
13
14
  export type { AutoCorrectStore, AutoCorrectEntry } from './autocorrect/autoCorrectStores.js';
14
15
  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,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,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';
@@ -0,0 +1,9 @@
1
+ export declare function $canJoinLines(): boolean;
2
+ /**
3
+ * Joins the lines in the selection into one paragraph. Soft breaks and the
4
+ * paragraph breaks between selected paragraphs become a single space each,
5
+ * unless there is already a space at that point. A soft break right after the
6
+ * selection becomes a paragraph break, so the joined text stands on its own.
7
+ */
8
+ export declare function $joinSelectedLines(): boolean;
9
+ //# sourceMappingURL=joinLinesActions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"joinLinesActions.d.ts","sourceRoot":"","sources":["../../src/join/joinLinesActions.ts"],"names":[],"mappings":"AAyBA,wBAAgB,aAAa,IAAI,OAAO,CAKvC;AAuCD;;;;;GAKG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAyB5C"}
@@ -0,0 +1,89 @@
1
+ import { $createTextNode, $getSelection, $isLineBreakNode, $isParagraphNode, $isRangeSelection, $createParagraphNode, } from 'lexical';
2
+ function $paragraphsInSelection(selection) {
3
+ const seen = new Set();
4
+ const paragraphs = [];
5
+ for (const node of selection.getNodes()) {
6
+ const block = node.getTopLevelElement();
7
+ if (!block || !$isParagraphNode(block) || seen.has(block.getKey()))
8
+ continue;
9
+ seen.add(block.getKey());
10
+ paragraphs.push(block);
11
+ }
12
+ return paragraphs;
13
+ }
14
+ export function $canJoinLines() {
15
+ const selection = $getSelection();
16
+ if (!$isRangeSelection(selection) || selection.isCollapsed())
17
+ return false;
18
+ if ($paragraphsInSelection(selection).length > 1)
19
+ return true;
20
+ return selection.getNodes().some($isLineBreakNode);
21
+ }
22
+ /** A soft break becomes one space, unless the text either side already has a space there. */
23
+ function $replaceBreakWithSpace(lineBreak) {
24
+ const before = lineBreak.getPreviousSibling()?.getTextContent() ?? '';
25
+ const after = lineBreak.getNextSibling()?.getTextContent() ?? '';
26
+ if (before === '' || after === '' || /\s$/.test(before) || /^\s/.test(after)) {
27
+ lineBreak.remove();
28
+ return;
29
+ }
30
+ lineBreak.replace($createTextNode(' '));
31
+ }
32
+ /** Moves a paragraph's content onto the end of the target, with a space between when needed. */
33
+ function $appendParagraph(target, paragraph) {
34
+ const content = paragraph.getTextContent();
35
+ if (content !== '') {
36
+ const targetText = target.getTextContent();
37
+ if (targetText !== '' && !/\s$/.test(targetText) && !/^\s/.test(content)) {
38
+ target.append($createTextNode(' '));
39
+ }
40
+ for (const child of paragraph.getChildren())
41
+ target.append(child);
42
+ }
43
+ paragraph.remove();
44
+ }
45
+ /** Turns a soft break into a paragraph break: the content after it moves to a new paragraph. */
46
+ function $splitParagraphAt(lineBreak, paragraph) {
47
+ const rest = $createParagraphNode();
48
+ let sibling = lineBreak.getNextSibling();
49
+ while (sibling) {
50
+ const next = sibling.getNextSibling();
51
+ rest.append(sibling);
52
+ sibling = next;
53
+ }
54
+ lineBreak.remove();
55
+ paragraph.insertAfter(rest);
56
+ }
57
+ /**
58
+ * Joins the lines in the selection into one paragraph. Soft breaks and the
59
+ * paragraph breaks between selected paragraphs become a single space each,
60
+ * unless there is already a space at that point. A soft break right after the
61
+ * selection becomes a paragraph break, so the joined text stands on its own.
62
+ */
63
+ export function $joinSelectedLines() {
64
+ const selection = $getSelection();
65
+ if (!$isRangeSelection(selection) || selection.isCollapsed())
66
+ return false;
67
+ const paragraphs = $paragraphsInSelection(selection);
68
+ const nodes = selection.getNodes();
69
+ const lineBreaks = nodes.filter($isLineBreakNode);
70
+ if (paragraphs.length < 2 && lineBreaks.length === 0)
71
+ return false;
72
+ const last = nodes[nodes.length - 1];
73
+ const breakAfter = last?.getNextSibling();
74
+ const trailingBreak = $isLineBreakNode(breakAfter) ? breakAfter : null;
75
+ for (const lineBreak of lineBreaks)
76
+ $replaceBreakWithSpace(lineBreak);
77
+ const [target, ...rest] = paragraphs;
78
+ if (!target)
79
+ return false;
80
+ for (const paragraph of rest)
81
+ $appendParagraph(target, paragraph);
82
+ if (trailingBreak?.isAttached()) {
83
+ const paragraph = trailingBreak.getParent();
84
+ if ($isParagraphNode(paragraph))
85
+ $splitParagraphAt(trailingBreak, paragraph);
86
+ }
87
+ target.selectEnd();
88
+ return true;
89
+ }
@@ -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
+ }