@kensio/pinyinjs 1.6.0 → 1.7.1

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.
Files changed (88) hide show
  1. package/README.md +211 -5
  2. package/data/core.entries +393 -392
  3. package/data/core.freq +0 -0
  4. package/data/core.keys +1 -0
  5. package/data/full.entries +10220 -10220
  6. package/data/manifest.json +12 -12
  7. package/data/script.map +7 -7
  8. package/data/standard.entries +957 -956
  9. package/data/standard.freq +0 -0
  10. package/data/standard.keys +1 -0
  11. package/dist/cli/arguments.d.ts +38 -0
  12. package/dist/cli/arguments.d.ts.map +1 -1
  13. package/dist/cli/arguments.js +46 -0
  14. package/dist/cli/arguments.js.map +1 -1
  15. package/dist/cli/commands.d.ts.map +1 -1
  16. package/dist/cli/commands.js +207 -1
  17. package/dist/cli/commands.js.map +1 -1
  18. package/dist/cli/help.d.ts.map +1 -1
  19. package/dist/cli/help.js +5 -0
  20. package/dist/cli/help.js.map +1 -1
  21. package/dist/decode/convert.d.ts +16 -0
  22. package/dist/decode/convert.d.ts.map +1 -1
  23. package/dist/decode/convert.js +3 -28
  24. package/dist/decode/convert.js.map +1 -1
  25. package/dist/decode/runs.d.ts +11 -0
  26. package/dist/decode/runs.d.ts.map +1 -1
  27. package/dist/decode/runs.js +19 -0
  28. package/dist/decode/runs.js.map +1 -1
  29. package/dist/decode/segment.d.ts +66 -0
  30. package/dist/decode/segment.d.ts.map +1 -0
  31. package/dist/decode/segment.js +79 -0
  32. package/dist/decode/segment.js.map +1 -0
  33. package/dist/dictionary/artifact.d.ts +10 -0
  34. package/dist/dictionary/artifact.d.ts.map +1 -1
  35. package/dist/dictionary/artifact.js +17 -0
  36. package/dist/dictionary/artifact.js.map +1 -1
  37. package/dist/dictionary/assertions.d.ts.map +1 -1
  38. package/dist/dictionary/assertions.js +38 -0
  39. package/dist/dictionary/assertions.js.map +1 -1
  40. package/dist/dictionary/corpus-mass.d.ts +68 -0
  41. package/dist/dictionary/corpus-mass.d.ts.map +1 -0
  42. package/dist/dictionary/corpus-mass.js +163 -0
  43. package/dist/dictionary/corpus-mass.js.map +1 -0
  44. package/dist/dictionary/dictionary.d.ts +56 -0
  45. package/dist/dictionary/dictionary.d.ts.map +1 -1
  46. package/dist/dictionary/dictionary.js +116 -1
  47. package/dist/dictionary/dictionary.js.map +1 -1
  48. package/dist/dictionary/erhua.d.ts +4 -0
  49. package/dist/dictionary/erhua.d.ts.map +1 -1
  50. package/dist/dictionary/erhua.js +6 -0
  51. package/dist/dictionary/erhua.js.map +1 -1
  52. package/dist/dictionary/merge.d.ts.map +1 -1
  53. package/dist/dictionary/merge.js +16 -8
  54. package/dist/dictionary/merge.js.map +1 -1
  55. package/dist/dictionary/tiers.d.ts +2 -1
  56. package/dist/dictionary/tiers.d.ts.map +1 -1
  57. package/dist/dictionary/tiers.js +77 -11
  58. package/dist/dictionary/tiers.js.map +1 -1
  59. package/dist/grade/check.d.ts +156 -0
  60. package/dist/grade/check.d.ts.map +1 -0
  61. package/dist/grade/check.js +356 -0
  62. package/dist/grade/check.js.map +1 -0
  63. package/dist/index.d.ts +11 -1
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +5 -0
  66. package/dist/index.js.map +1 -1
  67. package/dist/search/candidates.d.ts +84 -0
  68. package/dist/search/candidates.d.ts.map +1 -0
  69. package/dist/search/candidates.js +290 -0
  70. package/dist/search/candidates.js.map +1 -0
  71. package/dist/search/match.d.ts +72 -0
  72. package/dist/search/match.d.ts.map +1 -0
  73. package/dist/search/match.js +419 -0
  74. package/dist/search/match.js.map +1 -0
  75. package/dist/search/query.d.ts +49 -0
  76. package/dist/search/query.d.ts.map +1 -0
  77. package/dist/search/query.js +115 -0
  78. package/dist/search/query.js.map +1 -0
  79. package/dist/search/reverse-index.d.ts +141 -0
  80. package/dist/search/reverse-index.d.ts.map +1 -0
  81. package/dist/search/reverse-index.js +356 -0
  82. package/dist/search/reverse-index.js.map +1 -0
  83. package/dist/sources/unihan.d.ts +9 -0
  84. package/dist/sources/unihan.d.ts.map +1 -1
  85. package/dist/sources/unihan.js +12 -1
  86. package/dist/sources/unihan.js.map +1 -1
  87. package/package.json +2 -3
  88. package/CHANGELOG.md +0 -634
