@kensio/pinyinjs 1.7.5 → 1.7.7

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 (163) hide show
  1. package/README.md +128 -123
  2. package/data/full.entries +2637 -2637
  3. package/data/manifest.json +9 -9
  4. package/data/script.map +168 -3
  5. package/data/standard.entries +8 -8
  6. package/dist/decode/chang-complement.d.ts +19 -0
  7. package/dist/decode/chang-complement.d.ts.map +1 -0
  8. package/dist/decode/chang-complement.js +113 -0
  9. package/dist/decode/chang-complement.js.map +1 -0
  10. package/dist/decode/chang-context.d.ts +29 -0
  11. package/dist/decode/chang-context.d.ts.map +1 -0
  12. package/dist/decode/chang-context.js +99 -0
  13. package/dist/decode/chang-context.js.map +1 -0
  14. package/dist/decode/chang-rule.d.ts +47 -0
  15. package/dist/decode/chang-rule.d.ts.map +1 -0
  16. package/dist/decode/chang-rule.js +188 -0
  17. package/dist/decode/chang-rule.js.map +1 -0
  18. package/dist/decode/confidence.d.ts +11 -3
  19. package/dist/decode/confidence.d.ts.map +1 -1
  20. package/dist/decode/confidence.js +11 -3
  21. package/dist/decode/confidence.js.map +1 -1
  22. package/dist/decode/convert-pipeline.d.ts.map +1 -1
  23. package/dist/decode/convert-pipeline.js +5 -4
  24. package/dist/decode/convert-pipeline.js.map +1 -1
  25. package/dist/decode/de-rule.d.ts +74 -0
  26. package/dist/decode/de-rule.d.ts.map +1 -0
  27. package/dist/decode/de-rule.js +199 -0
  28. package/dist/decode/de-rule.js.map +1 -0
  29. package/dist/decode/decode.d.ts +12 -25
  30. package/dist/decode/decode.d.ts.map +1 -1
  31. package/dist/decode/decode.js +10 -47
  32. package/dist/decode/decode.js.map +1 -1
  33. package/dist/decode/decoders.d.ts +9 -4
  34. package/dist/decode/decoders.d.ts.map +1 -1
  35. package/dist/decode/decoders.js +7 -7
  36. package/dist/decode/decoders.js.map +1 -1
  37. package/dist/decode/edge-context.d.ts +19 -0
  38. package/dist/decode/edge-context.d.ts.map +1 -1
  39. package/dist/decode/edge-context.js +41 -0
  40. package/dist/decode/edge-context.js.map +1 -1
  41. package/dist/decode/hint-lattice.d.ts.map +1 -1
  42. package/dist/decode/hint-lattice.js +17 -3
  43. package/dist/decode/hint-lattice.js.map +1 -1
  44. package/dist/decode/jiao-rule.d.ts +9 -3
  45. package/dist/decode/jiao-rule.d.ts.map +1 -1
  46. package/dist/decode/jiao-rule.js +44 -5
  47. package/dist/decode/jiao-rule.js.map +1 -1
  48. package/dist/decode/lattice-decode.d.ts +28 -0
  49. package/dist/decode/lattice-decode.d.ts.map +1 -0
  50. package/dist/decode/lattice-decode.js +48 -0
  51. package/dist/decode/lattice-decode.js.map +1 -0
  52. package/dist/decode/lattice-slice.d.ts +22 -6
  53. package/dist/decode/lattice-slice.d.ts.map +1 -1
  54. package/dist/decode/lattice-slice.js +24 -18
  55. package/dist/decode/lattice-slice.js.map +1 -1
  56. package/dist/decode/lattice-types.d.ts +27 -8
  57. package/dist/decode/lattice-types.d.ts.map +1 -1
  58. package/dist/decode/lattice-types.js +27 -8
  59. package/dist/decode/lattice-types.js.map +1 -1
  60. package/dist/decode/numbers.d.ts +0 -8
  61. package/dist/decode/numbers.d.ts.map +1 -1
  62. package/dist/decode/numbers.js +0 -10
  63. package/dist/decode/numbers.js.map +1 -1
  64. package/dist/decode/polyphone-rules.d.ts +7 -27
  65. package/dist/decode/polyphone-rules.d.ts.map +1 -1
  66. package/dist/decode/polyphone-rules.js +7 -155
  67. package/dist/decode/polyphone-rules.js.map +1 -1
  68. package/dist/decode/reading-rules.d.ts +1 -29
  69. package/dist/decode/reading-rules.d.ts.map +1 -1
  70. package/dist/decode/reading-rules.js +5 -69
  71. package/dist/decode/reading-rules.js.map +1 -1
  72. package/dist/decode/rules.d.ts +1 -1
  73. package/dist/decode/rules.d.ts.map +1 -1
  74. package/dist/decode/rules.js +1 -1
  75. package/dist/decode/rules.js.map +1 -1
  76. package/dist/decode/run-context.d.ts +33 -0
  77. package/dist/decode/run-context.d.ts.map +1 -0
  78. package/dist/decode/run-context.js +47 -0
  79. package/dist/decode/run-context.js.map +1 -0
  80. package/dist/decode/run-lattice.d.ts +13 -10
  81. package/dist/decode/run-lattice.d.ts.map +1 -1
  82. package/dist/decode/run-lattice.js +15 -12
  83. package/dist/decode/run-lattice.js.map +1 -1
  84. package/dist/decode/run-words.d.ts +17 -0
  85. package/dist/decode/run-words.d.ts.map +1 -0
  86. package/dist/decode/run-words.js +29 -0
  87. package/dist/decode/run-words.js.map +1 -0
  88. package/dist/decode/sandhi-input.d.ts +30 -0
  89. package/dist/decode/sandhi-input.d.ts.map +1 -0
  90. package/dist/decode/sandhi-input.js +35 -0
  91. package/dist/decode/sandhi-input.js.map +1 -0
  92. package/dist/decode/sandhi-tones.d.ts +33 -5
  93. package/dist/decode/sandhi-tones.d.ts.map +1 -1
  94. package/dist/decode/sandhi-tones.js +59 -8
  95. package/dist/decode/sandhi-tones.js.map +1 -1
  96. package/dist/decode/sandhi.d.ts +9 -6
  97. package/dist/decode/sandhi.d.ts.map +1 -1
  98. package/dist/decode/sandhi.js +10 -7
  99. package/dist/decode/sandhi.js.map +1 -1
  100. package/dist/decode/script-region.d.ts +15 -0
  101. package/dist/decode/script-region.d.ts.map +1 -0
  102. package/dist/decode/script-region.js +61 -0
  103. package/dist/decode/script-region.js.map +1 -0
  104. package/dist/decode/script-types.d.ts +11 -2
  105. package/dist/decode/script-types.d.ts.map +1 -1
  106. package/dist/decode/script-types.js +2 -1
  107. package/dist/decode/script-types.js.map +1 -1
  108. package/dist/decode/script-words.d.ts +7 -5
  109. package/dist/decode/script-words.d.ts.map +1 -1
  110. package/dist/decode/script-words.js +18 -25
  111. package/dist/decode/script-words.js.map +1 -1
  112. package/dist/decode/script.d.ts.map +1 -1
  113. package/dist/decode/script.js +2 -1
  114. package/dist/decode/script.js.map +1 -1
  115. package/dist/decode/write-run.d.ts +0 -7
  116. package/dist/decode/write-run.d.ts.map +1 -1
  117. package/dist/decode/write-run.js +4 -39
  118. package/dist/decode/write-run.js.map +1 -1
  119. package/dist/dictionary/cedict-senses.d.ts +7 -0
  120. package/dist/dictionary/cedict-senses.d.ts.map +1 -1
  121. package/dist/dictionary/cedict-senses.js +12 -0
  122. package/dist/dictionary/cedict-senses.js.map +1 -1
  123. package/dist/dictionary/constituent-repair.d.ts +36 -0
  124. package/dist/dictionary/constituent-repair.d.ts.map +1 -0
  125. package/dist/dictionary/constituent-repair.js +117 -0
  126. package/dist/dictionary/constituent-repair.js.map +1 -0
  127. package/dist/dictionary/measure-tone.d.ts +36 -0
  128. package/dist/dictionary/measure-tone.d.ts.map +1 -0
  129. package/dist/dictionary/measure-tone.js +139 -0
  130. package/dist/dictionary/measure-tone.js.map +1 -0
  131. package/dist/dictionary/merge-types.d.ts +13 -2
  132. package/dist/dictionary/merge-types.d.ts.map +1 -1
  133. package/dist/dictionary/merge-types.js.map +1 -1
  134. package/dist/dictionary/merge-walk.d.ts +27 -0
  135. package/dist/dictionary/merge-walk.d.ts.map +1 -0
  136. package/dist/dictionary/merge-walk.js +49 -0
  137. package/dist/dictionary/merge-walk.js.map +1 -0
  138. package/dist/dictionary/merge.d.ts +3 -3
  139. package/dist/dictionary/merge.d.ts.map +1 -1
  140. package/dist/dictionary/merge.js +24 -28
  141. package/dist/dictionary/merge.js.map +1 -1
  142. package/dist/dictionary/overrides.d.ts +5 -0
  143. package/dist/dictionary/overrides.d.ts.map +1 -1
  144. package/dist/dictionary/overrides.js +5 -5
  145. package/dist/dictionary/overrides.js.map +1 -1
  146. package/dist/dictionary/script-tables.js +91 -13
  147. package/dist/dictionary/script-tables.js.map +1 -1
  148. package/dist/dictionary/word-readings.d.ts.map +1 -1
  149. package/dist/dictionary/word-readings.js +6 -0
  150. package/dist/dictionary/word-readings.js.map +1 -1
  151. package/dist/index.d.ts +1 -1
  152. package/dist/index.d.ts.map +1 -1
  153. package/dist/index.js +1 -1
  154. package/dist/index.js.map +1 -1
  155. package/dist/script/glyphs.d.ts +9 -8
  156. package/dist/script/glyphs.d.ts.map +1 -1
  157. package/dist/script/glyphs.js +21 -48
  158. package/dist/script/glyphs.js.map +1 -1
  159. package/dist/script/reading-splits.d.ts +41 -0
  160. package/dist/script/reading-splits.d.ts.map +1 -0
  161. package/dist/script/reading-splits.js +98 -0
  162. package/dist/script/reading-splits.js.map +1 -0
  163. package/package.json +2 -2
