@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.
- package/dist/EditorRoot.d.ts +3 -0
- package/dist/EditorRoot.d.ts.map +1 -1
- package/dist/EditorRoot.js +2 -2
- package/dist/components/Toolbar.d.ts +9 -1
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +68 -154
- package/dist/i18n/strings.d.ts +3 -0
- package/dist/i18n/strings.d.ts.map +1 -1
- package/dist/i18n/strings.js +7 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/join/joinLinesActions.d.ts +9 -0
- package/dist/join/joinLinesActions.d.ts.map +1 -0
- package/dist/join/joinLinesActions.js +89 -0
- package/dist/spellcheck/SpellHighlightPlugin.d.ts +5 -3
- package/dist/spellcheck/SpellHighlightPlugin.d.ts.map +1 -1
- package/dist/spellcheck/SpellHighlightPlugin.js +164 -28
- package/dist/spellcheck/spellDictionaries.d.ts +2 -0
- package/dist/spellcheck/spellDictionaries.d.ts.map +1 -1
- package/dist/spellcheck/spellDictionaries.js +12 -2
- package/dist/spellcheck/spellcheckActions.d.ts +13 -0
- package/dist/spellcheck/spellcheckActions.d.ts.map +1 -1
- package/dist/spellcheck/spellcheckActions.js +35 -0
- package/dist/theme/editor.css +26 -73
- package/package.json +5 -5
package/dist/i18n/strings.js
CHANGED
|
@@ -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: '
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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":"
|
|
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 { $
|
|
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
|
-
|
|
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
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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;
|
|
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(
|
|
15
|
-
dic: await fetchText(
|
|
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
|
+
}
|