@@ -0,0 +1,290 @@
1
+ import { encodeReading } from "../dictionary/artifact.js";
2
+ import { convertCharacters } from "../script/conversion.js";
3
+ import { toCanonicalGlyphs } from "../script/glyphs.js";
4
+ import { normaliseUmlaut, readSyllable, writeSyllable, } from "../syllable/syllable.js";
5
+ import { applyToneMark, stripToneMarks } from "../tone/tone-mark.js";
6
+ import { TONES } from "../tone/tone.js";
7
+ import { readingKey } from "./reverse-index.js";
8
+ /**
9
+ * What a query may write between syllables, all of which mean nothing here.
10
+ *
11
+ * The same set the matcher accepts, minus its meaning: `match` treats a written
12
+ * boundary as evidence, because a haystack it is filtering can be read more than
13
+ * one way. A reverse index is asked for one reading and answers with the words
14
+ * that have it, so `yin hang` and `yinhang` are the same question.
15
+ */
16
+ const SEPARATORS = /[\s'’‘-]+/gu;
17
+ /**
18
+ * A tone written as a digit, which is the last character of a syllable.
19
+ */
20
+ const TONE_DIGIT = /^[0-5]$/u;
21
+ /**
22
+ * Every tone a syllable that wrote none could be standing for.
23
+ */
24
+ const TONE_DIGITS = ["1", "2", "3", "4", "5"];
25
+ /**
26
+ * The umlaut the readings are written with, and the letter a keyboard has.
27
+ */
28
+ const UMLAUT = "ü";
29
+ const PLAIN_U = "u";
30
+ /**
31
+ * The 儿化 suffix, which is the one letter a reading key keeps and a query may
32
+ * leave off.
33
+ */
34
+ const ERHUA = "r";
35
+ /**
36
+ * Put a typed query into both the forms a search needs.
37
+ *
38
+ * Case, spacing and the ways of writing ü are all noise on the way to the key,
39
+ * and the tone is noise there too — the index is keyed toneless. None of it is
40
+ * noise on the way to the *filter*, which is what honours a tone the query
41
+ * wrote, so the resolved spelling is kept alongside.
42
+ *
43
+ * A tone written as a mark counts here, unlike in `match`. The reason it cannot
44
+ * count there is that a mark says nothing about where its syllable ends and a
45
+ * half-typed query is exactly where that is unsettled; here the candidate's own
46
+ * reading says where every syllable ends, so the mark has somewhere to land.
47
+ */
48
+ function readQuery(query) {
49
+ const written = normaliseUmlaut(query.normalize("NFC").toLowerCase()).replaceAll(SEPARATORS, "");
50
+ return { key: readingKey(stripToneMarks(written)), written };
51
+ }
52
+ /**
53
+ * Every way a typist may write one syllable of a stored reading.
54
+ *
55
+ * `shi4` is written `shi`, `shi4` or `shì`; `lü4` adds `lu` and `lu4`, since ü
56
+ * is not on the keyboard; and an 儿化 `wanr2` adds the whole set again with the
57
+ * r left off, because 玩儿 is `wanr` to one typist and `wan` to another.
58
+ *
59
+ * The r comes off only where the syllable really is 儿化. 儿 itself is `er2`,
60
+ * and letting that shed its r would answer `e` with 儿.
61
+ */
62
+ function writingsOf(token) {
63
+ const digit = token.slice(-1);
64
+ const toned = TONE_DIGIT.test(digit);
65
+ const spelling = toned ? token.slice(0, -1) : token;
66
+ const syllable = readSyllable(token);
67
+ // A reading with no tone written cannot contradict one, so a query is free to
68
+ // write any. Every reading the shipped artifacts hold has a tone digit on it,
69
+ // so this is a case a hand-built dictionary reaches rather than a real one.
70
+ const tones = toned ? [digit] : TONE_DIGITS;
71
+ const written = new Set();
72
+ const add = (form) => {
73
+ const plain = form.replaceAll(UMLAUT, PLAIN_U);
74
+ written.add(form);
75
+ written.add(plain);
76
+ for (const tone of tones) {
77
+ written.add(`${form}${tone}`);
78
+ written.add(`${plain}${tone}`);
79
+ }
80
+ if (!toned) {
81
+ for (const tone of TONES) {
82
+ written.add(applyToneMark(form, tone));
83
+ }
84
+ }
85
+ };
86
+ add(spelling);
87
+ if (syllable !== undefined) {
88
+ written.add(writeSyllable(syllable, "marks"));
89
+ if (syllable.erhua === true) {
90
+ add(spelling.slice(0, -ERHUA.length));
91
+ written.add(writeSyllable({ ...syllable, erhua: false }, "marks"));
92
+ }
93
+ }
94
+ return [...written];
95
+ }
96
+ /**
97
+ * Whether a query spells a reading, syllable by syllable.
98
+ *
99
+ * Walked as a set of positions the query can have been read up to rather than
100
+ * one cursor, because a syllable can be written more than one way and the ways
101
+ * are different lengths: `wanr2` is `wan` or `wanr`, and only what follows says
102
+ * which was meant.
103
+ */
104
+ function spells(query, reading, writings) {
105
+ let reached = new Set([0]);
106
+ for (const token of reading.split(" ")) {
107
+ let forms = writings.get(token);
108
+ if (forms === undefined) {
109
+ forms = writingsOf(token);
110
+ writings.set(token, forms);
111
+ }
112
+ const next = new Set();
113
+ for (const at of reached) {
114
+ for (const form of forms) {
115
+ if (query.startsWith(form, at)) {
116
+ next.add(at + form.length);
117
+ }
118
+ }
119
+ }
120
+ if (next.size === 0) {
121
+ return false;
122
+ }
123
+ reached = next;
124
+ }
125
+ return reached.has(query.length);
126
+ }
127
+ /**
128
+ * The reading the dictionary itself reports for a word, in numbered spelling.
129
+ *
130
+ * Asked of the dictionary rather than re-derived, which is what makes the
131
+ * filter also the answer to the ghost keys: 校覈 is a key, `Dictionary.lookup`
132
+ * folds it to 校核 before it searches, and the reading that comes back is
133
+ * `jiao4 he2` rather than the `xiao4 he2` the raw key derives to. So a candidate
134
+ * the dictionary would disown does not survive being checked against it.
135
+ */
136
+ function readingOf(index, word) {
137
+ const entry = index.dictionary.lookup(word);
138
+ /* c8 ignore next -- only asked of a canonical key, which always looks up */
139
+ return entry === undefined ? "" : encodeReading(entry.reading);
140
+ }
141
+ /**
142
+ * The positions in every group a query can reach, likeliest first.
143
+ *
144
+ * Two groups, not one: the query as typed, and the query with the 儿化 r on the
145
+ * end. `wan` has to reach 玩儿 as well as 玩, and the r is kept in the key so
146
+ * that `wanr` can still reach only 玩儿. Whatever the second group brings in is
147
+ * then made honest by the filter, which is what stops `e` answering with 儿.
148
+ */
149
+ function rankedPositions(index, key) {
150
+ const found = [
151
+ ...index.positionsFor(key),
152
+ ...index.positionsFor(`${key}${ERHUA}`),
153
+ ];
154
+ // Both groups are already ranked, so this only has to interleave them. Sorted
155
+ // rather than merged because the comparison is one array read and the lists
156
+ // are short: the busiest reading in the full dictionary has 805 words in it.
157
+ return found.toSorted((left, right) => index.dictionary.frequencyAt(right) - index.dictionary.frequencyAt(left));
158
+ }
159
+ /**
160
+ * Keep one writing of a word where the group holds both scripts.
161
+ *
162
+ * Paired by 简体 form in both directions rather than by converting toward the
163
+ * preference, because simplification is many-to-one and so the 简体 form is the
164
+ * one both writings agree on. Going the other way needs a reading to tell 發
165
+ * from 髮, and a pair that failed to meet would simply be left as two.
166
+ *
167
+ * The kept writing takes the rank of the better-placed of the two, so
168
+ * collapsing a list never reorders what is left of it.
169
+ */
170
+ function preferScript(words, script) {
171
+ /** Whether a candidate is the writing the caller asked for. */
172
+ const isWanted = (word, simplified) => script.prefer === "Hans" ? word === simplified : word !== simplified;
173
+ const chosen = new Map();
174
+ const order = [];
175
+ for (const word of words) {
176
+ const simplified = convertCharacters(script.tables.toSimplified, word);
177
+ const held = chosen.get(simplified);
178
+ if (held === undefined) {
179
+ chosen.set(simplified, word);
180
+ order.push(simplified);
181
+ continue;
182
+ }
183
+ if (!isWanted(held, simplified) && isWanted(word, simplified)) {
184
+ chosen.set(simplified, word);
185
+ }
186
+ }
187
+ /* c8 ignore next -- every entry in `order` was put in `chosen` beside it */
188
+ return order.map((simplified) => chosen.get(simplified) ?? simplified);
189
+ }
190
+ /**
191
+ * The words a pinyin query could be spelling, likeliest first.
192
+ *
193
+ * ```ts
194
+ * candidates(index, "shi"); // 是, 时, 事, 使, 市, …
195
+ * candidates(index, "shi4"); // narrowed by tone
196
+ * candidates(index, "shì"); // the same, written the other way
197
+ * candidates(index, "yinhang"); // 銀行, 银行, 引吭, 引航, 印航
198
+ * ```
199
+ *
200
+ * This is the half of search that has no haystack: `match` filters Chinese text
201
+ * you already hold, and this answers a query with nothing but the dictionary
202
+ * behind it — pinyin-only lookup, a homophone list, or an input method for
203
+ * somebody with no Chinese keyboard.
204
+ *
205
+ * **A query is a whole reading, not a prefix.** `yinhang` finds 银行 and `yinha`
206
+ * finds nothing, because a reading key is a key rather than a stem. What a query
207
+ * may leave out is the parts a keyboard makes awkward: the tones, the ü, the
208
+ * spaces between syllables, and the r of 儿化.
209
+ *
210
+ * Both scripts are dictionary keys, so 银行 and 銀行 both come back unless a
211
+ * {@link ScriptPreference} says which to keep.
212
+ */
213
+ export function candidates(index, query, options = {}) {
214
+ const { key, written } = readQuery(query);
215
+ if (key === "") {
216
+ return [];
217
+ }
218
+ const writings = new Map();
219
+ const found = [];
220
+ for (const position of rankedPositions(index, key)) {
221
+ const word = index.dictionary.wordAt(position);
222
+ // A key whose glyphs are not the canonical ones can never be returned by
223
+ // `Dictionary.lookup`, which folds them before it searches, so offering it
224
+ // would be offering 中峯 beside the 中峰 that is already here. 281 of the
225
+ // full tier's keys are like that, and dropping them at the query rather
226
+ // than the build is what makes it free: filtering the whole key list with
227
+ // `toCanonicalGlyphs` costs 52.9 ms, and filtering one answer costs nothing.
228
+ if (toCanonicalGlyphs(word) !== word) {
229
+ continue;
230
+ }
231
+ if (spells(written, readingOf(index, word), writings)) {
232
+ found.push(word);
233
+ }
234
+ }
235
+ const preferred = options.script === undefined ? found : preferScript(found, options.script);
236
+ return options.limit === undefined
237
+ ? preferred
238
+ : preferred.slice(0, Math.max(0, options.limit));
239
+ }
240
+ /**
241
+ * The other words read exactly as this one is, likeliest first.
242
+ *
243
+ * ```ts
244
+ * homophonesOf(index, "长城"); // 長城, 長程, 长程, 常程 — everything read chángchéng
245
+ * ```
246
+ *
247
+ * A homophone list wants the tone, where a typist's query does not, and it gets
248
+ * one without a second index: the toneless group is narrowed to the words whose
249
+ * stored reading is the same string. Over the eight busiest readings in the full
250
+ * dictionary — 4,659 candidates — that narrowing costs 1.2 ms in total.
251
+ *
252
+ * The word itself is never in its own list. Neither is its other-script writing
253
+ * where a {@link ScriptPreference} is given, since 銀行 is not a homophone of
254
+ * 银行 but the same word spelled for a different reader.
255
+ *
256
+ * Returns nothing for a word the dictionary does not hold, since a word with no
257
+ * reading has no homophones rather than all of them.
258
+ */
259
+ export function homophonesOf(index, word, options = {}) {
260
+ const entry = index.dictionary.lookup(word);
261
+ if (entry === undefined) {
262
+ return [];
263
+ }
264
+ const reading = encodeReading(entry.reading);
265
+ const same = new Set([word, entry.word]);
266
+ if (options.script !== undefined) {
267
+ same.add(convertCharacters(options.script.tables.toSimplified, word));
268
+ }
269
+ const found = [];
270
+ for (const position of rankedPositions(index, readingKey(reading))) {
271
+ const candidate = index.dictionary.wordAt(position);
272
+ if (toCanonicalGlyphs(candidate) !== candidate) {
273
+ continue;
274
+ }
275
+ const pairing = options.script === undefined
276
+ ? candidate
277
+ : convertCharacters(options.script.tables.toSimplified, candidate);
278
+ if (same.has(candidate) || same.has(pairing)) {
279
+ continue;
280
+ }
281
+ if (readingOf(index, candidate) === reading) {
282
+ found.push(candidate);
283
+ }
284
+ }
285
+ const preferred = options.script === undefined ? found : preferScript(found, options.script);
286
+ return options.limit === undefined
287
+ ? preferred
288
+ : preferred.slice(0, Math.max(0, options.limit));
289
+ }
290
+ //# sourceMappingURL=candidates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"candidates.js","sourceRoot":"","sources":["../../src/search/candidates.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ import type { Dictionary } from "../dictionary/dictionary.js";
2
+ /**
3
+ * A stretch of the haystack the query spelled out.
4
+ */
5
+ export interface MatchRange {
6
+ /**
7
+ * Where it starts, in code points from the start of the haystack.
8
+ *
9
+ * Code points rather than UTF-16 units, exactly as `Segment.at` is, so that a
10
+ * character outside the basic plane counts as the one character it is.
11
+ */
12
+ readonly at: number;
13
+ /** How many characters it covers. */
14
+ readonly length: number;
15
+ }
16
+ /**
17
+ * What a query matched, and how well.
18
+ */
19
+ export interface PinyinMatch {
20
+ /**
21
+ * The stretches of the haystack the query spelled, in order.
22
+ *
23
+ * Usually one. There is more than one where the query ran over something
24
+ * with no reading of its own: 北京·大学 is matched by `bjdx` as two ranges
25
+ * with the separator between them left out, so that highlighting them marks
26
+ * what was matched rather than what lay between.
27
+ */
28
+ readonly ranges: readonly MatchRange[];
29
+ /**
30
+ * How good the match is, where higher is better.
31
+ *
32
+ * For ordering the haystacks one query matched — sort descending and the
33
+ * best is first. Comparable within a query and not across queries: what it
34
+ * weighs is described at {@link CONTEXT_READING}, and none of it is a
35
+ * probability.
36
+ */
37
+ readonly score: number;
38
+ }
39
+ /**
40
+ * Find where a pinyin query matches a Chinese text, for filtering and
41
+ * highlighting.
42
+ *
43
+ * ```ts
44
+ * match(dictionary, "北京大学", "bjdx")?.ranges; // [{ at: 0, length: 4 }]
45
+ * ```
46
+ *
47
+ * The query is what somebody types on a Latin keyboard with no Chinese input
48
+ * method in front of them: full syllables joined or spaced, `beijing` and
49
+ * `bei jing`; initials, `bj`; the two mixed, `beij` and `bjing`; tones as
50
+ * digits where they are worth writing, `bei3jing1`; and `v` or `u:` for ü.
51
+ * Returns undefined where none of it matches.
52
+ *
53
+ * **No index is built and none is needed.** The haystack is Chinese, so the
54
+ * query is tested as a path over each character's readings rather than the text
55
+ * being spelled out in advance and searched — which is what makes every reading
56
+ * of a polyphone matchable rather than whichever one a default table picked.
57
+ * 行 is found by `xing` and by `hang`, and 长江 by `cj` as well as by `zj`.
58
+ *
59
+ * The reading the decoder settles on in context is then what ranks them, so the
60
+ * one that reads correctly here sorts above the one that merely could:
61
+ *
62
+ * ```ts
63
+ * const scoreOf = (query: string) => match(dictionary, "银行", query)?.score;
64
+ * scoreOf("yh") > scoreOf("yx"); // true — 银行 is yínháng, not yínxíng
65
+ * ```
66
+ *
67
+ * Ranges rather than a boolean, so that a caller can mark what matched, in code
68
+ * points from the start of the text. Everything this needs is in the `core`
69
+ * dictionary, so a page that never loads a word list can still do it.
70
+ */
71
+ export declare function match(dictionary: Dictionary, haystack: string, query: string): PinyinMatch | undefined;
72
+ //# sourceMappingURL=match.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../../src/search/match.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAe9D;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,CAAC;IACvC;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AA+cD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,KAAK,CACnB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,WAAW,GAAG,SAAS,CAMzB"}