@inshapardaz/likhari-react 0.1.20 → 0.1.21-dev.194

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.
@@ -0,0 +1,40 @@
1
+ /** Languages the spellchecker has a dictionary slot for. */
2
+ export type SpellLanguage = 'en' | 'ur' | 'pa-shahmukhi';
3
+ /** A Hunspell dictionary as the two text files every Hunspell dictionary ships with. */
4
+ export interface HunspellFiles {
5
+ /** Affix file: language, encoding, character rules. */
6
+ aff: string;
7
+ /** Dictionary file: one word per line, with optional affix flags. */
8
+ dic: string;
9
+ }
10
+ export interface Speller {
11
+ correct(word: string): boolean;
12
+ suggest(word: string): string[];
13
+ }
14
+ type Loader = () => Promise<HunspellFiles>;
15
+ /**
16
+ * Registers a dictionary for a language. Call once at startup with files you
17
+ * have licence to ship — e.g. the Urdu `.aff`/`.dic` pair — either as strings
18
+ * or as a loader that fetches them on demand. Registering again replaces the
19
+ * previous dictionary for that language.
20
+ */
21
+ export declare function registerSpellDictionary(language: SpellLanguage, files: HunspellFiles | Loader): void;
22
+ /** Whether a dictionary is registered for the language. */
23
+ export declare function hasSpellDictionary(language: SpellLanguage): boolean;
24
+ /**
25
+ * The speller for a language, loading its dictionary the first time it is
26
+ * asked for. Resolves to null when no dictionary is registered, so callers
27
+ * can skip spellchecking that language rather than flag every word.
28
+ */
29
+ export declare function getSpeller(language: SpellLanguage): Promise<Speller> | null;
30
+ /** A word in a text, with its position as character offsets. */
31
+ export interface WordSpan {
32
+ word: string;
33
+ start: number;
34
+ end: number;
35
+ }
36
+ export declare function wordsIn(text: string): WordSpan[];
37
+ /** The words in `text` the speller does not recognise. */
38
+ export declare function misspellingsIn(text: string, speller: Speller): WordSpan[];
39
+ export {};
40
+ //# sourceMappingURL=spellDictionaries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spellDictionaries.d.ts","sourceRoot":"","sources":["../../src/spellcheck/spellDictionaries.ts"],"names":[],"mappings":"AAEA,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,CAS3E;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"}
@@ -0,0 +1,62 @@
1
+ import nspell from 'nspell';
2
+ const loaders = new Map();
3
+ const spellers = new Map();
4
+ async function fetchText(url) {
5
+ const response = await fetch(url);
6
+ if (!response.ok)
7
+ throw new Error(`Spellcheck dictionary failed to load: ${url}`);
8
+ return response.text();
9
+ }
10
+ /** English ships with the editor: its files are fetched on first use. */
11
+ const englishLoader = async () => ({
12
+ aff: await fetchText(new URL('./dictionaries/en/index.aff', import.meta.url).href),
13
+ dic: await fetchText(new URL('./dictionaries/en/index.dic', import.meta.url).href),
14
+ });
15
+ loaders.set('en', englishLoader);
16
+ /**
17
+ * Registers a dictionary for a language. Call once at startup with files you
18
+ * have licence to ship — e.g. the Urdu `.aff`/`.dic` pair — either as strings
19
+ * or as a loader that fetches them on demand. Registering again replaces the
20
+ * previous dictionary for that language.
21
+ */
22
+ export function registerSpellDictionary(language, files) {
23
+ loaders.set(language, typeof files === 'function' ? files : async () => files);
24
+ spellers.delete(language);
25
+ }
26
+ /** Whether a dictionary is registered for the language. */
27
+ export function hasSpellDictionary(language) {
28
+ return loaders.has(language);
29
+ }
30
+ /**
31
+ * The speller for a language, loading its dictionary the first time it is
32
+ * asked for. Resolves to null when no dictionary is registered, so callers
33
+ * can skip spellchecking that language rather than flag every word.
34
+ */
35
+ export function getSpeller(language) {
36
+ const loader = loaders.get(language);
37
+ if (!loader)
38
+ return null;
39
+ let speller = spellers.get(language);
40
+ if (!speller) {
41
+ speller = loader().then(({ aff, dic }) => nspell(aff, dic));
42
+ spellers.set(language, speller);
43
+ }
44
+ return speller;
45
+ }
46
+ /**
47
+ * Letters and combining marks, with apostrophes inside a word kept. The
48
+ * Unicode classes cover Arabic-script letters and their diacritics, so Urdu
49
+ * and Shahmukhi words are found the same way English ones are.
50
+ */
51
+ const WORD = /[\p{L}\p{M}]+(?:['’][\p{L}\p{M}]+)*/gu;
52
+ export function wordsIn(text) {
53
+ return [...text.matchAll(WORD)].map((match) => ({
54
+ word: match[0],
55
+ start: match.index ?? 0,
56
+ end: (match.index ?? 0) + match[0].length,
57
+ }));
58
+ }
59
+ /** The words in `text` the speller does not recognise. */
60
+ export function misspellingsIn(text, speller) {
61
+ return wordsIn(text).filter((span) => !speller.correct(span.word));
62
+ }
@@ -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
+ }
@@ -870,3 +870,130 @@
870
870
  opacity: 0.8;
871
871
  white-space: nowrap;
872
872
  }
