@kensio/pinyinjs 1.6.0 → 1.7.0

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 (85) hide show
  1. package/README.md +211 -5
  2. package/data/core.entries +397 -396
  3. package/data/core.freq +0 -0
  4. package/data/core.keys +1 -0
  5. package/data/full.entries +9717 -9717
  6. package/data/manifest.json +12 -12
  7. package/data/script.map +7 -7
  8. package/data/standard.entries +967 -966
  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/corpus-mass.d.ts +68 -0
  38. package/dist/dictionary/corpus-mass.d.ts.map +1 -0
  39. package/dist/dictionary/corpus-mass.js +123 -0
  40. package/dist/dictionary/corpus-mass.js.map +1 -0
  41. package/dist/dictionary/dictionary.d.ts +56 -0
  42. package/dist/dictionary/dictionary.d.ts.map +1 -1
  43. package/dist/dictionary/dictionary.js +116 -1
  44. package/dist/dictionary/dictionary.js.map +1 -1
  45. package/dist/dictionary/erhua.d.ts +4 -0
  46. package/dist/dictionary/erhua.d.ts.map +1 -1
  47. package/dist/dictionary/erhua.js +6 -0
  48. package/dist/dictionary/erhua.js.map +1 -1
  49. package/dist/dictionary/merge.d.ts.map +1 -1
  50. package/dist/dictionary/merge.js +16 -8
  51. package/dist/dictionary/merge.js.map +1 -1
  52. package/dist/dictionary/tiers.d.ts +2 -1
  53. package/dist/dictionary/tiers.d.ts.map +1 -1
  54. package/dist/dictionary/tiers.js +77 -11
  55. package/dist/dictionary/tiers.js.map +1 -1
  56. package/dist/grade/check.d.ts +156 -0
  57. package/dist/grade/check.d.ts.map +1 -0
  58. package/dist/grade/check.js +356 -0
  59. package/dist/grade/check.js.map +1 -0
  60. package/dist/index.d.ts +11 -1
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +5 -0
  63. package/dist/index.js.map +1 -1
  64. package/dist/search/candidates.d.ts +84 -0
  65. package/dist/search/candidates.d.ts.map +1 -0
  66. package/dist/search/candidates.js +290 -0
  67. package/dist/search/candidates.js.map +1 -0
  68. package/dist/search/match.d.ts +72 -0
  69. package/dist/search/match.d.ts.map +1 -0
  70. package/dist/search/match.js +419 -0
  71. package/dist/search/match.js.map +1 -0
  72. package/dist/search/query.d.ts +49 -0
  73. package/dist/search/query.d.ts.map +1 -0
  74. package/dist/search/query.js +115 -0
  75. package/dist/search/query.js.map +1 -0
  76. package/dist/search/reverse-index.d.ts +141 -0
  77. package/dist/search/reverse-index.d.ts.map +1 -0
  78. package/dist/search/reverse-index.js +356 -0
  79. package/dist/search/reverse-index.js.map +1 -0
  80. package/dist/sources/unihan.d.ts +9 -0
  81. package/dist/sources/unihan.d.ts.map +1 -1
  82. package/dist/sources/unihan.js +12 -1
  83. package/dist/sources/unihan.js.map +1 -1
  84. package/package.json +2 -3
  85. package/CHANGELOG.md +0 -634
