@inshapardaz/likhari-react 0.1.22 → 0.1.23-dev.221
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 +24 -11
- package/dist/autocorrect/AutoCorrectPlugin.d.ts +27 -0
- package/dist/autocorrect/AutoCorrectPlugin.d.ts.map +1 -0
- package/dist/autocorrect/AutoCorrectPlugin.js +124 -0
- package/dist/autocorrect/autoCorrectActions.d.ts +41 -0
- package/dist/autocorrect/autoCorrectActions.d.ts.map +1 -0
- package/dist/autocorrect/autoCorrectActions.js +212 -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 +19 -0
- package/dist/components/AutoCorrectPanel.d.ts.map +1 -0
- package/dist/components/AutoCorrectPanel.js +44 -0
- package/dist/components/SpellcheckPanel.d.ts +5 -1
- package/dist/components/SpellcheckPanel.d.ts.map +1 -1
- package/dist/components/SpellcheckPanel.js +12 -2
- package/dist/components/Toolbar.d.ts +10 -1
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +128 -109
- package/dist/i18n/strings.d.ts +25 -0
- package/dist/i18n/strings.d.ts.map +1 -1
- package/dist/i18n/strings.js +80 -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.map +1 -1
- package/dist/spellcheck/SpellHighlightPlugin.js +3 -0
- package/dist/spellcheck/spellDictionaries.d.ts.map +1 -1
- package/dist/spellcheck/spellDictionaries.js +8 -1
- 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 +58 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -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":"AAUA;;;;;GAKG;AACH,wBAAgB,oBAAoB,SAkDnC"}
|
|
@@ -2,6 +2,7 @@ import { useEffect } from 'react';
|
|
|
2
2
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
3
3
|
import { $collectMisspellings } from './spellcheckActions.js';
|
|
4
4
|
import { getSpeller } from './spellDictionaries.js';
|
|
5
|
+
import { onSpellWordsChange } from './userWords.js';
|
|
5
6
|
/** The CSS Custom Highlight name; editor.css styles it as a red wavy underline. */
|
|
6
7
|
const HIGHLIGHT = 'likhari-spelling';
|
|
7
8
|
const DEBOUNCE_MS = 300;
|
|
@@ -42,6 +43,7 @@ export function SpellHighlightPlugin() {
|
|
|
42
43
|
timer = setTimeout(paint, DEBOUNCE_MS);
|
|
43
44
|
};
|
|
44
45
|
const unregister = editor.registerUpdateListener(schedule);
|
|
46
|
+
const unsubscribe = onSpellWordsChange(schedule);
|
|
45
47
|
Promise.all([getSpeller('en'), getSpeller('ur')]).then(async ([en, ur]) => {
|
|
46
48
|
checkers = { ltr: en ? await en : undefined, rtl: ur ? await ur : undefined };
|
|
47
49
|
schedule();
|
|
@@ -50,6 +52,7 @@ export function SpellHighlightPlugin() {
|
|
|
50
52
|
active = false;
|
|
51
53
|
clearTimeout(timer);
|
|
52
54
|
unregister();
|
|
55
|
+
unsubscribe();
|
|
53
56
|
CSS.highlights.delete(HIGHLIGHT);
|
|
54
57
|
};
|
|
55
58
|
}, [editor]);
|
|
@@ -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,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();
|
package/dist/theme/editor.css
CHANGED
|
@@ -997,3 +997,61 @@
|
|
|
997
997
|
gap: 4px;
|
|
998
998
|
}
|
|
999
999
|
|
|
1000
|
+
/* Auto-correct panel: the same floating spot as the spellcheck panel. */
|
|
1001
|
+
.likhari-autocorrect-panel {
|
|
1002
|
+
position: absolute;
|
|
1003
|
+
z-index: 5;
|
|
1004
|
+
top: 8px;
|
|
1005
|
+
right: 16px;
|
|
1006
|
+
width: 260px;
|
|
1007
|
+
max-width: calc(100% - 32px);
|
|
1008
|
+
padding: 8px;
|
|
1009
|
+
border: 1px solid var(--editor-line);
|
|
1010
|
+
border-radius: 6px;
|
|
1011
|
+
background: var(--editor-paper-dim);
|
|
1012
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
|
1013
|
+
font-size: 13px;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
.likhari-autocorrect-panel[dir='rtl'] {
|
|
1017
|
+
right: auto;
|
|
1018
|
+
left: 16px;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
.likhari-autocorrect-field {
|
|
1022
|
+
display: flex;
|
|
1023
|
+
flex-direction: column;
|
|
1024
|
+
gap: 2px;
|
|
1025
|
+
margin-bottom: 6px;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
.likhari-autocorrect-field input,
|
|
1029
|
+
.likhari-autocorrect-field select {
|
|
1030
|
+
padding: 3px 6px;
|
|
1031
|
+
border: 1px solid var(--editor-line);
|
|
1032
|
+
border-radius: 4px;
|
|
1033
|
+
background: var(--editor-paper);
|
|
1034
|
+
color: inherit;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.likhari-autocorrect-actions {
|
|
1038
|
+
display: flex;
|
|
1039
|
+
flex-direction: column;
|
|
1040
|
+
gap: 6px;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
.likhari-autocorrect-actions button {
|
|
1044
|
+
align-self: flex-start;
|
|
1045
|
+
padding: 2px 8px;
|
|
1046
|
+
border: 1px solid var(--editor-line);
|
|
1047
|
+
border-radius: 4px;
|
|
1048
|
+
background: transparent;
|
|
1049
|
+
color: inherit;
|
|
1050
|
+
cursor: pointer;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.likhari-autocorrect-actions button:disabled {
|
|
1054
|
+
opacity: 0.5;
|
|
1055
|
+
cursor: default;
|
|
1056
|
+
}
|
|
1057
|
+
|