@inshapardaz/likhari-react 0.1.21 → 0.1.22-dev.218
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 +12 -0
- package/dist/EditorRoot.d.ts.map +1 -1
- package/dist/EditorRoot.js +21 -4
- package/dist/autocorrect/AutoCorrectPlugin.d.ts +27 -0
- package/dist/autocorrect/AutoCorrectPlugin.d.ts.map +1 -0
- package/dist/autocorrect/AutoCorrectPlugin.js +108 -0
- package/dist/autocorrect/autoCorrectActions.d.ts +35 -0
- package/dist/autocorrect/autoCorrectActions.d.ts.map +1 -0
- package/dist/autocorrect/autoCorrectActions.js +182 -0
- package/dist/autocorrect/autoCorrectStores.d.ts +52 -0
- package/dist/autocorrect/autoCorrectStores.d.ts.map +1 -0
- package/dist/autocorrect/autoCorrectStores.js +115 -0
- package/dist/autocorrect/punctuationRules.d.ts +26 -0
- package/dist/autocorrect/punctuationRules.d.ts.map +1 -0
- package/dist/autocorrect/punctuationRules.js +53 -0
- package/dist/cleanup/textCleanupActions.d.ts +8 -0
- package/dist/cleanup/textCleanupActions.d.ts.map +1 -0
- package/dist/cleanup/textCleanupActions.js +28 -0
- package/dist/components/AutoCorrectPanel.d.ts +15 -0
- package/dist/components/AutoCorrectPanel.d.ts.map +1 -0
- package/dist/components/AutoCorrectPanel.js +44 -0
- package/dist/components/SpellcheckPanel.d.ts +15 -0
- package/dist/components/SpellcheckPanel.d.ts.map +1 -0
- package/dist/components/SpellcheckPanel.js +91 -0
- package/dist/components/Toolbar.d.ts +10 -1
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +125 -109
- package/dist/find/findReplaceActions.d.ts +13 -0
- package/dist/find/findReplaceActions.d.ts.map +1 -1
- package/dist/find/findReplaceActions.js +2 -2
- package/dist/i18n/strings.d.ts +34 -0
- package/dist/i18n/strings.d.ts.map +1 -1
- package/dist/i18n/strings.js +107 -5
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/normalization/urduNormalize.d.ts +24 -0
- package/dist/normalization/urduNormalize.d.ts.map +1 -0
- package/dist/normalization/urduNormalize.js +116 -0
- package/dist/spellcheck/SpellHighlightPlugin.d.ts +8 -0
- package/dist/spellcheck/SpellHighlightPlugin.d.ts.map +1 -0
- package/dist/spellcheck/SpellHighlightPlugin.js +60 -0
- package/dist/spellcheck/spellDictionaries.d.ts.map +1 -1
- package/dist/spellcheck/spellDictionaries.js +8 -1
- package/dist/spellcheck/spellcheckActions.d.ts +21 -0
- package/dist/spellcheck/spellcheckActions.d.ts.map +1 -0
- package/dist/spellcheck/spellcheckActions.js +38 -0
- package/dist/spellcheck/spellingMenu.d.ts +15 -0
- package/dist/spellcheck/spellingMenu.d.ts.map +1 -0
- package/dist/spellcheck/spellingMenu.js +27 -0
- package/dist/spellcheck/userWords.d.ts +39 -0
- package/dist/spellcheck/userWords.d.ts.map +1 -0
- package/dist/spellcheck/userWords.js +132 -0
- package/dist/theme/editor.css +185 -0
- package/package.json +1 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Urdu character normalisation, ported from urduhack's `normalization/character.py`
|
|
3
|
+
* (MIT licence, https://github.com/urduhack/urduhack). The mapping replaces
|
|
4
|
+
* Arabic and presentation-form letters with the standard Urdu letter, and
|
|
5
|
+
* the tables below keep the same entries and order as the original, so the
|
|
6
|
+
* output matches it character for character.
|
|
7
|
+
*/
|
|
8
|
+
/** Replaces Arabic and presentation-form letters with standard Urdu letters, character by character. */
|
|
9
|
+
export declare function normalizeUrduCharacters(text: string): string;
|
|
10
|
+
export declare function removeUrduDiacritics(text: string): string;
|
|
11
|
+
/** Converts Urdu digits to English digits, or the reverse. */
|
|
12
|
+
export declare function replaceUrduDigits(text: string, to: 'english' | 'urdu'): string;
|
|
13
|
+
export interface UrduNormalizationOptions {
|
|
14
|
+
/** Remove harakat. Off by default, since they are meaningful in poetry and religious text. */
|
|
15
|
+
removeDiacritics?: boolean;
|
|
16
|
+
/** Digit style to convert to; 'keep' (the default) leaves digits as they are. */
|
|
17
|
+
digits?: 'keep' | 'english' | 'urdu';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The normalisation urduhack's `normalize` applies, with the diacritic and
|
|
21
|
+
* digit steps made optional. Character mapping always runs.
|
|
22
|
+
*/
|
|
23
|
+
export declare function normalizeUrdu(text: string, options?: UrduNormalizationOptions): string;
|
|
24
|
+
//# sourceMappingURL=urduNormalize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"urduNormalize.d.ts","sourceRoot":"","sources":["../../src/normalization/urduNormalize.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA8EH,wGAAwG;AACxG,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI5D;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIzD;AAED,8DAA8D;AAC9D,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,GAAG,MAAM,GAAG,MAAM,CAS9E;AAED,MAAM,WAAW,wBAAwB;IACvC,8FAA8F;IAC9F,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;CACtC;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,wBAA6B,GAAG,MAAM,CAK1F"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Urdu character normalisation, ported from urduhack's `normalization/character.py`
|
|
3
|
+
* (MIT licence, https://github.com/urduhack/urduhack). The mapping replaces
|
|
4
|
+
* Arabic and presentation-form letters with the standard Urdu letter, and
|
|
5
|
+
* the tables below keep the same entries and order as the original, so the
|
|
6
|
+
* output matches it character for character.
|
|
7
|
+
*/
|
|
8
|
+
const CORRECT_URDU_CHARACTERS = {
|
|
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
|
+
const TRANSLATOR = new Map();
|
|
73
|
+
for (const [key, values] of Object.entries(CORRECT_URDU_CHARACTERS)) {
|
|
74
|
+
for (const value of values)
|
|
75
|
+
TRANSLATOR.set(value, key);
|
|
76
|
+
}
|
|
77
|
+
/** Combining diacritics (harakat and similar), as urduhack's URDU_DIACRITICS. */
|
|
78
|
+
const DIACRITICS = new Set(['َ', 'ً', 'ٰ', 'ِ', 'ُ', 'ٍ']);
|
|
79
|
+
const ENGLISH_DIGITS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
|
80
|
+
const URDU_DIGITS = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
|
|
81
|
+
/** Replaces Arabic and presentation-form letters with standard Urdu letters, character by character. */
|
|
82
|
+
export function normalizeUrduCharacters(text) {
|
|
83
|
+
let out = '';
|
|
84
|
+
for (const char of text)
|
|
85
|
+
out += TRANSLATOR.get(char) ?? char;
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
export function removeUrduDiacritics(text) {
|
|
89
|
+
let out = '';
|
|
90
|
+
for (const char of text)
|
|
91
|
+
if (!DIACRITICS.has(char))
|
|
92
|
+
out += char;
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/** Converts Urdu digits to English digits, or the reverse. */
|
|
96
|
+
export function replaceUrduDigits(text, to) {
|
|
97
|
+
const from = to === 'english' ? URDU_DIGITS : ENGLISH_DIGITS;
|
|
98
|
+
const target = to === 'english' ? ENGLISH_DIGITS : URDU_DIGITS;
|
|
99
|
+
let out = '';
|
|
100
|
+
for (const char of text) {
|
|
101
|
+
const index = from.indexOf(char);
|
|
102
|
+
out += index === -1 ? char : target[index];
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The normalisation urduhack's `normalize` applies, with the diacritic and
|
|
108
|
+
* digit steps made optional. Character mapping always runs.
|
|
109
|
+
*/
|
|
110
|
+
export function normalizeUrdu(text, options = {}) {
|
|
111
|
+
let out = options.removeDiacritics ? removeUrduDiacritics(text) : text;
|
|
112
|
+
out = normalizeUrduCharacters(out);
|
|
113
|
+
if (options.digits === 'english' || options.digits === 'urdu')
|
|
114
|
+
out = replaceUrduDigits(out, options.digits);
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
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.
|
|
6
|
+
*/
|
|
7
|
+
export declare function SpellHighlightPlugin(): null;
|
|
8
|
+
//# sourceMappingURL=SpellHighlightPlugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SpellHighlightPlugin.d.ts","sourceRoot":"","sources":["../../src/spellcheck/SpellHighlightPlugin.tsx"],"names":[],"mappings":"AAUA;;;;;GAKG;AACH,wBAAgB,oBAAoB,SAkDnC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
3
|
+
import { $collectMisspellings } from './spellcheckActions.js';
|
|
4
|
+
import { getSpeller } from './spellDictionaries.js';
|
|
5
|
+
import { onSpellWordsChange } from './userWords.js';
|
|
6
|
+
/** The CSS Custom Highlight name; editor.css styles it as a red wavy underline. */
|
|
7
|
+
const HIGHLIGHT = 'likhari-spelling';
|
|
8
|
+
const DEBOUNCE_MS = 300;
|
|
9
|
+
/**
|
|
10
|
+
* 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.
|
|
14
|
+
*/
|
|
15
|
+
export function SpellHighlightPlugin() {
|
|
16
|
+
const [editor] = useLexicalComposerContext();
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (typeof CSS === 'undefined' || !('highlights' in CSS))
|
|
19
|
+
return;
|
|
20
|
+
let checkers = null;
|
|
21
|
+
let timer;
|
|
22
|
+
let active = true;
|
|
23
|
+
const paint = () => {
|
|
24
|
+
if (!active || !checkers)
|
|
25
|
+
return;
|
|
26
|
+
const found = editor.getEditorState().read(() => $collectMisspellings(checkers));
|
|
27
|
+
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);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
CSS.highlights.set(HIGHLIGHT, new Highlight(...ranges));
|
|
40
|
+
};
|
|
41
|
+
const schedule = () => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
timer = setTimeout(paint, DEBOUNCE_MS);
|
|
44
|
+
};
|
|
45
|
+
const unregister = editor.registerUpdateListener(schedule);
|
|
46
|
+
const unsubscribe = onSpellWordsChange(schedule);
|
|
47
|
+
Promise.all([getSpeller('en'), getSpeller('ur')]).then(async ([en, ur]) => {
|
|
48
|
+
checkers = { ltr: en ? await en : undefined, rtl: ur ? await ur : undefined };
|
|
49
|
+
schedule();
|
|
50
|
+
});
|
|
51
|
+
return () => {
|
|
52
|
+
active = false;
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
unregister();
|
|
55
|
+
unsubscribe();
|
|
56
|
+
CSS.highlights.delete(HIGHLIGHT);
|
|
57
|
+
};
|
|
58
|
+
}, [editor]);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spellDictionaries.d.ts","sourceRoot":"","sources":["../../src/spellcheck/spellDictionaries.ts"],"names":[],"mappings":"
|
|
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;AAmB3C;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAGpG;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,CAe3E;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,4 +1,5 @@
|
|
|
1
1
|
import nspell from 'nspell';
|
|
2
|
+
import { isAcceptedWord } from './userWords.js';
|
|
2
3
|
const loaders = new Map();
|
|
3
4
|
const spellers = new Map();
|
|
4
5
|
async function fetchText(url) {
|
|
@@ -38,7 +39,13 @@ export function getSpeller(language) {
|
|
|
38
39
|
return null;
|
|
39
40
|
let speller = spellers.get(language);
|
|
40
41
|
if (!speller) {
|
|
41
|
-
speller = loader()
|
|
42
|
+
speller = loader()
|
|
43
|
+
.then(({ aff, dic }) => nspell(aff, dic))
|
|
44
|
+
.then((base) => ({
|
|
45
|
+
// Words the user added or ignored count as correct, whatever the dictionary says.
|
|
46
|
+
correct: (word) => isAcceptedWord(language, word) || base.correct(word),
|
|
47
|
+
suggest: (word) => base.suggest(word),
|
|
48
|
+
}));
|
|
42
49
|
spellers.set(language, speller);
|
|
43
50
|
}
|
|
44
51
|
return speller;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type FindMatch } from '../find/findReplaceActions.js';
|
|
2
|
+
import { type Speller } from './spellDictionaries.js';
|
|
3
|
+
/** The checker for each text direction: English for LTR blocks, Urdu for RTL. */
|
|
4
|
+
export interface Checkers {
|
|
5
|
+
ltr?: Speller;
|
|
6
|
+
rtl?: Speller;
|
|
7
|
+
}
|
|
8
|
+
/** The part of a misspelled word that falls inside one text node. */
|
|
9
|
+
export interface Segment {
|
|
10
|
+
key: string;
|
|
11
|
+
start: number;
|
|
12
|
+
end: number;
|
|
13
|
+
}
|
|
14
|
+
export interface Misspelling {
|
|
15
|
+
word: string;
|
|
16
|
+
match: FindMatch;
|
|
17
|
+
segments: Segment[];
|
|
18
|
+
}
|
|
19
|
+
/** Every word in the document its block's checker does not recognise. */
|
|
20
|
+
export declare function $collectMisspellings(checkers: Checkers): Misspelling[];
|
|
21
|
+
//# sourceMappingURL=spellcheckActions.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { $textGroups, $locate } from '../find/findReplaceActions.js';
|
|
2
|
+
import { wordsIn } from './spellDictionaries.js';
|
|
3
|
+
/** Every word in the document its block's checker does not recognise. */
|
|
4
|
+
export function $collectMisspellings(checkers) {
|
|
5
|
+
const found = [];
|
|
6
|
+
for (const group of $textGroups()) {
|
|
7
|
+
const direction = group[0].getParentOrThrow().getDirection();
|
|
8
|
+
const speller = direction === 'rtl' ? checkers.rtl : checkers.ltr;
|
|
9
|
+
if (!speller)
|
|
10
|
+
continue;
|
|
11
|
+
const text = group.map((node) => node.getTextContent()).join('');
|
|
12
|
+
for (const span of wordsIn(text)) {
|
|
13
|
+
if (speller.correct(span.word))
|
|
14
|
+
continue;
|
|
15
|
+
const start = $locate(group, span.start, false);
|
|
16
|
+
const end = $locate(group, span.end, true);
|
|
17
|
+
found.push({
|
|
18
|
+
word: span.word,
|
|
19
|
+
match: { anchorKey: start.key, anchorOffset: start.offset, focusKey: end.key, focusOffset: end.offset },
|
|
20
|
+
segments: segmentsOf(group, span.start, span.end),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return found;
|
|
25
|
+
}
|
|
26
|
+
function segmentsOf(group, start, end) {
|
|
27
|
+
const segments = [];
|
|
28
|
+
let offset = 0;
|
|
29
|
+
for (const node of group) {
|
|
30
|
+
const length = node.getTextContent().length;
|
|
31
|
+
const from = Math.max(start, offset);
|
|
32
|
+
const to = Math.min(end, offset + length);
|
|
33
|
+
if (to > from)
|
|
34
|
+
segments.push({ key: node.getKey(), start: from - offset, end: to - offset });
|
|
35
|
+
offset += length;
|
|
36
|
+
}
|
|
37
|
+
return segments;
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FindMatch } from '../find/findReplaceActions.js';
|
|
2
|
+
/** A word under the pointer, as a Lexical range, with the direction of its block. */
|
|
3
|
+
export interface WordAtPoint {
|
|
4
|
+
word: string;
|
|
5
|
+
direction: 'ltr' | 'rtl';
|
|
6
|
+
match: FindMatch;
|
|
7
|
+
}
|
|
8
|
+
/** The DOM text node and offset under a viewport point, or null if the point is not on text. */
|
|
9
|
+
export declare function textPointAt(x: number, y: number): {
|
|
10
|
+
node: Node;
|
|
11
|
+
offset: number;
|
|
12
|
+
} | null;
|
|
13
|
+
/** The whole word around an offset in a Lexical text node. Must run inside an editor read or update. */
|
|
14
|
+
export declare function $wordAtDomPoint(domText: Node, offset: number): WordAtPoint | null;
|
|
15
|
+
//# sourceMappingURL=spellingMenu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spellingMenu.d.ts","sourceRoot":"","sources":["../../src/spellcheck/spellingMenu.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAG5D,qFAAqF;AACrF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC;IACzB,KAAK,EAAE,SAAS,CAAC;CAClB;AAED,gGAAgG;AAChG,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAUvF;AAED,wGAAwG;AACxG,wBAAgB,eAAe,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAWjF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { $getNearestNodeFromDOMNode, $isTextNode } from 'lexical';
|
|
2
|
+
import { wordsIn } from './spellDictionaries.js';
|
|
3
|
+
/** The DOM text node and offset under a viewport point, or null if the point is not on text. */
|
|
4
|
+
export function textPointAt(x, y) {
|
|
5
|
+
const doc = document;
|
|
6
|
+
if (doc.caretPositionFromPoint) {
|
|
7
|
+
const position = doc.caretPositionFromPoint(x, y);
|
|
8
|
+
return position && position.offsetNode.nodeType === Node.TEXT_NODE ? { node: position.offsetNode, offset: position.offset } : null;
|
|
9
|
+
}
|
|
10
|
+
const range = doc.caretRangeFromPoint?.(x, y);
|
|
11
|
+
return range && range.startContainer.nodeType === Node.TEXT_NODE ? { node: range.startContainer, offset: range.startOffset } : null;
|
|
12
|
+
}
|
|
13
|
+
/** The whole word around an offset in a Lexical text node. Must run inside an editor read or update. */
|
|
14
|
+
export function $wordAtDomPoint(domText, offset) {
|
|
15
|
+
const node = $getNearestNodeFromDOMNode(domText);
|
|
16
|
+
if (!$isTextNode(node))
|
|
17
|
+
return null;
|
|
18
|
+
const text = node.getTextContent();
|
|
19
|
+
const span = wordsIn(text).find((s) => offset >= s.start && offset <= s.end);
|
|
20
|
+
if (!span)
|
|
21
|
+
return null;
|
|
22
|
+
return {
|
|
23
|
+
word: span.word,
|
|
24
|
+
direction: node.getParentOrThrow().getDirection() === 'rtl' ? 'rtl' : 'ltr',
|
|
25
|
+
match: { anchorKey: node.getKey(), anchorOffset: span.start, focusKey: node.getKey(), focusOffset: span.end },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { SpellLanguage } from './spellDictionaries.js';
|
|
2
|
+
/**
|
|
3
|
+
* Where words the user has added to the dictionary are kept. Like the
|
|
4
|
+
* auto-correct stores: the host can pass several, and words are added to the
|
|
5
|
+
* first one that accepts writes.
|
|
6
|
+
*/
|
|
7
|
+
export interface UserWordStore {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly readOnly?: boolean;
|
|
10
|
+
load(language: SpellLanguage): Promise<string[]>;
|
|
11
|
+
append?(language: SpellLanguage, word: string): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
/** Words added to the dictionary, kept in this browser's localStorage. */
|
|
14
|
+
export declare function localStorageUserWordStore(options?: {
|
|
15
|
+
key?: string;
|
|
16
|
+
}): UserWordStore;
|
|
17
|
+
/**
|
|
18
|
+
* Words served by an API. `load` calls `GET {url}?language=xx` and expects a JSON
|
|
19
|
+
* array of strings; `append` calls `POST {url}` with `{ language, word }`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function apiUserWordStore(options: {
|
|
22
|
+
id?: string;
|
|
23
|
+
url: string;
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
fetch?: typeof fetch;
|
|
26
|
+
}): UserWordStore;
|
|
27
|
+
/** Calls `listener` whenever accepted or ignored words change. Returns an unsubscribe function. */
|
|
28
|
+
export declare function onSpellWordsChange(listener: () => void): () => void;
|
|
29
|
+
/** Whether the speller should treat the word as correct: it is in the user's dictionary or ignored this session. */
|
|
30
|
+
export declare function isAcceptedWord(language: SpellLanguage, word: string): boolean;
|
|
31
|
+
/** Loads the user's words for each language from the stores (in order). */
|
|
32
|
+
export declare function loadUserWords(stores: UserWordStore[]): Promise<void>;
|
|
33
|
+
/** Adds a word to the first store that accepts writes. Returns false if none does. */
|
|
34
|
+
export declare function addUserWord(stores: UserWordStore[], language: SpellLanguage, word: string): Promise<boolean>;
|
|
35
|
+
/** Loads the session's ignored words. Call once at startup. */
|
|
36
|
+
export declare function restoreIgnoredWords(): void;
|
|
37
|
+
/** Ignores a word for the rest of this browser session (kept in sessionStorage). */
|
|
38
|
+
export declare function ignoreWord(language: SpellLanguage, word: string): void;
|
|
39
|
+
//# sourceMappingURL=userWords.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"userWords.d.ts","sourceRoot":"","sources":["../../src/spellcheck/userWords.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,IAAI,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACjD,MAAM,CAAC,CAAC,QAAQ,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D;AAoBD,0EAA0E;AAC1E,wBAAgB,yBAAyB,CAAC,OAAO,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,aAAa,CAoBvF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE;IACxC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB,GAAG,aAAa,CAmBhB;AAWD,mGAAmG;AACnG,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAGnE;AAED,oHAAoH;AACpH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAE7E;AAED,2EAA2E;AAC3E,wBAAsB,aAAa,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ1E;AAED,sFAAsF;AACtF,wBAAsB,WAAW,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,QAAQ,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOlH;AAWD,+DAA+D;AAC/D,wBAAgB,mBAAmB,IAAI,IAAI,CAG1C;AAED,oFAAoF;AACpF,wBAAgB,UAAU,CAAC,QAAQ,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAUtE"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const LOCAL_KEY = 'likhari-user-words';
|
|
2
|
+
const SESSION_KEY = 'likhari-spell-ignore';
|
|
3
|
+
const LANGUAGES = ['en', 'ur', 'pa-shahmukhi'];
|
|
4
|
+
function readStored(key) {
|
|
5
|
+
try {
|
|
6
|
+
const parsed = JSON.parse(localStorage.getItem(key) ?? '[]');
|
|
7
|
+
return Array.isArray(parsed) ? parsed.filter((e) => typeof e?.word === 'string') : [];
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Words added to the dictionary, kept in this browser's localStorage. */
|
|
14
|
+
export function localStorageUserWordStore(options = {}) {
|
|
15
|
+
const key = options.key ?? LOCAL_KEY;
|
|
16
|
+
return {
|
|
17
|
+
id: 'local-storage',
|
|
18
|
+
async load(language) {
|
|
19
|
+
return readStored(key)
|
|
20
|
+
.filter((e) => e.language === language)
|
|
21
|
+
.map((e) => e.word);
|
|
22
|
+
},
|
|
23
|
+
async append(language, word) {
|
|
24
|
+
const all = readStored(key);
|
|
25
|
+
if (all.some((e) => e.language === language && e.word === word))
|
|
26
|
+
return;
|
|
27
|
+
all.push({ language, word });
|
|
28
|
+
try {
|
|
29
|
+
localStorage.setItem(key, JSON.stringify(all));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Storage full or disabled: the word stays for this session only.
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Words served by an API. `load` calls `GET {url}?language=xx` and expects a JSON
|
|
39
|
+
* array of strings; `append` calls `POST {url}` with `{ language, word }`.
|
|
40
|
+
*/
|
|
41
|
+
export function apiUserWordStore(options) {
|
|
42
|
+
const doFetch = options.fetch ?? fetch;
|
|
43
|
+
return {
|
|
44
|
+
id: options.id ?? 'api',
|
|
45
|
+
async load(language) {
|
|
46
|
+
const response = await doFetch(`${options.url}?language=${encodeURIComponent(language)}`, { headers: options.headers });
|
|
47
|
+
if (!response.ok)
|
|
48
|
+
throw new Error(`User word API failed: ${response.status}`);
|
|
49
|
+
const data = await response.json();
|
|
50
|
+
return Array.isArray(data) ? data.filter((w) => typeof w === 'string') : [];
|
|
51
|
+
},
|
|
52
|
+
async append(language, word) {
|
|
53
|
+
const response = await doFetch(options.url, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'Content-Type': 'application/json', ...options.headers },
|
|
56
|
+
body: JSON.stringify({ language, word }),
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok)
|
|
59
|
+
throw new Error(`User word API rejected the word: ${response.status}`);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// Words the user accepted (kept in their store), and words ignored for this session.
|
|
64
|
+
const accepted = new Map(LANGUAGES.map((l) => [l, new Set()]));
|
|
65
|
+
const ignored = new Map(LANGUAGES.map((l) => [l, new Set()]));
|
|
66
|
+
const listeners = new Set();
|
|
67
|
+
function notify() {
|
|
68
|
+
for (const listener of listeners)
|
|
69
|
+
listener();
|
|
70
|
+
}
|
|
71
|
+
/** Calls `listener` whenever accepted or ignored words change. Returns an unsubscribe function. */
|
|
72
|
+
export function onSpellWordsChange(listener) {
|
|
73
|
+
listeners.add(listener);
|
|
74
|
+
return () => listeners.delete(listener);
|
|
75
|
+
}
|
|
76
|
+
/** Whether the speller should treat the word as correct: it is in the user's dictionary or ignored this session. */
|
|
77
|
+
export function isAcceptedWord(language, word) {
|
|
78
|
+
return accepted.get(language)?.has(word) === true || ignored.get(language)?.has(word) === true;
|
|
79
|
+
}
|
|
80
|
+
/** Loads the user's words for each language from the stores (in order). */
|
|
81
|
+
export async function loadUserWords(stores) {
|
|
82
|
+
for (const language of LANGUAGES) {
|
|
83
|
+
const set = accepted.get(language);
|
|
84
|
+
set.clear();
|
|
85
|
+
const results = await Promise.allSettled(stores.map((store) => store.load(language)));
|
|
86
|
+
for (const result of results)
|
|
87
|
+
if (result.status === 'fulfilled')
|
|
88
|
+
for (const word of result.value)
|
|
89
|
+
set.add(word);
|
|
90
|
+
}
|
|
91
|
+
notify();
|
|
92
|
+
}
|
|
93
|
+
/** Adds a word to the first store that accepts writes. Returns false if none does. */
|
|
94
|
+
export async function addUserWord(stores, language, word) {
|
|
95
|
+
const writable = stores.find((store) => !store.readOnly && store.append);
|
|
96
|
+
if (!writable?.append)
|
|
97
|
+
return false;
|
|
98
|
+
await writable.append(language, word);
|
|
99
|
+
accepted.get(language)?.add(word);
|
|
100
|
+
notify();
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
function readIgnored() {
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(sessionStorage.getItem(SESSION_KEY) ?? '[]');
|
|
106
|
+
return Array.isArray(parsed) ? parsed.filter((e) => typeof e?.word === 'string') : [];
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Loads the session's ignored words. Call once at startup. */
|
|
113
|
+
export function restoreIgnoredWords() {
|
|
114
|
+
for (const { language, word } of readIgnored())
|
|
115
|
+
ignored.get(language)?.add(word);
|
|
116
|
+
notify();
|
|
117
|
+
}
|
|
118
|
+
/** Ignores a word for the rest of this browser session (kept in sessionStorage). */
|
|
119
|
+
export function ignoreWord(language, word) {
|
|
120
|
+
ignored.get(language)?.add(word);
|
|
121
|
+
const all = readIgnored();
|
|
122
|
+
if (!all.some((e) => e.language === language && e.word === word))
|
|
123
|
+
all.push({ language, word });
|
|
124
|
+
try {
|
|
125
|
+
sessionStorage.setItem(SESSION_KEY, JSON.stringify(all));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Session storage unavailable: the word is ignored until the page reloads.
|
|
129
|
+
}
|
|
130
|
+
notify();
|
|
131
|
+
}
|
|
132
|
+
restoreIgnoredWords();
|