@@ -0,0 +1,419 @@
1
+ import { isSameReading } from "../dictionary/entry.js";
2
+ import { isErCharacter, withErhua } from "../dictionary/erhua.js";
3
+ import { sourcesOf } from "../decode/convert.js";
4
+ import { segment } from "../decode/segment.js";
5
+ import { toCharacters } from "../script/characters.js";
6
+ import { toCanonicalGlyphs } from "../script/glyphs.js";
7
+ import { normaliseQuery, readQueryChunks, skipSeparators, } from "./query.js";
8
+ /**
9
+ * What a match scores for reading its characters the way the text reads them.
10
+ *
11
+ * The one thing this has that a reverse index cannot: every reading a character
12
+ * takes is matchable, so 行 is found by both `xing` and `hang` — and the decoder
13
+ * has already settled which of them the text in front of it means. So 银行 ranks
14
+ * `yh` above `yx`, and 长江 ranks `cj` above `zj`, without either being refused.
15
+ *
16
+ * Weighted by the share of the matched characters that agree, rather than by how
17
+ * many, so that a long match and a short one are scored on the same scale.
18
+ */
19
+ const CONTEXT_READING = 4;
20
+ /**
21
+ * What a match scores for starting where a word starts.
22
+ *
23
+ * 大学 in 北京大学 is the second half of a word a reader thinks of as one thing;
24
+ * 大学 in 大学生 starts it. Both are matches and the second is the better one.
25
+ */
26
+ const WORD_START = 2;
27
+ /**
28
+ * What a match at the very start of the haystack scores, decaying with position.
29
+ *
30
+ * A tie-break rather than a judgement, and deliberately smaller than
31
+ * {@link WORD_START}: it settles two matches that are otherwise alike, and never
32
+ * outranks starting a word.
33
+ */
34
+ const EARLINESS = 1;
35
+ /**
36
+ * What one character scores for being written out in full rather than initialled.
37
+ *
38
+ * Only used to choose between the ways one query can be read over one haystack,
39
+ * never in the score a caller sees: `beij` over 北京大学 is 北 spelled out and 京
40
+ * initialled, and no other reading of it is on offer. Where two are — `xian` is
41
+ * both 西安 and 县 — the fuller spelling is the one the typist meant.
42
+ *
43
+ * Per character, which is what settles 这儿 against `zher`: read as 这 `zhèr` it
44
+ * spells two characters out at once, where 这 abbreviated to `zh` and 儿 as `er`
45
+ * spells one of them and initials the other.
46
+ */
47
+ const SPELLED_OUT = 1;
48
+ /**
49
+ * What one step down a character's reading list costs, likeliest reading first.
50
+ *
51
+ * The dictionary's own order, applied before the decoder has been asked
52
+ * anything: it is what separates 行 `xíng` from 行 `hàng` when the query could
53
+ * be either and the surrounding text says nothing.
54
+ */
55
+ const RARE_READING = 2;
56
+ /**
57
+ * What crossing one character with no reading costs.
58
+ *
59
+ * A separator inside a name is worth stepping over — 北京·大学 is one thing
60
+ * with a mark in the middle of it — and a query that steps over half a line of
61
+ * Latin to reach its second half has found something more tenuous.
62
+ */
63
+ const CROSSED = 0.5;
64
+ /**
65
+ * The query having been read to its end, with nothing left to account for.
66
+ */
67
+ const COMPLETE = { steps: [], score: 0 };
68
+ /**
69
+ * The steps of a match, gathered into the stretches they cover.
70
+ */
71
+ function rangesOf(steps) {
72
+ const ranges = [];
73
+ for (const step of steps) {
74
+ if (!step.isRead) {
75
+ continue;
76
+ }
77
+ const last = ranges.at(-1);
78
+ if (last !== undefined && last.at + last.length === step.at) {
79
+ ranges[ranges.length - 1] = { at: last.at, length: last.length + 1 };
80
+ continue;
81
+ }
82
+ ranges.push({ at: step.at, length: 1 });
83
+ }
84
+ return ranges;
85
+ }
86
+ /**
87
+ * Ask the decoder what the haystack says, for the two things ranking needs.
88
+ *
89
+ * Worked out once a match is known to exist rather than before, because this is
90
+ * a decode of the whole text and matching is not: a search box filtering a list
91
+ * asks the cheap question of every item and pays for this on the few that hit.
92
+ */
93
+ function contextOf(dictionary, haystack) {
94
+ const preferred = [];
95
+ const wordStarts = new Set();
96
+ for (const word of segment(dictionary, haystack)) {
97
+ if (word.reading.length === 0) {
98
+ continue;
99
+ }
100
+ wordStarts.add(word.at);
101
+ // `sourcesOf` is the decoder's own answer to which characters a syllable
102
+ // reads, 儿化 included. It names the whole word and then nothing where it
103
+ // cannot say, which is not an alignment and is dropped rather than guessed.
104
+ const sources = sourcesOf(word.text, word.reading).filter((source) => source !== undefined);
105
+ if (sources.length !== word.reading.length) {
106
+ continue;
107
+ }
108
+ let at = word.at;
109
+ for (const [index, source] of sources.entries()) {
110
+ for (const _ of toCharacters(source)) {
111
+ preferred[at] = word.reading[index];
112
+ at++;
113
+ }
114
+ }
115
+ }
116
+ return { preferred, wordStarts };
117
+ }
118
+ /**
119
+ * How much of a match was written the way the text is actually read.
120
+ *
121
+ * Asked of what the query wrote rather than of the reading the search happened
122
+ * to take it by: 西 is stored with a neutral-toned `xi` and a first-tone `xī`,
123
+ * and a query writing `xi` has picked neither of them over the other. What it
124
+ * has done is write something the settled reading can account for, which is the
125
+ * question — and the one `yx` over 银行 answers differently from `yh`.
126
+ */
127
+ function agreementOf(query, steps, preferred) {
128
+ let comparable = 0;
129
+ let agreeing = 0;
130
+ for (const step of steps) {
131
+ const settled = preferred[step.at];
132
+ if (!step.isRead || settled === undefined) {
133
+ continue;
134
+ }
135
+ comparable++;
136
+ const written = readQueryChunks(query, step.from, settled);
137
+ if (written.some((chunk) => chunk.next === step.next)) {
138
+ agreeing++;
139
+ }
140
+ }
141
+ return comparable === 0 ? 0 : agreeing / comparable;
142
+ }
143
+ /**
144
+ * One query being read over one haystack.
145
+ *
146
+ * The search is a walk over two sequences at once — the haystack's characters
147
+ * and the query's letters — where each character may account for some of the
148
+ * query in more than one way, since it may have more than one reading and each
149
+ * reading may be written out or abbreviated. Memoised on the pair of positions,
150
+ * which is what stops the ways of splitting a long query multiplying out.
151
+ */
152
+ class Search {
153
+ #dictionary;
154
+ #haystack;
155
+ #characters;
156
+ #query;
157
+ #readings = [];
158
+ #paths = new Map();
159
+ constructor(dictionary, haystack, query) {
160
+ this.#dictionary = dictionary;
161
+ this.#haystack = haystack;
162
+ this.#characters = toCharacters(haystack);
163
+ this.#query = query;
164
+ }
165
+ /**
166
+ * Every reading a character offers, likeliest first, asked for once.
167
+ *
168
+ * Canonicalised here rather than left to the dictionary, so that a Hong Kong
169
+ * glyph form is matched by the readings of the Taiwan-standard key it folds
170
+ * to rather than by nothing.
171
+ *
172
+ * The 國語 reading goes on the end where it differs, so that 圾 answers to
173
+ * `se` as well as to `ji`. A search box is not the place to insist on one
174
+ * standard: somebody typing what they say is not typing it wrongly, and the
175
+ * decoder's own reading is what ranks the two afterwards.
176
+ */
177
+ #readingsAt(at) {
178
+ const held = this.#readings[at];
179
+ if (held !== undefined) {
180
+ return held;
181
+ }
182
+ /* c8 ignore next -- only ever asked about a character that is there */
183
+ const character = toCanonicalGlyphs(this.#characters[at] ?? "");
184
+ const found = this.#dictionary.readingsOf(character);
185
+ const taiwan = this.#dictionary.lookup(character)?.taiwanReading;
186
+ const readings = taiwan === undefined ||
187
+ found.some((reading) => isSameReading(reading, taiwan))
188
+ ? found
189
+ : [...found, taiwan];
190
+ this.#readings[at] = readings;
191
+ return readings;
192
+ }
193
+ /**
194
+ * Every reading the query could be accounting for at a character.
195
+ *
196
+ * One per reading the character has — and one more for each of them where an
197
+ * 儿 follows, carrying the r suffix that character stands for. 玩儿 is
198
+ * `wánr`, one syllable over two characters, and somebody looking for it types
199
+ * `wanr`: the r is part of how the word is said, so it is part of how the
200
+ * word is searched for.
201
+ *
202
+ * Offered wherever an 儿 follows rather than only where the dictionary
203
+ * attests the 儿化, because the query is what decides which of them was
204
+ * meant. 女儿 is `nǚ'ér` and not `nǚr`, and typing `nvr` still finds it —
205
+ * below `nver`, since the reading the text takes is what ranks them.
206
+ */
207
+ #candidatesAt(at) {
208
+ const readings = this.#readingsAt(at);
209
+ const plain = readings.map((reading, rank) => ({
210
+ reading,
211
+ rank,
212
+ characters: 1,
213
+ }));
214
+ if (!isErCharacter(this.#characters[at + 1] ?? "")) {
215
+ return plain;
216
+ }
217
+ return [
218
+ ...plain,
219
+ ...readings.flatMap((reading, rank) => {
220
+ const last = reading.at(-1);
221
+ /* c8 ignore next 3 -- a stored reading is never empty or already r-ed */
222
+ if (last === undefined || last.erhua === true) {
223
+ return [];
224
+ }
225
+ return [
226
+ {
227
+ reading: [...reading.slice(0, -1), withErhua(last)],
228
+ rank,
229
+ characters: 2,
230
+ },
231
+ ];
232
+ }),
233
+ ];
234
+ }
235
+ /**
236
+ * Every position the query can be read up to by one whole reading.
237
+ *
238
+ * A reading is usually one syllable, and is not always: 瓩 is `qiānwǎ`, one
239
+ * character read as two. The query accounts for them in order, and may run
240
+ * out partway through, which is a query still being typed.
241
+ */
242
+ #chunksFor(from, reading) {
243
+ let reached = [{ next: from, isFull: true }];
244
+ for (const syllable of reading) {
245
+ const found = [];
246
+ for (const chunk of reached) {
247
+ const start = skipSeparators(this.#query, chunk.next);
248
+ if (start === this.#query.length) {
249
+ found.push({ next: start, isFull: chunk.isFull });
250
+ continue;
251
+ }
252
+ for (const one of readQueryChunks(this.#query, start, syllable)) {
253
+ found.push({
254
+ next: one.next,
255
+ isFull: chunk.isFull && one.isFull,
256
+ });
257
+ }
258
+ }
259
+ reached = found;
260
+ }
261
+ return reached;
262
+ }
263
+ /**
264
+ * The best way of reading the rest of the query from a character, or nothing.
265
+ */
266
+ #from(at, offset) {
267
+ const start = skipSeparators(this.#query, offset);
268
+ if (start === this.#query.length) {
269
+ return COMPLETE;
270
+ }
271
+ if (at >= this.#characters.length) {
272
+ return undefined;
273
+ }
274
+ const key = `${String(at)}:${String(start)}`;
275
+ const held = this.#paths.get(key);
276
+ if (held !== undefined || this.#paths.has(key)) {
277
+ return held;
278
+ }
279
+ const found = this.#read(at, start);
280
+ this.#paths.set(key, found);
281
+ return found;
282
+ }
283
+ /**
284
+ * Read one character, every way it can be read, and keep the best.
285
+ */
286
+ #read(at, start) {
287
+ const candidates = this.#candidatesAt(at);
288
+ if (candidates.length === 0) {
289
+ return this.#cross(at, start);
290
+ }
291
+ let best;
292
+ for (const candidate of candidates) {
293
+ for (const chunk of this.#chunksFor(start, candidate.reading)) {
294
+ const rest = this.#from(at + candidate.characters, chunk.next);
295
+ if (rest === undefined) {
296
+ continue;
297
+ }
298
+ const score = rest.score +
299
+ (chunk.isFull ? SPELLED_OUT * candidate.characters : 0) -
300
+ candidate.rank * RARE_READING;
301
+ if (best === undefined || score > best.score) {
302
+ best = {
303
+ steps: [
304
+ ...Array.from({ length: candidate.characters }, (_, covered) => ({
305
+ at: at + covered,
306
+ isRead: true,
307
+ from: start,
308
+ next: chunk.next,
309
+ })),
310
+ ...rest.steps,
311
+ ],
312
+ score,
313
+ };
314
+ }
315
+ }
316
+ }
317
+ return best;
318
+ }
319
+ /**
320
+ * Step over a character with no reading, which a match may span but not end
321
+ * on: the query is checked for having been read to its end first.
322
+ */
323
+ #cross(at, start) {
324
+ const rest = this.#from(at + 1, start);
325
+ return rest === undefined
326
+ ? undefined
327
+ : {
328
+ steps: [
329
+ { at, isRead: false, from: start, next: start },
330
+ ...rest.steps,
331
+ ],
332
+ score: rest.score - CROSSED,
333
+ };
334
+ }
335
+ /**
336
+ * Where the query matches at all, and how it reads at each of those places.
337
+ */
338
+ #everyMatch() {
339
+ const found = [];
340
+ for (const [at] of this.#characters.entries()) {
341
+ if (this.#readingsAt(at).length === 0) {
342
+ continue;
343
+ }
344
+ const path = this.#from(at, 0);
345
+ if (path !== undefined) {
346
+ found.push(path);
347
+ }
348
+ }
349
+ return found;
350
+ }
351
+ /**
352
+ * The best match, or undefined where the query matches nowhere.
353
+ */
354
+ best() {
355
+ const found = this.#everyMatch();
356
+ if (found.length === 0) {
357
+ return undefined;
358
+ }
359
+ const context = contextOf(this.#dictionary, this.#haystack);
360
+ let best;
361
+ for (const path of found) {
362
+ const ranges = rangesOf(path.steps);
363
+ /* c8 ignore next 3 -- a match always starts on a character it read */
364
+ if (ranges[0] === undefined) {
365
+ continue;
366
+ }
367
+ const start = ranges[0].at;
368
+ const score = CONTEXT_READING *
369
+ agreementOf(this.#query, path.steps, context.preferred) +
370
+ (context.wordStarts.has(start) ? WORD_START : 0) +
371
+ EARLINESS / (1 + start);
372
+ // Strictly better, so that two matches worth the same keep the earlier.
373
+ if (best === undefined || score > best.score) {
374
+ best = { ranges, score };
375
+ }
376
+ }
377
+ return best;
378
+ }
379
+ }
380
+ /**
381
+ * Find where a pinyin query matches a Chinese text, for filtering and
382
+ * highlighting.
383
+ *
384
+ * ```ts
385
+ * match(dictionary, "北京大学", "bjdx")?.ranges; // [{ at: 0, length: 4 }]
386
+ * ```
387
+ *
388
+ * The query is what somebody types on a Latin keyboard with no Chinese input
389
+ * method in front of them: full syllables joined or spaced, `beijing` and
390
+ * `bei jing`; initials, `bj`; the two mixed, `beij` and `bjing`; tones as
391
+ * digits where they are worth writing, `bei3jing1`; and `v` or `u:` for ü.
392
+ * Returns undefined where none of it matches.
393
+ *
394
+ * **No index is built and none is needed.** The haystack is Chinese, so the
395
+ * query is tested as a path over each character's readings rather than the text
396
+ * being spelled out in advance and searched — which is what makes every reading
397
+ * of a polyphone matchable rather than whichever one a default table picked.
398
+ * 行 is found by `xing` and by `hang`, and 长江 by `cj` as well as by `zj`.
399
+ *
400
+ * The reading the decoder settles on in context is then what ranks them, so the
401
+ * one that reads correctly here sorts above the one that merely could:
402
+ *
403
+ * ```ts
404
+ * const scoreOf = (query: string) => match(dictionary, "银行", query)?.score;
405
+ * scoreOf("yh") > scoreOf("yx"); // true — 银行 is yínháng, not yínxíng
406
+ * ```
407
+ *
408
+ * Ranges rather than a boolean, so that a caller can mark what matched, in code
409
+ * points from the start of the text. Everything this needs is in the `core`
410
+ * dictionary, so a page that never loads a word list can still do it.
411
+ */
412
+ export function match(dictionary, haystack, query) {
413
+ const normalised = normaliseQuery(query);
414
+ if (normalised === "" || haystack === "") {
415
+ return undefined;
416
+ }
417
+ return new Search(dictionary, haystack, normalised).best();
418
+ }
419
+ //# sourceMappingURL=match.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"match.js","sourceRoot":"","sources":["../../src/search/match.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import { type Syllable } from "../syllable/syllable.js";
2
+ /**
3
+ * Put a typed query into the one form the matcher reads.
4
+ *
5
+ * Case is dropped, every separator becomes a single space, and the `v` and `u:`
6
+ * conventions become ü — all three are ways of writing the same query, and
7
+ * folding them here is what keeps the matcher itself about syllables.
8
+ *
9
+ * **A tone written as a mark is dropped rather than honoured.** Constraining a
10
+ * match by tone needs to know where the syllable the mark sits inside ends, and
11
+ * a query is exactly the text where that is not yet known: `bei` may still
12
+ * become `beijing`. A tone written as a digit says where the syllable ends by
13
+ * being there at all, so that one is honoured — see {@link readQueryChunks}.
14
+ */
15
+ export declare function normaliseQuery(query: string): string;
16
+ /**
17
+ * Step over any separators at a position, which fall between syllables.
18
+ */
19
+ export declare function skipSeparators(query: string, at: number): number;
20
+ /**
21
+ * One way a query can account for one syllable.
22
+ */
23
+ export interface QueryChunk {
24
+ /** Where the query has been read up to, once this chunk is taken. */
25
+ readonly next: number;
26
+ /** Whether the syllable was written out in full, rather than abbreviated. */
27
+ readonly isFull: boolean;
28
+ }
29
+ /**
30
+ * Every way the query at a position can account for one syllable.
31
+ *
32
+ * Three forms, which between them are every way a syllable gets typed:
33
+ *
34
+ * - written out in full, `bei`, optionally with the tone as a digit, `bei3`,
35
+ * which matches only where the reading carries that tone;
36
+ * - abbreviated to its initial, `b`, which is what a query of initials is;
37
+ * - cut short by the query simply ending, `bei` against `běijīng` — anything
38
+ * the syllable starts with, so that a search box still matches while the
39
+ * query is being typed.
40
+ *
41
+ * The third is confined to the end of the query on purpose. A part-syllable in
42
+ * the middle would let `bejing` through as `be` + `jing`, which is a typo
43
+ * rather than an abbreviation, and nothing in front of a search box means it.
44
+ *
45
+ * Returns one chunk per distinct position the query can be read up to, since
46
+ * the forms overlap: `b` is both an initial and the whole of what is left.
47
+ */
48
+ export declare function readQueryChunks(query: string, at: number, syllable: Syllable): readonly QueryChunk[];
49
+ //# sourceMappingURL=query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/search/query.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,yBAAyB,CAAC;AAoBjC;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIpD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAMhE;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAyBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,QAAQ,GACjB,SAAS,UAAU,EAAE,CA2CvB"}
@@ -0,0 +1,115 @@
1
+ import { normaliseUmlaut, writeSyllableSpelling, } from "../syllable/syllable.js";
2
+ import { stripToneMarks } from "../tone/tone-mark.js";
3
+ import { toneFromNotation } from "../tone/tone.js";
4
+ /**
5
+ * What a query may write between syllables.
6
+ *
7
+ * Whitespace is what anybody types; the apostrophe is the 隔音符号 and the
8
+ * hyphen is what a 成语 or a reduplicated word is written with, so both arrive
9
+ * in a query pasted from written pinyin rather than typed.
10
+ */
11
+ const QUERY_SEPARATORS = /[\s'’‘-]+/gu;
12
+ /**
13
+ * The one separator a normalised query is written with.
14
+ */
15
+ const SEPARATOR = " ";
16
+ const TONE_DIGIT = /^[0-5]$/u;
17
+ /**
18
+ * Put a typed query into the one form the matcher reads.
19
+ *
20
+ * Case is dropped, every separator becomes a single space, and the `v` and `u:`
21
+ * conventions become ü — all three are ways of writing the same query, and
22
+ * folding them here is what keeps the matcher itself about syllables.
23
+ *
24
+ * **A tone written as a mark is dropped rather than honoured.** Constraining a
25
+ * match by tone needs to know where the syllable the mark sits inside ends, and
26
+ * a query is exactly the text where that is not yet known: `bei` may still
27
+ * become `beijing`. A tone written as a digit says where the syllable ends by
28
+ * being there at all, so that one is honoured — see {@link readQueryChunks}.
29
+ */
30
+ export function normaliseQuery(query) {
31
+ return normaliseUmlaut(stripToneMarks(query.normalize("NFC").toLowerCase()))
32
+ .replaceAll(QUERY_SEPARATORS, SEPARATOR)
33
+ .trim();
34
+ }
35
+ /**
36
+ * Step over any separators at a position, which fall between syllables.
37
+ */
38
+ export function skipSeparators(query, at) {
39
+ let next = at;
40
+ while (query[next] === SEPARATOR) {
41
+ next++;
42
+ }
43
+ return next;
44
+ }
45
+ /**
46
+ * How a syllable may be abbreviated in a query.
47
+ *
48
+ * The first letter is the abbreviation everybody means by "initials only" —
49
+ * 北京 is `bj` and 西安 is `xa`, which is why this is taken from the spelling
50
+ * rather than from {@link Syllable.initial}: 安 has no initial and is still
51
+ * typed `a`. The initial itself is offered alongside it so that a retroflex
52
+ * keeps its h — 中文 is `zw` to one typist and `zhw` to another.
53
+ */
54
+ function abbreviationsOf(syllable, spelling) {
55
+ const first = spelling.slice(0, 1);
56
+ const { initial } = syllable;
57
+ return [first, initial].filter((abbreviation, at, all) => abbreviation !== "" &&
58
+ abbreviation.length < spelling.length &&
59
+ all.indexOf(abbreviation) === at);
60
+ }
61
+ /**
62
+ * Every way the query at a position can account for one syllable.
63
+ *
64
+ * Three forms, which between them are every way a syllable gets typed:
65
+ *
66
+ * - written out in full, `bei`, optionally with the tone as a digit, `bei3`,
67
+ * which matches only where the reading carries that tone;
68
+ * - abbreviated to its initial, `b`, which is what a query of initials is;
69
+ * - cut short by the query simply ending, `bei` against `běijīng` — anything
70
+ * the syllable starts with, so that a search box still matches while the
71
+ * query is being typed.
72
+ *
73
+ * The third is confined to the end of the query on purpose. A part-syllable in
74
+ * the middle would let `bejing` through as `be` + `jing`, which is a typo
75
+ * rather than an abbreviation, and nothing in front of a search box means it.
76
+ *
77
+ * Returns one chunk per distinct position the query can be read up to, since
78
+ * the forms overlap: `b` is both an initial and the whole of what is left.
79
+ */
80
+ export function readQueryChunks(query, at, syllable) {
81
+ const spelling = writeSyllableSpelling(syllable);
82
+ const found = new Map();
83
+ /**
84
+ * Note that the query can be read up to here, keeping the fuller reading of
85
+ * the two where a position is reached both ways.
86
+ */
87
+ const take = (next, isFull) => {
88
+ found.set(next, (found.get(next) ?? false) || isFull);
89
+ };
90
+ if (query.startsWith(spelling, at)) {
91
+ const after = at + spelling.length;
92
+ const digit = query[after] ?? "";
93
+ // A reading with no tone written cannot contradict one, and a few hundred
94
+ // characters are stored that way: 西 is `xi`, with `xī` listed beside it.
95
+ if (TONE_DIGIT.test(digit) &&
96
+ (syllable.tone === undefined ||
97
+ toneFromNotation(Number(digit)) === syllable.tone)) {
98
+ take(after + 1, true);
99
+ }
100
+ take(after, true);
101
+ }
102
+ for (const abbreviation of abbreviationsOf(syllable, spelling)) {
103
+ if (query.startsWith(abbreviation, at)) {
104
+ take(at + abbreviation.length, false);
105
+ }
106
+ }
107
+ const tail = query.slice(at);
108
+ if (tail !== "" &&
109
+ tail.length < spelling.length &&
110
+ spelling.startsWith(tail)) {
111
+ take(query.length, false);
112
+ }
113
+ return [...found].map(([next, isFull]) => ({ next, isFull }));
114
+ }
115
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/search/query.ts"],"names":[],"mappings":"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"}