873
+
874
+ /* Spellcheck panel: same top-right floating spot as the find widget, which
875
+ closes when this opens. */
876
+ .likhari-spell-panel {
877
+ position: absolute;
878
+ z-index: 5;
879
+ top: 8px;
880
+ right: 16px;
881
+ width: max-content;
882
+ max-width: min(360px, calc(100% - 32px));
883
+ max-height: 60%;
884
+ overflow-y: auto;
885
+ overflow-x: hidden;
886
+ padding: 8px;
887
+ border: 1px solid var(--editor-line);
888
+ border-radius: 6px;
889
+ background: var(--editor-paper-dim);
890
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
891
+ font-size: 13px;
892
+ }
893
+
894
+ .likhari-spell-panel[dir='rtl'] {
895
+ right: auto;
896
+ left: 16px;
897
+ }
898
+
899
+ .likhari-spell-header,
900
+ .likhari-spell-controls {
901
+ display: flex;
902
+ flex-wrap: wrap;
903
+ align-items: center;
904
+ justify-content: space-between;
905
+ gap: 6px;
906
+ margin-bottom: 6px;
907
+ }
908
+
909
+ .likhari-spell-controls select {
910
+ min-width: 0;
911
+ max-width: 100%;
912
+ }
913
+
914
+ .likhari-spell-panel button {
915
+ padding: 2px 8px;
916
+ border: 1px solid var(--editor-line);
917
+ border-radius: 4px;
918
+ background: transparent;
919
+ color: inherit;
920
+ cursor: pointer;
921
+ }
922
+
923
+ .likhari-spell-panel button:disabled {
924
+ opacity: 0.5;
925
+ cursor: default;
926
+ }
927
+
928
+ .likhari-spell-icon {
929
+ display: inline-flex;
930
+ align-items: center;
931
+ justify-content: center;
932
+ width: 24px;
933
+ height: 24px;
934
+ padding: 0 !important;
935
+ border: none !important;
936
+ }
937
+
938
+ .likhari-spell-note {
939
+ margin: 4px 0;
940
+ opacity: 0.8;
941
+ }
942
+
943
+ .likhari-spell-list {
944
+ list-style: none;
945
+ margin: 0;
946
+ padding: 0;
947
+ }
948
+
949
+ .likhari-spell-list li {
950
+ display: flex;
951
+ flex-wrap: wrap;
952
+ align-items: center;
953
+ gap: 4px;
954
+ padding: 4px 0;
955
+ border-top: 1px solid var(--editor-line);
956
+ }
957
+
958
+ .likhari-spell-word {
959
+ font-weight: 600;
960
+ color: #b42318 !important;
961
+ }
962
+
963
+ .likhari-spell-suggestion {
964
+ font-size: 12px;
965
+ }
966
+
967
+ /* Misspelled words, underlined like the browser's own spellcheck. Styled
968
+ through the CSS Custom Highlight API by SpellHighlightPlugin. */
969
+ ::highlight(likhari-spelling) {
970
+ text-decoration: red wavy underline;
971
+ text-decoration-skip-ink: none;
972
+ }
973
+
974
+ .likhari-spell-nav {
975
+ display: flex;
976
+ align-items: center;
977
+ justify-content: flex-end;
978
+ gap: 4px;
979
+ margin-bottom: 6px;
980
+ }
981
+
982
+ .likhari-spell-status {
983
+ opacity: 0.8;
984
+ }
985
+
986
+ .likhari-spell-item {
987
+ display: flex;
988
+ flex-direction: column;
989
+ gap: 6px;
990
+ padding-top: 6px;
991
+ border-top: 1px solid var(--editor-line);
992
+ }
993
+
994
+ .likhari-spell-suggestions {
995
+ display: flex;
996
+ flex-wrap: wrap;
997
+ gap: 4px;
998
+ }
999
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inshapardaz/likhari-react",
3
- "version": "0.1.20",
3
+ "version": "0.1.21-dev.194",
4
4
  "description": "React implementation of the Likhari editor, built on @lexical/react.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,10 +29,12 @@
29
29
  "*.css"
30
30
  ],
31
31
  "scripts": {
32
- "build": "tsc -p tsconfig.build.json && node ../../scripts/fix-esm-extensions.mjs ./dist && node ../../scripts/copy-assets.mjs src dist \"**/*.css\"",
32
+ "build": "tsc -p tsconfig.build.json && node ../../scripts/fix-esm-extensions.mjs ./dist && node ../../scripts/copy-assets.mjs src dist \"**/*.css\" && node ../../scripts/copy-assets.mjs src dist \"**/*.aff\" && node ../../scripts/copy-assets.mjs src dist \"**/*.dic\" && node ../../scripts/copy-assets.mjs src dist \"**/*.txt\"",
33
33
  "prepublishOnly": "npm run build"
34
34
  },
35
35
  "dependencies": {
36
+ "@inshapardaz/likhari-converters": "^0.1.0",
37
+ "@inshapardaz/likhari-core": "^0.1.0",
36
38
  "@lexical/link": "^0.20.0",
37
39
  "@lexical/list": "^0.20.0",
38
40
  "@lexical/react": "^0.20.0",
@@ -40,10 +42,9 @@
40
42
  "@lexical/selection": "^0.20.0",
41
43
  "@lexical/table": "^0.20.0",
42
44
  "@lexical/utils": "^0.20.0",
43
- "@inshapardaz/likhari-converters": "^0.1.0",
44
- "@inshapardaz/likhari-core": "^0.1.0",
45
45
  "@tabler/icons-react": "^3.46.0",
46
- "lexical": "^0.20.0"
46
+ "lexical": "^0.20.0",
47
+ "nspell": "^2.1.5"
47
48
  },
48
49
  "peerDependencies": {
49
50
  "@mantine/core": "^9.6.3",
@@ -53,6 +54,7 @@
53
54
  },
54
55
  "devDependencies": {
55
56
  "@mantine/core": "^9.6.3",
56
- "@mantine/hooks": "^9.6.3"
57
+ "@mantine/hooks": "^9.6.3",
58
+ "@types/nspell": "^2.1.6"
57
59
  }
58
60
  }