package/README.md CHANGED
@@ -14,13 +14,13 @@ pnpm add @kensio/pinyinjs
14
14
  Node 22+, or any browser. The core imports no Node built-ins, and the package is
15
15
  ESM only.
16
16
 
17
- The dictionaries ship inside the package, which is what makes it a 4 MB
18
- download: `data/` is 10 MB of artifacts and the point of the whole thing.
17
+ The dictionaries ship inside the package, and that is what makes it a 4 MB
18
+ download. `data/` is 10 MB of artifacts and the point of the whole thing.
19
19
 
20
20
  ## Command line
21
21
 
22
- Installing the package installs a `pinyinjs` command, which is the quickest way
23
- to try any of this.
22
+ Installing the package installs a `pinyinjs` command, the quickest way to try
23
+ any of this.
24
24
 
25
25
  ```console
26
26
  $ pinyinjs convert 我要去北京。
@@ -43,7 +43,7 @@ $ pinyinjs check 银行 yínxíng
43
43
  $ pinyinjs explain 银行
44
44
  银行 yínháng
45
45
  yín locked
46
- háng word xíng +24.6 héng +26.6 hàng +27.6
46
+ háng word xíng +14.6 héng +16.6 hàng +17.6
47
47
 
48
48
  $ pinyinjs lookup 头发
49
49
  头发 tóu fa n
@@ -99,8 +99,7 @@ never coloured. See [the command line](docs/cli/#colour).
99
99
 
100
100
  ### Calling it from something else
101
101
 
102
- `convert` writes the pinyin and nothing else, so it drops straight into a
103
- pipeline:
102
+ `convert` writes the pinyin and no more, ready for a pipeline:
104
103
 
105
104
  ```console
