@inshapardaz/likhari-react 0.1.21 → 0.1.22-dev.216
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 +9 -0
- package/dist/EditorRoot.d.ts.map +1 -1
- package/dist/EditorRoot.js +14 -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 +13 -0
- package/dist/components/SpellcheckPanel.d.ts.map +1 -0
- package/dist/components/SpellcheckPanel.js +81 -0
- package/dist/components/Toolbar.d.ts +7 -1
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +112 -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 +32 -0
- package/dist/i18n/strings.d.ts.map +1 -1
- package/dist/i18n/strings.js +101 -5
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -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 +57 -0
- 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/theme/editor.css +185 -0
- package/package.json +1 -1
package/dist/EditorRoot.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ import { type FormatId } from '@inshapardaz/likhari-converters';
|
|
|
4
4
|
import { type EditorFeatureConfig, type FeatureConfigPresetName } from '@inshapardaz/likhari-core';
|
|
5
5
|
import { type FontOption } from './fonts/index.js';
|
|
6
6
|
import { type DraftRestoreMode } from './components/DraftRestore.js';
|
|
7
|
+
import { type AutoCorrectStore } from './autocorrect/autoCorrectStores.js';
|
|
8
|
+
import type { UrduNormalizationOptions } from './normalization/urduNormalize.js';
|
|
9
|
+
import type { PunctuationOptions } from './autocorrect/punctuationRules.js';
|
|
7
10
|
import { type Locale } from './i18n/index.js';
|
|
8
11
|
/** What happens when the user is about to leave with unsaved changes. */
|
|
9
12
|
export type NavigationGuardMode = 'confirm' | 'save-draft' | 'off';
|
|
@@ -13,6 +16,12 @@ export interface EditorInitialContent {
|
|
|
13
16
|
}
|
|
14
17
|
export interface EditorRootProps {
|
|
15
18
|
documentId?: string;
|
|
19
|
+
/** Where auto-corrections are loaded from and saved to, in priority order. Pass a stable array. */
|
|
20
|
+
autoCorrectStores?: AutoCorrectStore[];
|
|
21
|
+
/** Urdu normalisation applied as part of auto-correct. Diacritics are kept unless `removeDiacritics` is set. */
|
|
22
|
+
urduNormalization?: UrduNormalizationOptions;
|
|
23
|
+
/** Common punctuation fixes, and whether a straight " becomes ”. Both on by default. */
|
|
24
|
+
punctuation?: PunctuationOptions;
|
|
16
25
|
initialContent?: EditorInitialContent;
|
|
17
26
|
featureConfig?: EditorFeatureConfig;
|
|
18
27
|
featurePreset?: FeatureConfigPresetName;
|
package/dist/EditorRoot.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditorRoot.d.ts","sourceRoot":"","sources":["../src/EditorRoot.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAY1D,OAAO,KAAK,EAA8B,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAEjF,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AACvF,OAAO,EAAwB,KAAK,mBAAmB,EAAE,KAAK,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAKzH,OAAO,EAAyB,KAAK,UAAU,EAAE,MAAM,SAAS,CAAC;AAOjE,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"EditorRoot.d.ts","sourceRoot":"","sources":["../src/EditorRoot.tsx"],"names":[],"mappings":"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"}
|
package/dist/EditorRoot.js
CHANGED
|
@@ -27,6 +27,11 @@ import { PoetryPlugin } from './plugins/PoetryPlugin.js';
|
|
|
27
27
|
import { PoetryResizer } from './blocks/PoetryResizer.js';
|
|
28
28
|
import { DraftRestore } from './components/DraftRestore.js';
|
|
29
29
|
import { FindReplaceBar } from './components/FindReplaceBar.js';
|
|
30
|
+
import { AutoCorrectPanel } from './components/AutoCorrectPanel.js';
|
|
31
|
+
import { AutoCorrectPlugin } from './autocorrect/AutoCorrectPlugin.js';
|
|
32
|
+
import { localStorageAutoCorrectStore } from './autocorrect/autoCorrectStores.js';
|
|
33
|
+
import { SpellcheckPanel } from './components/SpellcheckPanel.js';
|
|
34
|
+
import { SpellHighlightPlugin } from './spellcheck/SpellHighlightPlugin.js';
|
|
30
35
|
import { LeaveDialog } from './components/LeaveDialog.js';
|
|
31
36
|
import { clearDraft, createDraftId, hasDraftContent, pruneDrafts, writeDraft, } from './persistence/draftStorage.js';
|
|
32
37
|
import { ImageOptionsContext } from './image/ImageOptionsContext.js';
|
|
@@ -35,13 +40,15 @@ import { UiStringsContext, getStrings } from './i18n/index.js';
|
|
|
35
40
|
const DEFAULT_AUTOSAVE_DELAY_MS = 750;
|
|
36
41
|
const DEFAULT_AUTOSAVE_MAX_BYTES = 2000000;
|
|
37
42
|
const DEFAULT_AUTOSAVE_MAX_DRAFTS = 20;
|
|
43
|
+
/** Used when the host passes no stores: corrections are kept in this browser. */
|
|
44
|
+
const DEFAULT_AUTOCORRECT_STORES = [localStorageAutoCorrectStore()];
|
|
38
45
|
function initialEditorStateJson(initialContent) {
|
|
39
46
|
if (!initialContent)
|
|
40
47
|
return undefined;
|
|
41
48
|
const state = defaultFormatRegistry.parse(initialContent.format, initialContent.value);
|
|
42
49
|
return JSON.stringify(state);
|
|
43
50
|
}
|
|
44
|
-
export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialContent, featureConfig, featurePreset, theme, colorScheme, accentColor, locale = 'en', placeholder, height = '480px', onChange, onSave, showSave = Boolean(onSave), onImageUpload, fetchImage, fontOptions, autosave = true, restoreDraft = 'prompt', navigationGuard = 'confirm', onDraftRestored, autosaveDelayMs = DEFAULT_AUTOSAVE_DELAY_MS, autosaveMaxBytes = DEFAULT_AUTOSAVE_MAX_BYTES, autosaveMaxDrafts = DEFAULT_AUTOSAVE_MAX_DRAFTS, }, ref) {
|
|
51
|
+
export const EditorRoot = forwardRef(function EditorRoot({ documentId, autoCorrectStores, urduNormalization, punctuation, initialContent, featureConfig, featurePreset, theme, colorScheme, accentColor, locale = 'en', placeholder, height = '480px', onChange, onSave, showSave = Boolean(onSave), onImageUpload, fetchImage, fontOptions, autosave = true, restoreDraft = 'prompt', navigationGuard = 'confirm', onDraftRestored, autosaveDelayMs = DEFAULT_AUTOSAVE_DELAY_MS, autosaveMaxBytes = DEFAULT_AUTOSAVE_MAX_BYTES, autosaveMaxDrafts = DEFAULT_AUTOSAVE_MAX_DRAFTS, }, ref) {
|
|
45
52
|
const config = useMemo(() => resolveFeatureConfig(featureConfig, featurePreset), [featureConfig, featurePreset]);
|
|
46
53
|
const strings = useMemo(() => getStrings(locale), [locale]);
|
|
47
54
|
const resolvedPlaceholder = placeholder ?? strings.editor.placeholder;
|
|
@@ -68,7 +75,10 @@ export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialCo
|
|
|
68
75
|
const editorStateRef = useRef(null);
|
|
69
76
|
const lastSavedJsonRef = useRef(null);
|
|
70
77
|
const [isDirty, setIsDirty] = useState(false);
|
|
71
|
-
const [
|
|
78
|
+
const [openPanel, setOpenPanel] = useState(null);
|
|
79
|
+
const [autoCorrectVersion, setAutoCorrectVersion] = useState(0);
|
|
80
|
+
const stores = autoCorrectStores ?? DEFAULT_AUTOCORRECT_STORES;
|
|
81
|
+
const togglePanel = (panel) => setOpenPanel((current) => (current === panel ? null : panel));
|
|
72
82
|
const isDirtyRef = useRef(isDirty);
|
|
73
83
|
isDirtyRef.current = isDirty;
|
|
74
84
|
const autosaveTimerRef = useRef(null);
|
|
@@ -281,9 +291,9 @@ export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialCo
|
|
|
281
291
|
// rather than colliding on a shared one.
|
|
282
292
|
const portalTargetId = useId();
|
|
283
293
|
const portalTargetSelector = `#${CSS.escape(portalTargetId)}`;
|
|
284
|
-
return (_jsx(EditorThemeProvider, { theme: theme, colorScheme: colorScheme, accentColor: accentColor, scopeElementId: portalTargetId, children: _jsxs("div", { className: "likhari-root", dir: dir, ref: rootElementRef, "data-document-id": documentId, style: { height: typeof height === 'number' ? `${height}px` : height }, children: [_jsx("div", { id: portalTargetId, className: "likhari-portal-target" }), _jsx(PortalTargetContext.Provider, { value: portalTargetSelector, children: _jsxs(UiStringsContext.Provider, { value: strings, children: [_jsx(ImageOptionsContext.Provider, { value: imageOptions, children: _jsxs(LexicalComposer, { initialConfig: initialConfig, children: [_jsx(Toolbar, { config: config, onSave: handleSave, findOpen:
|
|
294
|
+
return (_jsx(EditorThemeProvider, { theme: theme, colorScheme: colorScheme, accentColor: accentColor, scopeElementId: portalTargetId, children: _jsxs("div", { className: "likhari-root", dir: dir, ref: rootElementRef, "data-document-id": documentId, style: { height: typeof height === 'number' ? `${height}px` : height }, children: [_jsx("div", { id: portalTargetId, className: "likhari-portal-target" }), _jsx(PortalTargetContext.Provider, { value: portalTargetSelector, children: _jsxs(UiStringsContext.Provider, { value: strings, children: [_jsx(ImageOptionsContext.Provider, { value: imageOptions, children: _jsxs(LexicalComposer, { initialConfig: initialConfig, children: [_jsx(Toolbar, { config: config, onSave: handleSave, findOpen: openPanel === 'find', onToggleFind: () => togglePanel('find'), spellOpen: openPanel === 'spell', onToggleSpell: () => togglePanel('spell'), autoCorrectOpen: openPanel === 'autocorrect', onToggleAutoCorrect: () => togglePanel('autocorrect'), isDirty: isDirty, showSave: showSave, fontOptions: fontOptions, direction: dir, locale: locale, drafts: autosave
|
|
285
295
|
? { currentId: draftId, maxBytes: autosaveMaxBytes, adoptOnRestore: !documentId, onRestored: handleDraftRestored }
|
|
286
|
-
: undefined }), documentId && (_jsx(DraftRestore, { draftId: documentId, mode: restoreDraft, initialJson: initialContentJsonRef.current, locale: locale, onRestored: handleDraftRestored })), _jsxs("div", { className: "likhari-canvas-frame", children: [config.findReplace &&
|
|
296
|
+
: undefined }), documentId && (_jsx(DraftRestore, { draftId: documentId, mode: restoreDraft, initialJson: initialContentJsonRef.current, locale: locale, onRestored: handleDraftRestored })), _jsxs("div", { className: "likhari-canvas-frame", children: [config.findReplace && openPanel === 'find' && _jsx(FindReplaceBar, { strings: strings, dir: dir, onClose: () => setOpenPanel(null) }), config.language.spellCheck && openPanel === 'spell' && (_jsx(SpellcheckPanel, { strings: strings, dir: dir, onClose: () => setOpenPanel(null) })), config.language.autocorrect && openPanel === 'autocorrect' && (_jsx(AutoCorrectPanel, { strings: strings, dir: dir, stores: stores, onSaved: () => setAutoCorrectVersion((v) => v + 1), onClose: () => setOpenPanel(null) })), _jsx("div", { className: "likhari-canvas", children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "likhari-content-editable", dir: dir, spellCheck: false, "aria-label": strings.editor.contentLabel }), placeholder: _jsx("div", { className: "likhari-placeholder", children: resolvedPlaceholder }), ErrorBoundary: LexicalErrorBoundary }) })] }), config.history && _jsx(HistoryPlugin, {}), (config.lists.bullet || config.lists.numbered || config.lists.check) && _jsx(ListPlugin, {}), config.lists.check && _jsx(CheckListPlugin, {}), config.links && _jsx(LinkPlugin, {}), config.links && _jsx(LinkPastePlugin, {}), config.blocks.pageBreak && _jsx(PageBreakPlugin, {}), config.language.spellCheck && _jsx(SpellHighlightPlugin, {}), (config.language.autocorrect || config.language.textCleanup) && _jsx(AutoCorrectPlugin, { stores: stores, version: autoCorrectVersion, enabled: true, typing: config.language.autocorrect, urduNormalization: urduNormalization, punctuation: punctuation }), config.columns && _jsx(LayoutPlugin, {}), config.footnotes && _jsx(FootnotePlugin, {}), config.poetry.enabled && _jsx(PoetryPlugin, {}), config.poetry.enabled && _jsx(PoetryResizer, {}), config.tables && _jsx(TablePlugin, { hasCellMerge: true, hasTabHandler: true }), config.blocks.horizontalRule && _jsx(HorizontalRulePlugin, {}), _jsx(EditorInstancePlugin, { instanceRef: editorInstanceRef }), _jsx(OnChangePlugin, { onChange: handleChange })] }) }), _jsx(LeaveDialog, { opened: leavePrompt !== null, canSave: Boolean(onSave), canSaveDraft: autosave || navigationGuard === 'save-draft', draftFailed: leavePrompt?.draftFailed ?? false, onSave: () => {
|
|
287
297
|
handleSave();
|
|
288
298
|
settleLeave(true);
|
|
289
299
|
}, onSaveDraft: () => {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type LexicalCommand } from 'lexical';
|
|
2
|
+
import { type PunctuationOptions } from './punctuationRules.js';
|
|
3
|
+
import { type AutoCorrectStore } from './autoCorrectStores.js';
|
|
4
|
+
import { type UrduNormalizationOptions } from '../normalization/urduNormalize.js';
|
|
5
|
+
/** Keys that end a word: a space or punctuation, in Latin and Arabic-script forms. */
|
|
6
|
+
/** Corrects every word in the document, e.g. after pasting or loading content. */
|
|
7
|
+
export declare const CORRECT_DOCUMENT_COMMAND: LexicalCommand<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Corrects typed words as the user finishes them, the way the reference editor
|
|
10
|
+
* does, but from any set of stores. Corrections are loaded once per change
|
|
11
|
+
* of `stores` or `version` (bump `version` after saving a new correction so
|
|
12
|
+
* the table reloads). The table used depends on the block's direction: RTL
|
|
13
|
+
* blocks use the Urdu and Shahmukhi tables, LTR blocks the English one.
|
|
14
|
+
*/
|
|
15
|
+
export declare function AutoCorrectPlugin({ stores, version, enabled, typing, urduNormalization, punctuation, }: {
|
|
16
|
+
stores: AutoCorrectStore[];
|
|
17
|
+
version: number;
|
|
18
|
+
/** Whether the tables and the whole-document command are active. */
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
/** Whether corrections run as the user types (auto-correct), not only on demand. */
|
|
21
|
+
typing: boolean;
|
|
22
|
+
/** Urdu normalisation for right-to-left text; character mapping always applies. */
|
|
23
|
+
urduNormalization?: UrduNormalizationOptions;
|
|
24
|
+
/** Common punctuation fixes (from the bundled Urdu rule list). */
|
|
25
|
+
punctuation?: PunctuationOptions;
|
|
26
|
+
}): null;
|
|
27
|
+
//# sourceMappingURL=AutoCorrectPlugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AutoCorrectPlugin.d.ts","sourceRoot":"","sources":["../../src/autocorrect/AutoCorrectPlugin.tsx"],"names":[],"mappings":"AAEA,OAAO,EAA4E,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAUxH,OAAO,EAA0B,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACrF,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,EAAiB,KAAK,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAG9F,sFAAsF;AACtF,kFAAkF;AAClF,eAAO,MAAM,wBAAwB,EAAE,cAAc,CAAC,IAAI,CAA6C,CAAC;AAMxG;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EACN,OAAO,EACP,OAAO,EACP,MAAM,EACN,iBAAiB,EACjB,WAAW,GACZ,EAAE;IACD,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,OAAO,EAAE,OAAO,CAAC;IACjB,oFAAoF;IACpF,MAAM,EAAE,OAAO,CAAC;IAChB,mFAAmF;IACnF,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;IAC7C,kEAAkE;IAClE,WAAW,CAAC,EAAE,kBAAkB,CAAC;CAClC,QA4FA"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
3
|
+
import { $getSelection, $isRangeSelection, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical';
|
|
4
|
+
import { $correctDocument, $correctPunctuationBeforeCaret, $correctPunctuationDocument, $correctWordBeforeCaret, $preservingSelection, $spacePunctuationDocument, } from './autoCorrectActions.js';
|
|
5
|
+
import { $cleanUpText } from '../cleanup/textCleanupActions.js';
|
|
6
|
+
import { activePunctuationRules } from './punctuationRules.js';
|
|
7
|
+
import { loadAutoCorrections } from './autoCorrectStores.js';
|
|
8
|
+
import { normalizeUrdu } from '../normalization/urduNormalize.js';
|
|
9
|
+
/** Keys that end a word: a space or punctuation, in Latin and Arabic-script forms. */
|
|
10
|
+
/** Corrects every word in the document, e.g. after pasting or loading content. */
|
|
11
|
+
export const CORRECT_DOCUMENT_COMMAND = createCommand('CORRECT_DOCUMENT_COMMAND');
|
|
12
|
+
const BOUNDARY_KEYS = new Set([' ', '.', ',', ';', ':', '!', '?', '،', '؛', '؟', '۔']);
|
|
13
|
+
const LANGUAGES = ['en', 'ur', 'pa-shahmukhi'];
|
|
14
|
+
const RTL_LANGUAGES = ['ur', 'pa-shahmukhi'];
|
|
15
|
+
/**
|
|
16
|
+
* Corrects typed words as the user finishes them, the way the reference editor
|
|
17
|
+
* does, but from any set of stores. Corrections are loaded once per change
|
|
18
|
+
* of `stores` or `version` (bump `version` after saving a new correction so
|
|
19
|
+
* the table reloads). The table used depends on the block's direction: RTL
|
|
20
|
+
* blocks use the Urdu and Shahmukhi tables, LTR blocks the English one.
|
|
21
|
+
*/
|
|
22
|
+
export function AutoCorrectPlugin({ stores, version, enabled, typing, urduNormalization, punctuation, }) {
|
|
23
|
+
const [editor] = useLexicalComposerContext();
|
|
24
|
+
// Effects below run once per editor, so they reach the latest options through this ref.
|
|
25
|
+
const normalizeOptions = useRef(urduNormalization);
|
|
26
|
+
normalizeOptions.current = urduNormalization;
|
|
27
|
+
const punctuationRules = useRef(activePunctuationRules(punctuation));
|
|
28
|
+
punctuationRules.current = activePunctuationRules(punctuation);
|
|
29
|
+
const normalizeRtl = (text) => normalizeUrdu(text, normalizeOptions.current);
|
|
30
|
+
const tables = useRef(new Map());
|
|
31
|
+
const rtlTable = useRef(new Map());
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!enabled)
|
|
34
|
+
return;
|
|
35
|
+
let active = true;
|
|
36
|
+
Promise.all(LANGUAGES.map(async (language) => [language, await loadAutoCorrections(stores, language)])).then((loaded) => {
|
|
37
|
+
if (!active)
|
|
38
|
+
return;
|
|
39
|
+
tables.current = new Map(loaded);
|
|
40
|
+
// Urdu and Shahmukhi share right-to-left blocks, so both apply there; Urdu wins a clash.
|
|
41
|
+
const combined = new Map();
|
|
42
|
+
for (const language of RTL_LANGUAGES) {
|
|
43
|
+
for (const [from, to] of tables.current.get(language) ?? [])
|
|
44
|
+
if (!combined.has(from))
|
|
45
|
+
combined.set(from, to);
|
|
46
|
+
}
|
|
47
|
+
rtlTable.current = combined;
|
|
48
|
+
});
|
|
49
|
+
return () => {
|
|
50
|
+
active = false;
|
|
51
|
+
};
|
|
52
|
+
}, [stores, version, enabled]);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!enabled)
|
|
55
|
+
return;
|
|
56
|
+
return editor.registerCommand(CORRECT_DOCUMENT_COMMAND, () => {
|
|
57
|
+
editor.update(() => {
|
|
58
|
+
$preservingSelection(() => {
|
|
59
|
+
$cleanUpText();
|
|
60
|
+
if (punctuation?.spacing)
|
|
61
|
+
$spacePunctuationDocument();
|
|
62
|
+
$correctPunctuationDocument(punctuationRules.current);
|
|
63
|
+
$correctDocument(tables.current.get('en') ?? new Map(), rtlTable.current, normalizeRtl);
|
|
64
|
+
});
|
|
65
|
+
}, { discrete: true });
|
|
66
|
+
return true;
|
|
67
|
+
}, COMMAND_PRIORITY_EDITOR);
|
|
68
|
+
}, [editor, enabled, punctuation?.spacing]);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!enabled || !typing)
|
|
71
|
+
return;
|
|
72
|
+
const tableFor = (rtl) => (rtl ? rtlTable.current : tables.current.get('en'));
|
|
73
|
+
const onKeyDown = (event) => {
|
|
74
|
+
if (event.isComposing || event.key.length !== 1)
|
|
75
|
+
return;
|
|
76
|
+
const boundary = BOUNDARY_KEYS.has(event.key);
|
|
77
|
+
// The character is inserted after this event; correct once it is there.
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
editor.update(() => {
|
|
80
|
+
const selection = $getSelection();
|
|
81
|
+
if (!$isRangeSelection(selection))
|
|
82
|
+
return;
|
|
83
|
+
// Punctuation can end in any character (e.g. the letter ه), so every key is checked.
|
|
84
|
+
$correctPunctuationBeforeCaret(punctuationRules.current);
|
|
85
|
+
if (!boundary)
|
|
86
|
+
return;
|
|
87
|
+
const block = selection.anchor.getNode().getTopLevelElement();
|
|
88
|
+
const rtl = block?.getDirection() === 'rtl';
|
|
89
|
+
const table = tableFor(rtl);
|
|
90
|
+
if (table)
|
|
91
|
+
$correctWordBeforeCaret(table, rtl ? normalizeRtl : undefined);
|
|
92
|
+
}, { discrete: true });
|
|
93
|
+
}, 0);
|
|
94
|
+
};
|
|
95
|
+
let root = null;
|
|
96
|
+
const attach = (next) => {
|
|
97
|
+
root?.removeEventListener('keydown', onKeyDown);
|
|
98
|
+
root = next;
|
|
99
|
+
root?.addEventListener('keydown', onKeyDown);
|
|
100
|
+
};
|
|
101
|
+
const unregister = editor.registerRootListener((next) => attach(next));
|
|
102
|
+
return () => {
|
|
103
|
+
unregister();
|
|
104
|
+
attach(null);
|
|
105
|
+
};
|
|
106
|
+
}, [editor, enabled, typing]);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type PunctuationRule } from './punctuationRules.js';
|
|
2
|
+
/**
|
|
3
|
+
* Corrects the word that ends just before the caret, when the character just
|
|
4
|
+
* typed (the word boundary: space or punctuation) is not a word character. The
|
|
5
|
+
* correction replaces only the word, and the caret stays right after the
|
|
6
|
+
* boundary. Returns true if a correction was made.
|
|
7
|
+
*/
|
|
8
|
+
export declare function $correctWordBeforeCaret(table: Map<string, string>, normalize?: (word: string) => string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Corrects every whole word in the document, for text that never went through
|
|
11
|
+
* typing (pasted or loaded content). Words are found per paragraph or cell, so a
|
|
12
|
+
* word split by formatting is still one word. Each block uses the table for its
|
|
13
|
+
* direction. Returns how many words were corrected.
|
|
14
|
+
*/
|
|
15
|
+
export declare function $correctDocument(ltr: Map<string, string>, rtl: Map<string, string>, normalizeRtl?: (text: string) => string): number;
|
|
16
|
+
/**
|
|
17
|
+
* Applies a punctuation rule to the text just before the caret, when that text
|
|
18
|
+
* ends with a rule's incorrect form. Longest match first. The caret ends after
|
|
19
|
+
* the replacement.
|
|
20
|
+
*/
|
|
21
|
+
export declare function $correctPunctuationBeforeCaret(rules: PunctuationRule[]): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Applies the punctuation rules to every paragraph and cell in the document.
|
|
24
|
+
* Where two matches overlap, the longer one wins. Returns how many were applied.
|
|
25
|
+
*/
|
|
26
|
+
export declare function $correctPunctuationDocument(rules: PunctuationRule[]): number;
|
|
27
|
+
/**
|
|
28
|
+
* Runs `change` and then puts the caret back where it was. Replacements move the
|
|
29
|
+
* selection onto each match they make, so a whole-document pass would otherwise
|
|
30
|
+
* leave the caret at the last match, or at the end of the document.
|
|
31
|
+
*/
|
|
32
|
+
export declare function $preservingSelection(change: () => void): void;
|
|
33
|
+
/** Applies urduhack-style punctuation spacing to every text node. Returns how many changed. */
|
|
34
|
+
export declare function $spacePunctuationDocument(): number;
|
|
35
|
+
//# sourceMappingURL=autoCorrectActions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"autoCorrectActions.d.ts","sourceRoot":"","sources":["../../src/autocorrect/autoCorrectActions.ts"],"names":[],"mappings":"AAWA,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAMhF;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,CAiCjH;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EACxB,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EACxB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GACtC,MAAM,CA8BR;AAQD;;;;GAIG;AACH,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,OAAO,CAqBhF;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,MAAM,CA2B5E;AAID;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,IAAI,GAAG,IAAI,CAoB7D;AAED,+FAA+F;AAC/F,wBAAgB,yBAAyB,IAAI,MAAM,CAWlD"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { $createRangeSelection, $getNodeByKey, $getRoot, $getSelection, $isRangeSelection, $isTextNode, $setSelection, } from 'lexical';
|
|
2
|
+
import { $locate, $replaceMatch, $textGroups } from '../find/findReplaceActions.js';
|
|
3
|
+
import { wordsIn } from '../spellcheck/spellDictionaries.js';
|
|
4
|
+
import { spacePunctuationText } from './punctuationRules.js';
|
|
5
|
+
/** The part of a word that counts as word characters at its end. */
|
|
6
|
+
const WORD_END = /[\p{L}\p{M}'’]+$/u;
|
|
7
|
+
const WORD_CHAR = /[\p{L}\p{M}'’]/u;
|
|
8
|
+
/**
|
|
9
|
+
* Corrects the word that ends just before the caret, when the character just
|
|
10
|
+
* typed (the word boundary: space or punctuation) is not a word character. The
|
|
11
|
+
* correction replaces only the word, and the caret stays right after the
|
|
12
|
+
* boundary. Returns true if a correction was made.
|
|
13
|
+
*/
|
|
14
|
+
export function $correctWordBeforeCaret(table, normalize) {
|
|
15
|
+
const selection = $getSelection();
|
|
16
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed())
|
|
17
|
+
return false;
|
|
18
|
+
const anchor = selection.anchor;
|
|
19
|
+
const node = anchor.getNode();
|
|
20
|
+
if (!$isTextNode(node))
|
|
21
|
+
return false;
|
|
22
|
+
const before = node.getTextContent().slice(0, anchor.offset);
|
|
23
|
+
const boundary = before.slice(-1);
|
|
24
|
+
if (boundary === '' || WORD_CHAR.test(boundary))
|
|
25
|
+
return false;
|
|
26
|
+
const body = before.slice(0, -1);
|
|
27
|
+
const word = WORD_END.exec(body)?.[0];
|
|
28
|
+
if (!word)
|
|
29
|
+
return false;
|
|
30
|
+
const replacement = correctionFor(word, table, normalize);
|
|
31
|
+
if (replacement === undefined || replacement === word)
|
|
32
|
+
return false;
|
|
33
|
+
const start = body.length - word.length;
|
|
34
|
+
$replaceMatch({ anchorKey: node.getKey(), anchorOffset: start, focusKey: node.getKey(), focusOffset: body.length }, replacement);
|
|
35
|
+
// Put the caret back after the boundary the user typed, not at the end of the replacement.
|
|
36
|
+
const after = $getSelection();
|
|
37
|
+
if ($isRangeSelection(after)) {
|
|
38
|
+
const focusNode = after.focus.getNode();
|
|
39
|
+
const caret = after.focus.offset + boundary.length;
|
|
40
|
+
after.anchor.set(focusNode.getKey(), caret, 'text');
|
|
41
|
+
after.focus.set(focusNode.getKey(), caret, 'text');
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Corrects every whole word in the document, for text that never went through
|
|
47
|
+
* typing (pasted or loaded content). Words are found per paragraph or cell, so a
|
|
48
|
+
* word split by formatting is still one word. Each block uses the table for its
|
|
49
|
+
* direction. Returns how many words were corrected.
|
|
50
|
+
*/
|
|
51
|
+
export function $correctDocument(ltr, rtl, normalizeRtl) {
|
|
52
|
+
if (normalizeRtl) {
|
|
53
|
+
for (const node of $getRoot().getAllTextNodes()) {
|
|
54
|
+
if (node.getParentOrThrow().getDirection() !== 'rtl')
|
|
55
|
+
continue;
|
|
56
|
+
const text = node.getTextContent();
|
|
57
|
+
const normalized = normalizeRtl(text);
|
|
58
|
+
if (normalized !== text)
|
|
59
|
+
node.setTextContent(normalized);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
let corrected = 0;
|
|
63
|
+
for (const group of $textGroups()) {
|
|
64
|
+
const rightToLeft = group[0].getParentOrThrow().getDirection() === 'rtl';
|
|
65
|
+
const table = rightToLeft ? rtl : ltr;
|
|
66
|
+
const text = group.map((node) => node.getTextContent()).join('');
|
|
67
|
+
const spans = wordsIn(text)
|
|
68
|
+
.map((span) => ({ span, replacement: correctionFor(span.word, table, rightToLeft ? normalizeRtl : undefined) }))
|
|
69
|
+
.filter((item) => item.replacement !== undefined && item.replacement !== item.span.word);
|
|
70
|
+
// Right to left, so earlier positions stay valid as each word is replaced.
|
|
71
|
+
for (const { span, replacement } of spans.reverse()) {
|
|
72
|
+
const start = $locate(group, span.start, false);
|
|
73
|
+
const end = $locate(group, span.end, true);
|
|
74
|
+
$replaceMatch({ anchorKey: start.key, anchorOffset: start.offset, focusKey: end.key, focusOffset: end.offset }, replacement);
|
|
75
|
+
corrected += 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return corrected;
|
|
79
|
+
}
|
|
80
|
+
/** What a word should become: its normalised form, then any table correction for that. */
|
|
81
|
+
function correctionFor(word, table, normalize) {
|
|
82
|
+
const normalized = normalize ? normalize(word) : word;
|
|
83
|
+
return table.get(normalized) ?? (normalized !== word ? normalized : undefined);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Applies a punctuation rule to the text just before the caret, when that text
|
|
87
|
+
* ends with a rule's incorrect form. Longest match first. The caret ends after
|
|
88
|
+
* the replacement.
|
|
89
|
+
*/
|
|
90
|
+
export function $correctPunctuationBeforeCaret(rules) {
|
|
91
|
+
const selection = $getSelection();
|
|
92
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed())
|
|
93
|
+
return false;
|
|
94
|
+
const anchor = selection.anchor;
|
|
95
|
+
const node = anchor.getNode();
|
|
96
|
+
if (!$isTextNode(node))
|
|
97
|
+
return false;
|
|
98
|
+
const before = node.getTextContent().slice(0, anchor.offset);
|
|
99
|
+
const rule = rules.find((r) => before.endsWith(r.incorrect) && (!r.completeWord || !WORD_CHAR.test(before.charAt(before.length - r.incorrect.length - 1) || ' ')));
|
|
100
|
+
if (!rule)
|
|
101
|
+
return false;
|
|
102
|
+
const start = before.length - rule.incorrect.length;
|
|
103
|
+
$replaceMatch({ anchorKey: node.getKey(), anchorOffset: start, focusKey: node.getKey(), focusOffset: before.length }, rule.correct);
|
|
104
|
+
const after = $getSelection();
|
|
105
|
+
if ($isRangeSelection(after)) {
|
|
106
|
+
const caret = start + rule.correct.length;
|
|
107
|
+
after.anchor.set(node.getKey(), caret, 'text');
|
|
108
|
+
after.focus.set(node.getKey(), caret, 'text');
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Applies the punctuation rules to every paragraph and cell in the document.
|
|
114
|
+
* Where two matches overlap, the longer one wins. Returns how many were applied.
|
|
115
|
+
*/
|
|
116
|
+
export function $correctPunctuationDocument(rules) {
|
|
117
|
+
let applied = 0;
|
|
118
|
+
for (const group of $textGroups()) {
|
|
119
|
+
const text = group.map((node) => node.getTextContent()).join('');
|
|
120
|
+
const candidates = [];
|
|
121
|
+
for (const rule of rules) {
|
|
122
|
+
let index = text.indexOf(rule.incorrect);
|
|
123
|
+
while (index !== -1) {
|
|
124
|
+
candidates.push({ start: index, end: index + rule.incorrect.length, correct: rule.correct });
|
|
125
|
+
index = text.indexOf(rule.incorrect, index + 1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Longest first, then earliest; keep only matches that do not overlap a kept one.
|
|
129
|
+
candidates.sort((a, b) => b.end - b.start - (a.end - a.start) || a.start - b.start);
|
|
130
|
+
const kept = [];
|
|
131
|
+
for (const c of candidates) {
|
|
132
|
+
if (!kept.some((k) => c.start < k.end && k.start < c.end))
|
|
133
|
+
kept.push(c);
|
|
134
|
+
}
|
|
135
|
+
// Right to left, so earlier positions stay valid as each match is replaced.
|
|
136
|
+
for (const c of kept.sort((a, b) => b.start - a.start)) {
|
|
137
|
+
const start = $locate(group, c.start, false);
|
|
138
|
+
const end = $locate(group, c.end, true);
|
|
139
|
+
$replaceMatch({ anchorKey: start.key, anchorOffset: start.offset, focusKey: end.key, focusOffset: end.offset }, c.correct);
|
|
140
|
+
applied += 1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return applied;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Runs `change` and then puts the caret back where it was. Replacements move the
|
|
147
|
+
* selection onto each match they make, so a whole-document pass would otherwise
|
|
148
|
+
* leave the caret at the last match, or at the end of the document.
|
|
149
|
+
*/
|
|
150
|
+
export function $preservingSelection(change) {
|
|
151
|
+
const before = $getSelection();
|
|
152
|
+
const saved = $isRangeSelection(before)
|
|
153
|
+
? {
|
|
154
|
+
anchor: { key: before.anchor.key, offset: before.anchor.offset, type: before.anchor.type },
|
|
155
|
+
focus: { key: before.focus.key, offset: before.focus.offset, type: before.focus.type },
|
|
156
|
+
}
|
|
157
|
+
: null;
|
|
158
|
+
change();
|
|
159
|
+
if (!saved) {
|
|
160
|
+
$setSelection(null);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (!$getNodeByKey(saved.anchor.key) || !$getNodeByKey(saved.focus.key))
|
|
164
|
+
return;
|
|
165
|
+
const selection = $createRangeSelection();
|
|
166
|
+
selection.anchor.set(saved.anchor.key, saved.anchor.offset, saved.anchor.type);
|
|
167
|
+
selection.focus.set(saved.focus.key, saved.focus.offset, saved.focus.type);
|
|
168
|
+
$setSelection(selection);
|
|
169
|
+
}
|
|
170
|
+
/** Applies urduhack-style punctuation spacing to every text node. Returns how many changed. */
|
|
171
|
+
export function $spacePunctuationDocument() {
|
|
172
|
+
let changed = 0;
|
|
173
|
+
for (const node of $getRoot().getAllTextNodes()) {
|
|
174
|
+
const text = node.getTextContent();
|
|
175
|
+
const spaced = spacePunctuationText(text);
|
|
176
|
+
if (spaced !== text) {
|
|
177
|
+
node.setTextContent(spaced);
|
|
178
|
+
changed += 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return changed;
|
|
182
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { SpellLanguage } from '../spellcheck/spellDictionaries.js';
|
|
2
|
+
/** One correction: a typed word and what it should become. */
|
|
3
|
+
export interface AutoCorrectEntry {
|
|
4
|
+
from: string;
|
|
5
|
+
to: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Where auto-corrections come from and go. The host supplies any number of
|
|
9
|
+
* stores; corrections from all of them are merged, and when two stores
|
|
10
|
+
* correct the same word, the one listed first wins. New corrections are
|
|
11
|
+
* appended to the first store that accepts writes.
|
|
12
|
+
*/
|
|
13
|
+
export interface AutoCorrectStore {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
/** A store without `append` (or with `readOnly`) is never written to. */
|
|
16
|
+
readonly readOnly?: boolean;
|
|
17
|
+
load(language: SpellLanguage): Promise<AutoCorrectEntry[]>;
|
|
18
|
+
append?(language: SpellLanguage, entry: AutoCorrectEntry): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Corrections kept in this browser's localStorage. Storage can be disabled or
|
|
22
|
+
* full, so every access is guarded: a failure means "no corrections", never a
|
|
23
|
+
* broken editor.
|
|
24
|
+
*/
|
|
25
|
+
export declare function localStorageAutoCorrectStore(options?: {
|
|
26
|
+
key?: string;
|
|
27
|
+
}): AutoCorrectStore;
|
|
28
|
+
/**
|
|
29
|
+
* Corrections served by an API. `load` calls `GET {url}?language=xx` and expects
|
|
30
|
+
* a JSON array of `{ from, to }`; `append` calls `POST {url}` with
|
|
31
|
+
* `{ language, from, to }`. Credentials and headers belong to the host.
|
|
32
|
+
*/
|
|
33
|
+
export declare function apiAutoCorrectStore(options: {
|
|
34
|
+
id?: string;
|
|
35
|
+
url: string;
|
|
36
|
+
headers?: Record<string, string>;
|
|
37
|
+
fetch?: typeof fetch;
|
|
38
|
+
}): AutoCorrectStore;
|
|
39
|
+
/**
|
|
40
|
+
* Corrections from a JSON file the host publishes — a list of `{ language, from, to }`.
|
|
41
|
+
* Read-only: it has no way to write back to a static file.
|
|
42
|
+
*/
|
|
43
|
+
export declare function fileAutoCorrectStore(options: {
|
|
44
|
+
id?: string;
|
|
45
|
+
url: string;
|
|
46
|
+
fetch?: typeof fetch;
|
|
47
|
+
}): AutoCorrectStore;
|
|
48
|
+
/** Corrections for one language, merged across stores. Failing stores are skipped. */
|
|
49
|
+
export declare function loadAutoCorrections(stores: AutoCorrectStore[], language: SpellLanguage): Promise<Map<string, string>>;
|
|
50
|
+
/** Saves a correction to the first store that accepts writes. Returns false if none does. */
|
|
51
|
+
export declare function appendAutoCorrection(stores: AutoCorrectStore[], language: SpellLanguage, entry: AutoCorrectEntry): Promise<boolean>;
|
|
52
|
+
//# sourceMappingURL=autoCorrectStores.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"autoCorrectStores.d.ts","sourceRoot":"","sources":["../../src/autocorrect/autoCorrectStores.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAErE,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,IAAI,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC;IAC3D,MAAM,CAAC,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1E;AAkBD;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,gBAAgB,CA6B7F;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE;IAC3C,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,gBAAgB,CAmBnB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;CAAE,GAAG,gBAAgB,CAkBlH;AAED,sFAAsF;AACtF,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,gBAAgB,EAAE,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAQ3H;AAED,6FAA6F;AAC7F,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,gBAAgB,EAAE,EAAE,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAKzI"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const STORAGE_KEY = 'likhari-autocorrect';
|
|
2
|
+
function isEntry(value) {
|
|
3
|
+
return (typeof value === 'object' &&
|
|
4
|
+
value !== null &&
|
|
5
|
+
typeof value.from === 'string' &&
|
|
6
|
+
typeof value.to === 'string' &&
|
|
7
|
+
value.from !== '');
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Corrections kept in this browser's localStorage. Storage can be disabled or
|
|
11
|
+
* full, so every access is guarded: a failure means "no corrections", never a
|
|
12
|
+
* broken editor.
|
|
13
|
+
*/
|
|
14
|
+
export function localStorageAutoCorrectStore(options = {}) {
|
|
15
|
+
const key = options.key ?? STORAGE_KEY;
|
|
16
|
+
function readAll() {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(localStorage.getItem(key) ?? '[]');
|
|
19
|
+
return Array.isArray(parsed) ? parsed.filter((e) => isEntry(e) && 'language' in e) : [];
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
id: 'local-storage',
|
|
27
|
+
async load(language) {
|
|
28
|
+
return readAll()
|
|
29
|
+
.filter((e) => e.language === language)
|
|
30
|
+
.map(({ from, to }) => ({ from, to }));
|
|
31
|
+
},
|
|
32
|
+
async append(language, entry) {
|
|
33
|
+
const all = readAll().filter((e) => !(e.language === language && e.from === entry.from));
|
|
34
|
+
all.push({ language, from: entry.from, to: entry.to });
|
|
35
|
+
try {
|
|
36
|
+
localStorage.setItem(key, JSON.stringify(all));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Quota exceeded or storage disabled: the correction is kept for this session only.
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Corrections served by an API. `load` calls `GET {url}?language=xx` and expects
|
|
46
|
+
* a JSON array of `{ from, to }`; `append` calls `POST {url}` with
|
|
47
|
+
* `{ language, from, to }`. Credentials and headers belong to the host.
|
|
48
|
+
*/
|
|
49
|
+
export function apiAutoCorrectStore(options) {
|
|
50
|
+
const doFetch = options.fetch ?? fetch;
|
|
51
|
+
return {
|
|
52
|
+
id: options.id ?? 'api',
|
|
53
|
+
async load(language) {
|
|
54
|
+
const response = await doFetch(`${options.url}?language=${encodeURIComponent(language)}`, { headers: options.headers });
|
|
55
|
+
if (!response.ok)
|
|
56
|
+
throw new Error(`Auto-correct API failed: ${response.status}`);
|
|
57
|
+
const data = await response.json();
|
|
58
|
+
return Array.isArray(data) ? data.filter(isEntry).map(({ from, to }) => ({ from, to })) : [];
|
|
59
|
+
},
|
|
60
|
+
async append(language, entry) {
|
|
61
|
+
const response = await doFetch(options.url, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'Content-Type': 'application/json', ...options.headers },
|
|
64
|
+
body: JSON.stringify({ language, from: entry.from, to: entry.to }),
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok)
|
|
67
|
+
throw new Error(`Auto-correct API rejected the entry: ${response.status}`);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Corrections from a JSON file the host publishes — a list of `{ language, from, to }`.
|
|
73
|
+
* Read-only: it has no way to write back to a static file.
|
|
74
|
+
*/
|
|
75
|
+
export function fileAutoCorrectStore(options) {
|
|
76
|
+
const doFetch = options.fetch ?? fetch;
|
|
77
|
+
let cached = null;
|
|
78
|
+
const all = () => {
|
|
79
|
+
cached ?? (cached = doFetch(options.url).then(async (response) => {
|
|
80
|
+
if (!response.ok)
|
|
81
|
+
throw new Error(`Auto-correct file failed: ${response.status}`);
|
|
82
|
+
const data = await response.json();
|
|
83
|
+
return Array.isArray(data) ? data.filter((e) => isEntry(e) && 'language' in e) : [];
|
|
84
|
+
}));
|
|
85
|
+
return cached;
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
id: options.id ?? 'file',
|
|
89
|
+
readOnly: true,
|
|
90
|
+
async load(language) {
|
|
91
|
+
return (await all()).filter((e) => e.language === language).map(({ from, to }) => ({ from, to }));
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** Corrections for one language, merged across stores. Failing stores are skipped. */
|
|
96
|
+
export async function loadAutoCorrections(stores, language) {
|
|
97
|
+
const table = new Map();
|
|
98
|
+
const results = await Promise.allSettled(stores.map((store) => store.load(language)));
|
|
99
|
+
results.forEach((result) => {
|
|
100
|
+
if (result.status !== 'fulfilled')
|
|
101
|
+
return;
|
|
102
|
+
for (const entry of result.value)
|
|
103
|
+
if (!table.has(entry.from))
|
|
104
|
+
table.set(entry.from, entry.to);
|
|
105
|
+
});
|
|
106
|
+
return table;
|
|
107
|
+
}
|
|
108
|
+
/** Saves a correction to the first store that accepts writes. Returns false if none does. */
|
|
109
|
+
export async function appendAutoCorrection(stores, language, entry) {
|
|
110
|
+
const writable = stores.find((store) => !store.readOnly && store.append);
|
|
111
|
+
if (!writable?.append)
|
|
112
|
+
return false;
|
|
113
|
+
await writable.append(language, entry);
|
|
114
|
+
return true;
|
|
115
|
+
}
|