106
105
  $ pinyinjs convert 银行
@@ -108,12 +107,12 @@ yínháng
108
107
  ```
109
108
 
110
109
  Everything else has columns for a person to read. Add `--json` to any command
111
- and it writes one JSON document per answer instead, which is what `jq` wants:
110
+ and it writes one JSON document per answer instead, the shape `jq` wants:
112
111
 
113
112
  ```console
114
113
  $ pinyinjs explain 长江大桥 --json | jq -c '.syllables[] | select(.state != "locked")'
115
- {"text":"Cháng","state":"word","tone":2,"alternatives":[{"reading":"zhǎng","cost":24.62}]}
116
- {"text":"Dà","state":"word","tone":4,"alternatives":[{"reading":"dài","cost":22.62}]}
114
+ {"text":"Cháng","state":"word","tone":2,"alternatives":[{"reading":"zhǎng","cost":14.62}]}
115
+ {"text":"Dà","state":"word","tone":4,"alternatives":[{"reading":"dài","cost":12.62}]}
117
116
 
118
117
  $ pinyinjs lookup 垃圾 --json | jq -r .taiwanReading
119
118
  lè sè
@@ -121,13 +120,13 @@ lè sè
121
120
  $ cat article.txt | pinyinjs convert --json | jq -r .pinyin
122
121
  ```
123
122
 
124
- One document per answer rather than one array for the whole run, so the shape
125
- is the same whether you convert one word or pipe a file through.
123
+ One document per answer, and never one array for the whole run. The shape is the
124
+ same whether you convert one word or pipe a file through.
126
125
 
127
126
  ## Load a dictionary
128
127
 
129
- Converting needs a dictionary, and it is a fetchable file rather than a
130
- JavaScript module, so loading it is asynchronous.
128
+ Converting needs a dictionary. A dictionary is a fetchable file, so loading one
129
+ is asynchronous.
131
130
 
132
131
  ```ts
133
132
  import { convert, loadDictionary } from "@kensio/pinyinjs";
@@ -149,9 +148,9 @@ convert(dictionary, "长城"); // "Chángchéng"
149
148
  ```
150
149
 
151
150
  Serve the artifacts uncompressed and let HTTP `Content-Encoding: br` compress
152
- them: `DecompressionStream` has no brotli.
151
+ them. `DecompressionStream` has no brotli.
153
152
 
154
- Load the dictionary once and keep it; it is immutable and safe to share.
153
+ Load the dictionary once and keep it. It is immutable and safe to share.
155
154
 
156
155
  ### Tiers
157
156
 
@@ -161,8 +160,8 @@ Load the dictionary once and keep it; it is immutable and safe to share.
161
160
  | `standard` | 66,970 | 377 KB | the most common words |
162
161
  | `full` | 461,555 | 2,378 KB | every word |
163
162
 
164
- `full` is the default. The tiers are nested, so a page can load `standard`
165
- first, convert with it, and reload as `full` arrives.
163
+ `full` is the default. The tiers are nested. A page can load `standard` first,
164
+ convert with it, and reload as `full` arrives.
166
165
 
167
166
  ## Convert
168
167
 
@@ -174,7 +173,7 @@ convert(dictionary, "3D银行"); // "sān D yínháng", the digit is read, the l
174
173
  ```
175
174
 
176
175
  A reading the dictionary cannot settle on its own is settled by context, with
177
- typed rules over the lattice rather than tweaks to the output:
176
+ typed rules over the lattice and never a tweak to the output:
178
177
 
179
178
  ```ts
180
179
  convert(dictionary, "我得走了"); // "wǒ děi zǒule", modal 得
@@ -216,9 +215,10 @@ convert(dictionary, "好好", { sandhi: { thirdTone: true } }); // "háohǎo"
216
215
 
217
216
  ### Spacing, capitals and punctuation
218
217
 
219
- **Capitals.** Proper nouns always; the first word of a sentence only when the
220
- source is punctuated as one, since that is the only thing separating 学生 looked
221
- up as a word from 这是我的书。written as a sentence. A comma does not count.
218
+ **Capitals.** Proper nouns always, and the first word of a sentence only when
219
+ the source is punctuated as one, since that is the only thing separating 学生
220
+ looked up as a word from 这是我的书。written as a sentence. A comma leaves the
221
+ next word lower case.
222
222
 
223
223
  ```ts
224
224
  convert(dictionary, "银行"); // "yínháng", not "Yínháng"
@@ -226,9 +226,9 @@ convert(dictionary, "我要去北京。"); // "Wǒ yào qù Běijīng."
226
226
  convert(dictionary, "你好,世界"); // "nǐ hǎo, shìjiè"
227
227
  ```
228
228
 
229
- **Apostrophes.** The 隔音符号 goes before any syllable of a word that starts with
230
- `a`, `o` or `e` and is not the first. `apostrophe: "standard"` writes it only
231
- where leaving it out would read as something else.
229
+ **Apostrophes.** The 隔音符号 goes before any syllable of a word that starts
230
+ with `a`, `o` or `e`, unless it is the first. `apostrophe: "standard"` writes it
231
+ only where leaving it out would read as something else.
232
232
 
233
233
  ```ts
234
234
  convert(dictionary, "天安门"); // "Tiān'ānmén"
@@ -239,7 +239,7 @@ convert(dictionary, "女儿"); // "nǚ'ér"
239
239
  the space the full-width glyph carried. Brackets and quotation marks are left
240
240
  alone. `punctuation: "keep"` leaves everything as it was.
241
241
 
242
- **Word spacing.** 分词连写 is applied to the decoded words: aspect particles
242
+ **Word spacing.** 分词连写 is applied to the decoded words. Aspect particles
243
243
  attach to their verb, suffixes to their stem, and the generic half of a place
244
244
  name separates and capitalises. A small curated list covers words the standard
245
245
  writes in a way no rule reaches.
@@ -256,12 +256,11 @@ convert(dictionary, "黄河"); // "Huáng Hé"
256
256
  convert(dictionary, "中国人"); // "Zhōngguórén"
257
257
  ```
258
258
 
259
- The list is not a complete 正词法 implementation, so some words it does not
260
- cover are written differently: 不但 is `búdàn`, 大米 is `dàmǐ`, 青海 is
261
- `Qīnghǎi`.
259
+ The list falls short of a complete 正词法 implementation, so a word it misses is
260
+ written differently. 不但 is `búdàn`, 大米 is `dàmǐ`, and 青海 is `Qīnghǎi`.
262
261
 
263
- **Reduplication** takes a hyphen rather than a space, since it is one word with
264
- a boundary inside it.
262
+ **Reduplication** takes a hyphen where a space would go, since it is one word
263
+ with a boundary inside it.
265
264
 
266
265
  ```ts
267
266
  convert(dictionary, "干干净净"); // "gāngān-jìngjìng"
@@ -270,7 +269,7 @@ convert(dictionary, "爸爸妈妈"); // "bàba māma", that shape, but two words
270
269
  ```
271
270
 
272
271
  **A 成语 that can be read as two disyllables** takes the same hyphen, from a
273
- curated list of 117; the rest are written solid, as the standard writes them.
272
+ curated list of 117. The rest are written solid, as the standard writes them.
274
273
 
275
274
  ```ts
276
275
  convert(dictionary, "风平浪静"); // "fēngpíng-làngjìng"
@@ -308,7 +307,7 @@ pieces[1]?.confidence?.alternatives.map((found) =>
308
307
  ); // ["xíng", "héng", "hàng"]
309
308
  ```
310
309
 
311
- A piece with no `syllable` is the text between two of them: a space, or a run
310
+ A piece with no `syllable` is the text between two of them, a space or a run
312
311
  that was never Han. `joinPieces(pieces)` gives back exactly what `convert`
313
312
  returns.
314
313
 
@@ -346,10 +345,10 @@ convertToHtml(dictionary, "行");
346
345
 
347
346
  One element per syllable, with `py-tone-1` to `py-tone-5` (5 is the neutral
348
347
  tone), and `py-uncertain` plus the rejected readings where the decoder was
349
- guessing. Each one declares itself pinyin — `zh-Latn-TW-pinyin` for a `zh-TW`
350
- conversion — so that a screen reader does not read it as the language of the
351
- page around it. Text that is not Han is escaped, not marked up. No styles are
352
- included, so write your own:
348
+ guessing. Each one declares itself pinyin, as `zh-Latn-TW-pinyin` for a `zh-TW`
349
+ conversion. A screen reader then reads it as pinyin, and not as the language of
350
+ the page around it. Text that was never Han is escaped and left unmarked. No
351
+ styles are included, so write your own:
353
352
 
354
353
  ```css
355
354
  .py-tone-1 {
@@ -365,8 +364,8 @@ and `lang: false`. `toHtml(pieces, options)` renders pieces you already have.
365
364
 
366
365
  ## Annotated output: hanzi and pinyin together
367
366
 
368
- `convertToHtml` writes the pinyin _instead of_ the hanzi. To keep both — a
369
- learner's text, a subtitle, a dictionary entry — annotate it instead:
367
+ `convertToHtml` writes the pinyin _instead of_ the hanzi. To keep both, for a
368
+ learner's text, a subtitle or a dictionary entry, annotate it instead:
370
369
 
371
370
  ```ts
372
371
  import { convertToAnnotatedHtml } from "@kensio/pinyinjs";
@@ -376,14 +375,15 @@ convertToAnnotatedHtml(dictionary, "银行");
376
375
  // lang="zh-Latn-CN-pinyin">yín</span></rt><rp>)</rp></ruby>…
377
376
  ```
378
377
 
379
- The markup is `<ruby>`, which browsers lay out natively — the reading sits
380
- above the characters, and `<rp>` puts it in parentheses where it does not. Each
381
- `<rt>` holds exactly what `convertToHtml` would have written, so tone colours
382
- and uncertainty marking work inside an annotation as they do outside one.
378
+ The markup is `<ruby>`, which browsers lay out natively. The reading sits above
379
+ the characters, and `<rp>` puts it in parentheses where a browser lacks ruby
380
+ support. Each `<rt>` holds exactly what `convertToHtml` would have written, so
381
+ tone colours and uncertainty marking work inside an annotation as they do
382
+ outside one.
383
383
 
384
- **A base is not always one character**, and this is where per-character
385
- annotation goes wrong. 玩儿 is two characters and the one syllable `wánr`, and a
386
- read number reverses on the way, so both are annotated whole:
384
+ **A base can span characters**, and this is where per-character annotation goes
385
+ wrong. 玩儿 is two characters and the one syllable `wánr`, and a read number
386
+ reverses on the way, so both are annotated whole:
387
387
 
388
388
  ```ts
389
389
  convertToAnnotatedHtml(dictionary, "玩儿"); // one <ruby>, 玩儿 over wánr
@@ -399,13 +399,13 @@ ruby rt {
399
399
  ```
400
400
 
401
401
  `toAnnotatedHtml(pieces, options)` renders pieces you already have, and
402
- `ConvertedPiece.source` is what makes that possible: the characters each piece
403
- reads, or undefined where it reads on into the ones before it.
402
+ `ConvertedPiece.source` is what makes that possible, holding the characters each
403
+ piece reads, or undefined where it reads on into the ones before it.
404
404
 
405
405
  ## Segment
406
406
 
407
407
  Converting has to find the words before it can read them, because the unit a
408
- reading belongs to is the word — 行 is `xíng` or `háng` and only 银行 and 行长
408
+ reading belongs to is the word. 行 is `xíng` or `háng` and only 银行 and 行长
409
409
  say which. `segment` returns that answer instead of throwing it away.
410
410
 
411
411
  ```ts
@@ -421,14 +421,14 @@ found[3]?.isProperNoun; // true
421
421
  found[3]?.at; // 3, in code points from the start of the text
422
422
  ```
423
423
 
424
- Every stretch comes back in order, including the ones that were never Han, so
425
- the segments rejoin into exactly the text they came from — which is what makes
426
- it safe to rebuild a document, highlight in place, or index a corpus. Filter on
424
+ Every stretch comes back in order, including the ones that were never Han. The
425
+ segments rejoin into exactly the text they came from. That is what makes it safe
426
+ to rebuild a document, highlight in place, or index a corpus. Filter on
427
427
  `isKnown` for the stretches the dictionary recognised.
428
428
 
429
- What it does not apply is 分词连写, the word spacing written pinyin wants: 他看了
430
- segments as 他 / 看 / 了 and converts as `tā kànle`, because attaching an aspect
431
- particle to its verb is a fact about writing pinyin rather than about where the
429
+ One thing it leaves alone is 分词连写, the word spacing written pinyin wants.
430
+ 他看了 segments as 他 / 看 / 了 and converts as `tā kànle`, because attaching an
431
+ aspect particle to its verb is a fact about writing pinyin, not about where the
432
432
  words are.
433
433
 
434
434
  ## Match a pinyin query
@@ -444,28 +444,32 @@ match(dictionary, "北京大学", "beijing")?.ranges; // [{ at: 0, length: 2 }]
444
444
  match(dictionary, "北京大学", "nanjing"); // undefined
445
445
  ```
446
446
 
447
- Every way anybody types it: full syllables joined or spaced, `beijing` and
448
- `bei jing`; initials, `bj`; the two mixed, `beij` and `bjing`; tones as digits
449
- where they are worth writing, `bei3jing1`; `v` or `u:` for ü; and the r of 儿化
450
- on the syllable it belongs to, so 玩儿 answers to `wanr`.
447
+ Every way anybody types it is accepted:
451
448
 
452
- **No index is built and none is needed.** The haystack is Chinese, so the query
453
- is tested as a path over each character's readings rather than the text being
454
- spelled out in advance — which is why every reading of a polyphone is
455
- matchable, where a default reading table only ever offers one of them:
449
+ - full syllables joined or spaced, `beijing` and `bei jing`
450
+ - initials, `bj`
451
+ - the two mixed, `beij` and `bjing`
452
+ - tones as digits where they are worth writing, `bei3jing1`
453
+ - `v` or `u:` for ü
454
+ - the r of 儿化 on the syllable it belongs to, so 玩儿 answers to `wanr`
455
+
456
+ **No index is built and none is needed.** The haystack is Chinese. The query is
457
+ tested as a path over each character's readings, with no text spelled out in
458
+ advance. That is why every reading of a polyphone is matchable, where a default
459
+ reading table only ever offers one of them:
456
460
 
457
461
  ```ts
458
462
  match(dictionary, "银行", "yh")?.score; // 7, and 银行 is yínháng
459
463
  match(dictionary, "银行", "yx")?.score; // 5 — a reading 行 has, but not here
460
464
  ```
461
465
 
462
- Both match, and the decoder's own reading is what ranks them, along with
463
- whether the match starts a word. Sort a filtered list by `score`, highest
464
- first. Ranges rather than a boolean, in code points, so a caller can highlight
465
- what matched — and there is more than one where the query stepped over
466
- something with no reading of its own, as 北京·大学 does.
466
+ Both match, and the decoder's own reading is what ranks them, along with whether
467
+ the match starts a word. Sort a filtered list by `score`, highest first. What
468
+ comes back is ranges in code points, so a caller can highlight what matched.
469
+ There is more than one where the query stepped over something with no reading of
470
+ its own, as 北京·大学 does.
467
471
 
468
- The `core` tier is enough, so a page that never loads a word list can still
472
+ The `core` tier is enough, and a page that never loads a word list can still
469
473
  filter. See [matching](docs/matching/).
470
474
 
471
475
  ## Go the other way: pinyin to hanzi
@@ -485,23 +489,23 @@ candidates(index, "yínháng"); // ["銀行", "银行"], narrowed by tone
485
489
  homophonesOf(index, "长城"); // ["長城", "長程", "长程", "常程"]
486
490
  ```
487
491
 
488
- This is the half of search with no haystack: `match` filters Chinese text you
489
- already hold, and this answers a query with nothing behind it but the
490
- dictionary. Pinyin-only lookup, a homophones section on a word page, or a
491
- browser input method for somebody with no Chinese keyboard.
492
+ This is the half of search with no haystack. `match` filters Chinese text you
493
+ already hold, and this answers a query with only the dictionary behind it.
494
+ Pinyin-only lookup, a homophones section on a word page, or a browser input
495
+ method for somebody with no Chinese keyboard.
492
496
 
493
- **Nothing is downloaded for it.** Shipping a reverse index of the `full` tier
497
+ **Everything is derived in memory.** Shipping a reverse index of the `full` tier
494
498
  would add 1,995 KB to a 2,378 KB download and could not be compressed below
495
- about 1,474 KB, so the client derives it instead — 510 ms and 2.03 MB of heap on
499
+ about 1,474 KB. The client derives it instead, at 510 ms and 2.03 MB of heap on
496
500
  `full`, and 4 ms on `core`. `ReverseIndex.building` drives that build a slice at
497
- a time, and `serialise` hands it out of a worker, so nothing has to hold the
498
- main thread for it.
501
+ a time, and `serialise` hands it out of a worker. The main thread never has to
502
+ hold it.
499
503
 
500
504
  Candidates come back likeliest first, because a posting is a dictionary position
501
505
  and a position indexes the frequency table. Tones may be written or left off,
502
506
  `v` and `u:` reach ü, and the r of 儿化 is optional, so 玩儿 answers to `wanr`
503
507
  and to `wan`. Both scripts are dictionary keys, so pass a script preference and
504
- the conversion tables to keep one writing of a word rather than both. See
508
+ the conversion tables to keep one writing of a word instead of both. See
505
509
  [candidates](docs/candidates/).
506
510
 
507
511
  ## Check what somebody typed
@@ -519,7 +523,7 @@ marked.score; // 0.5
519
523
  ```
520
524
 
521
525
  Each syllable comes back as one of six verdicts, with `source` and `at` naming
522
- the characters it reads so a mistake can be shown against the text rather than
526
+ the characters it reads so a mistake can be shown against the text and not
523
527
  against the answer:
524
528
 
525
529
  | Verdict | Means |
@@ -543,17 +547,17 @@ check(dictionary, "海鸥", "hǎiōu").isCorrect; // true, an apostrophe is not
543
547
  check(dictionary, "我的书", "wǒ de shū").isCorrect; // true, pinyin marks no neutral tone
544
548
  ```
545
549
 
546
- A guess is only forgiven where the library was guessing: 行 alone is chosen by a
547
- prior and nothing more, whereas 银行 settles both its syllables, so `yínxíng` is
548
- a real mistake. See [confidence](docs/confidence/).
550
+ A guess is only forgiven where the library was guessing. 行 alone is chosen by a
551
+ prior alone, whereas 银行 settles both its syllables, so `yínxíng` is a real
552
+ mistake. See [confidence](docs/confidence/).
549
553
 
550
554
  ### Tones and word spacing
551
555
 
552
556
  Two axes an exercise may or may not be teaching yet, each reported always and
553
557
  each counted only when asked for.
554
558
 
555
- Tones left off are `toneless` rather than `tone`, because `Syllable.tone` knows
556
- the difference:
559
+ Tones left off report as `toneless` and never as `tone`, because `Syllable.tone`
560
+ knows the difference:
557
561
 
558
562
  ```ts
559
563
  check(dictionary, "北京", "bei jing").isCorrect; // true
@@ -561,7 +565,7 @@ check(dictionary, "北京", "bei jing", { tones: "required" }).isCorrect; // fal
561
565
  ```
562
566
 
563
567
  Word spacing is graded on `spacing`, separately from the syllable's own verdict,
564
- because it is a separate mistake: `yín háng` reads 银行 perfectly and writes it
568
+ because it is a separate mistake. `yín háng` reads 银行 perfectly and writes it
565
569
  as two words.
566
570
 
567
571
  ```ts
@@ -572,7 +576,7 @@ check(dictionary, "银行", "yín háng", { spacing: "required" }).isCorrect; //
572
576
  ```
573
577
 
574
578
  It is `split` for a word written as two and `joined` for two written as one, and
575
- it is tolerant in the same spirit as everything else — 分词连写 and the words the
579
+ it is tolerant in the same spirit as everything else. 分词连写 and the words the
576
580
  dictionary knows are two conventions this package writes, and a learner may have
577
581
  been taught either:
578
582
 
@@ -602,8 +606,8 @@ dictionary.hasPrefix("银"); // true, does any word start with this?
602
606
  dictionary.readingsOf("行"); // xíng, háng, héng, hàng, likeliest first
603
607
  ```
604
608
 
605
- Both scripts are keys in the same dictionary, so nothing is converted before a
606
- lookup.
609
+ Both scripts are keys in the same dictionary. A 繁體 word is found directly,
610
+ with no conversion first.
607
611
 
608
612
  ## Syllables
609
613
 
@@ -621,8 +625,8 @@ readSyllable("běi3"); // undefined, one notation at a time
621
625
  isSyllable("wánr"); // true, 儿化 is a suffix, not a syllable of its own
622
626
  ```
623
627
 
624
- Initials and finals are the _underlying_ forms rather than the spelling, so 就 is
625
- `j` + `iou` and 军 is `j` + `ün`. Spelling is reconstructed on demand:
628
+ Initials and finals hold the _underlying_ form, the one behind the spelling, so
629
+ 就 is `j` + `iou` and 军 is `j` + `ün`. Spelling is reconstructed on demand:
626
630
 
627
631
  ```ts
628
632
  const jiu = { initial: "j", final: "iou", tone: 4 } as const;
@@ -635,8 +639,8 @@ writeSyllable(jiu, "none"); // "jiu"
635
639
  Input takes either notation, the `v` and `u:` conventions for ü, and raised tone
636
640
  digits. Output is standard diacritics unless asked otherwise.
637
641
 
638
- Parsing answers whether a spelling is _well formed_, not whether Mandarin uses
639
- it: `shong` parses and is not a real syllable. The attested inventory is
642
+ Parsing answers whether a spelling is _well formed_, and never whether Mandarin
643
+ uses it. `shong` parses and no Mandarin word uses it. The attested inventory is
640
644
  separate:
641
645
 
642
646
  ```ts
@@ -676,14 +680,14 @@ stripToneMarks("hǎo"); // "hao"
676
680
  toneFromMarks("hǎo"); // 3
677
681
  ```
678
682
 
679
- `Syllable.tone` is `Tone | undefined`, and undefined is not the neutral tone: the
683
+ `Syllable.tone` is `Tone | undefined`, and the two mean different things. The
680
684
  `de` in 我的 is neutral (5), whereas the `bei` in a typed `beijing` has no tone
681
685
  written at all.
682
686
 
683
687
  ## Numbers
684
688
 
685
- Reading a number needs no dictionary, just arithmetic and about twenty
686
- readings, so this works with nothing loaded.
689
+ Reading a number needs no dictionary, just arithmetic and about twenty readings.
690
+ It runs before anything is loaded.
687
691
 
688
692
  ```ts
689
693
  import { numeralHanzi, percentHanzi, readNumeral } from "@kensio/pinyinjs";
@@ -695,8 +699,8 @@ numeralHanzi(2, { counts: true }); // "两", as in 两个西瓜, never 二个
695
699
  percentHanzi(95); // "百分之九十五", the order reverses
696
700
  ```
697
701
 
698
- The same digits are read two ways and nothing in the number says which, since
699
- 2026年 is spelled out and 2026个 is counted, so the style is the caller's:
702
+ The same digits are read two ways and the number alone leaves the choice open,
703
+ since 2026年 is spelled out and 2026个 is counted. The style is the caller's:
700
704
 
701
705
  ```ts
702
706
  numeralHanzi(2026); // "两千零二十六"
@@ -708,7 +712,7 @@ More in [numbers](docs/numerals/).
708
712
 
709
713
  ## Bopomofo, Wade-Giles, Yale, Gwoyeu Romatzyh and IPA
710
714
 
711
- Also dictionary-free: a romanisation is a mapping over about 420 syllables, so
715
+ Also dictionary-free. A romanisation is a mapping over about 420 syllables, so
712
716
  hanzi → Wade-Giles is hanzi → pinyin → Wade-Giles.
713
717
 
714
718
  ```ts
@@ -731,7 +735,7 @@ writeIpa(jiu); // "tɕiou˥˩"
731
735
  ```
732
736
 
733
737
  Gwoyeu Romatzyh is the odd one, and it needs no tone mark because the tone is
734
- spelled into the syllable, which is why 陝西 is Shaanxi in English and 山西 is
738
+ spelled into the syllable. That is why 陝西 is Shaanxi in English and 山西 is
735
739
  Shanxi:
736
740
 
737
741
  ```ts
@@ -755,7 +759,7 @@ first candidate recovers 79.05% of them. More in
755
759
  ## Sandhi
756
760
 
757
761
  The dictionary stores underlying tones, and sandhi is applied to the syllable
758
- array, so it can be switched off and works across word boundaries.
762
+ array. It can be switched off, and it works across word boundaries.
759
763
 
760
764
  ```ts
761
765
  import { applySandhi, readWord } from "@kensio/pinyinjs";
@@ -769,15 +773,15 @@ applySandhi(niHao); // unchanged by default
769
773
  applySandhi(niHao, { thirdTone: true }); // ní hǎo
770
774
  ```
771
775
 
772
- Third-tone sandhi is off by default: standard orthography writes 你好 as
776
+ Third-tone sandhi is off by default, because standard orthography writes 你好 as
773
777
  `nǐ hǎo` even though it is said `ní hǎo`. Turn it on for transcribing speech.
774
- Its domain is the prosodic foot rather than the syllable string, so 展览馆 is
778
+ Its domain is the prosodic foot and not the syllable string, so 展览馆 is
775
779
  `zhánlánguǎn` and 纸老虎 `zhǐláohǔ`, and 老板很好 keeps the 板 that a
776
780
  left-to-right scan would lower.
777
781
 
778
782
  ## Scripts and locales
779
783
 
780
- Three axes, not one. Taiwan writes 繁體 with `zh-TW` readings, but mainland
784
+ Three separate axes. Taiwan writes 繁體 with `zh-TW` readings, but mainland
781
785
  editions of classical texts use 繁體 with `zh-CN` readings, and Singapore uses
782
786
  简体.
783
787
 
@@ -794,7 +798,8 @@ Kong's 繁體 forms are recognised on the way in, so 羣眾 and 麪包 read exac
794
798
  ## Simplified and traditional
795
799
 
796
800
  `toScript` converts between the scripts, and the reading is what makes it
797
- accurate. Simplification merged distinct characters; the reading un-merges them.
801
+ accurate. Simplification merged distinct characters, and the reading un-merges
802
+ them.
798
803
 
799
804
  ```ts
800
805
  import { loadScriptTables, toScript } from "@kensio/pinyinjs";
@@ -808,12 +813,12 @@ toScript(dictionary, tables, "头发", { to: "zh-Hant" }); // "頭髮", tóufà
808
813
  toScript(dictionary, tables, "出发", { to: "zh-Hant" }); // "出發", chūfā
809
814
  ```
810
815
 
811
- 发 is 發 or 髮 and nothing about the character says which. Every other converter
812
- works from phrase tables alone, so it is right about the words on the list and
813
- guessing past it.
816
+ 发 is 發 or 髮, and the character on its own leaves the choice open. Every other
817
+ converter works from phrase tables alone. Each is right about the words on the
818
+ list and guessing past it.
814
819
 
815
- It runs both ways — 乾燥 is `gānzào` and simplifies to 干燥, while 乾隆 is
816
- `Qiánlóng` and stays 乾隆 — and it targets a region, because there is no
820
+ It runs both ways (乾燥 is `gānzào` and simplifies to 干燥, while 乾隆 is
821
+ `Qiánlóng` and stays 乾隆) and it targets a region, because there is no
817
822
  region-free 繁體:
818
823
 
819
824
  ```ts
@@ -821,12 +826,12 @@ toScript(dictionary, tables, "面包", { to: "zh-Hant-TW" }); // "麵包"
821
826
  toScript(dictionary, tables, "面包", { to: "zh-Hant-HK" }); // "麪包"
822
827
  ```
823
828
 
824
- `toScriptPieces` reports what settled each character, and which were guesses:
825
- 下面 is a surface or a bowl of noodles, both `xiàmiàn`, and it says so rather
826
- than picking silently. The tables load separately from the dictionary, so
827
- converting hanzi to pinyin costs nothing for them.
829
+ `toScriptPieces` reports what settled each character, and which were guesses.
830
+ 下面 is a surface or a bowl of noodles, both `xiàmiàn`, and it says so instead
831
+ of picking silently. The tables load separately from the dictionary, so
832
+ converting hanzi to pinyin never pays for them.
828
833
 
829
- This is orthography and not translation — 软件 becomes 軟件, never 軟體. See
834
+ This is orthography and not translation. 软件 becomes 軟件, never 軟體. See
830
835
  [script conversion](docs/script-conversion/).
831
836
 
832
837
  ## API
@@ -874,19 +879,19 @@ pnpm polyphones # score them against 20,139 hand-labelled polyphones
874
879
  pnpm build:data # rebuild data/ from the upstream sources
875
880
  ```
876
881
 
877
- `pnpm check` must pass: oxlint including the type-aware rules, `tsc` with
878
- `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess`, and vitest at 95%
879
- coverage thresholds.
882
+ `pnpm check` must pass, running oxlint including the type-aware rules, `tsc`
883
+ with `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess`, and vitest at
884
+ 95% coverage thresholds.
880
885
 
881
886
  Every example in this README is executed by `src/readme.test.ts` against the
882
887
  committed dictionary, so the two cannot drift apart. Change them together.
883
888
 
884
889
  The compiled dictionaries in `data/` are committed, so what ships is exactly
885
- what was tested. `pnpm build:data` fetches the four sources into `.cache/`
886
- (~32 MB), merges them, runs the build assertions, and rewrites `data/` and
887
- `NOTICE`. It fails rather than warns: no artifact is written unless 儿化 is
888
- repaired both ways, 一 and 不 sandhi is normalised out, every syllable is one
889
- the inventory knows, and every tier reads back exactly as it was built.
890
+ what was tested. `pnpm build:data` fetches the four sources into `.cache/` (~32
891
+ MB), merges them, runs the build assertions, and rewrites `data/` and `NOTICE`.
892
+ It fails where it could warn. No artifact is written unless 儿化 is repaired
893
+ both ways, 一 and 不 sandhi is normalised out, every syllable is one the
894
+ inventory knows, and every tier reads back exactly as it was built.
890
895
 
891
896
  ## Data sources
892
897
 
@@ -901,7 +906,7 @@ the inventory knows, and every tier reads back exactly as it was built.
901
906
 
902
907
  ## Licence
903
908
 
904
- `Apache-2.0 AND CC-BY-SA-4.0`, because the package is two things: the code is
909
+ `Apache-2.0 AND CC-BY-SA-4.0`, because the package is two things. The code is
905
910
  Apache-2.0, and the compiled dictionaries in `data/` are share-alike, CC-CEDICT
906
911
  being CC BY-SA 4.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE), which is
907
912
  generated from the sources the pipeline actually fetched.