@kensio/pinyinjs 1.5.3 → 1.6.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 (57) hide show
  1. package/CHANGELOG.md +153 -4
  2. package/README.md +44 -3
  3. package/data/core.entries +82 -81
  4. package/data/core.freq +0 -0
  5. package/data/core.keys +1 -0
  6. package/data/full.entries +4897 -4899
  7. package/data/full.freq +0 -0
  8. package/data/full.keys +0 -2
  9. package/data/manifest.json +12 -12
  10. package/data/script.map +27 -48
  11. package/data/standard.entries +438 -437
  12. package/data/standard.freq +0 -0
  13. package/data/standard.keys +1 -0
  14. package/dist/cli/commands.d.ts.map +1 -1
  15. package/dist/cli/commands.js +22 -1
  16. package/dist/cli/commands.js.map +1 -1
  17. package/dist/decode/convert.d.ts +42 -0
  18. package/dist/decode/convert.d.ts.map +1 -1
  19. package/dist/decode/convert.js +140 -17
  20. package/dist/decode/convert.js.map +1 -1
  21. package/dist/decode/decode.d.ts +3 -2
  22. package/dist/decode/decode.d.ts.map +1 -1
  23. package/dist/decode/decode.js +15 -10
  24. package/dist/decode/decode.js.map +1 -1
  25. package/dist/decode/hints.d.ts +138 -0
  26. package/dist/decode/hints.d.ts.map +1 -0
  27. package/dist/decode/hints.js +214 -0
  28. package/dist/decode/hints.js.map +1 -0
  29. package/dist/decode/lattice.d.ts +9 -1
  30. package/dist/decode/lattice.d.ts.map +1 -1
  31. package/dist/decode/lattice.js +5 -3
  32. package/dist/decode/lattice.js.map +1 -1
  33. package/dist/decode/reading-rules.d.ts +28 -0
  34. package/dist/decode/reading-rules.d.ts.map +1 -1
  35. package/dist/decode/reading-rules.js +157 -0
  36. package/dist/decode/reading-rules.js.map +1 -1
  37. package/dist/dictionary/assertions.d.ts.map +1 -1
  38. package/dist/dictionary/assertions.js +8 -0
  39. package/dist/dictionary/assertions.js.map +1 -1
  40. package/dist/dictionary/frequency-table.js +1 -1
  41. package/dist/dictionary/locale.js +2 -2
  42. package/dist/dictionary/merge.d.ts.map +1 -1
  43. package/dist/dictionary/merge.js +30 -1
  44. package/dist/dictionary/merge.js.map +1 -1
  45. package/dist/format/html.d.ts +32 -0
  46. package/dist/format/html.d.ts.map +1 -1
  47. package/dist/format/html.js +144 -0
  48. package/dist/format/html.js.map +1 -1
  49. package/dist/index.d.ts +3 -2
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +2 -2
  52. package/dist/index.js.map +1 -1
  53. package/dist/sources/unihan.d.ts +13 -4
  54. package/dist/sources/unihan.d.ts.map +1 -1
  55. package/dist/sources/unihan.js +63 -13
  56. package/dist/sources/unihan.js.map +1 -1
  57. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -9,6 +9,155 @@ which a dictionary rebuild can change, and the artifact format under `data/`.
9
9
 
10
10
  ## Unreleased
11
11
 
12
+ ## 1.6.0
13
+
14
+ ### Added
15
+
16
+ - **Hanzi and pinyin together, rather than one instead of the other.**
17
+ `convertToAnnotatedHtml` writes the characters with their reading above them,
18
+ as `<ruby>` markup — the output every other mode could not give, and what a
19
+ learner's text, a subtitle and a dictionary entry all want. `convert` and
20
+ `convertToHtml` write the reading _instead of_ the characters.
21
+ `toAnnotatedHtml(pieces)` renders pieces you already have, and the command
22
+ line has `pinyinjs annotate`.
23
+
24
+ Each `<rt>` holds exactly what `convertToHtml` would have written, so tone
25
+ classes and uncertainty marking work inside an annotation as they do outside
26
+ one. `<rp>` gives the parenthesised fallback, the `<ruby>` carries `lang="zh"`
27
+ and the syllables inside it keep `zh-Latn-CN-pinyin`, since on an English page
28
+ a screen reader needs to be told both.
29
+
30
+ **A base is not always one character**, which is what makes this harder than
31
+ it looks and what every naive per-character annotation gets wrong. 玩儿 is two
32
+ characters and the one syllable `wánr`; 95% is `bǎifēnzhījiǔshíwǔ` over three
33
+ written characters in the other order; 1988 is `yī jiǔ bā bā` over one. All
34
+ are annotated whole. `ConvertedPiece` gains a `source` — the characters a
35
+ piece reads, or undefined where it reads on into the ones named before it —
36
+ which is what makes that expressible.
37
+
38
+ Pinyin orthography stays out of the hanzi it annotates: no word spaces, since
39
+ Chinese is not written with them; the hyphen of `gāngān-jìngjìng` sits in the
40
+ reading, being a boundary inside one word rather than a gap between two; and
41
+ the base keeps 。 where the conversion writes a full stop.
42
+
43
+ - **`readings`: a conversion takes readings the caller asserts.** No rule
44
+ settles every polyphone, and some texts are genuinely ambiguous —
45
+ 孩子越长越漂亮 grows where 头发越长越漂亮 lengthens, and nothing in the
46
+ characters says which. An application that knows its own content can now say
47
+ so, rather than the library guessing on its behalf.
48
+
49
+ ```ts
50
+ convert(dictionary, "这篇文章不太长。", { readings: { 太长: "tài cháng" } });
51
+ ```
52
+
53
+ A **word** hint asserts something about the text it names and rewrites exactly
54
+ those characters, so a bare `长` leaves 校长 as `xiàozhǎng` — the dictionary
55
+ knowing that word is better evidence about the stretch than a remark about one
56
+ of its characters, and that is what makes a corrections table safe to
57
+ accumulate. Naming the whole word does reach it, and keeps it whole:
58
+ `{ 银行: "yín xíng" }` gives `yínxíng`.
59
+
60
+ A **positional** hint asserts something about one character of one text, and
61
+ nothing outranks it, the enclosing word included. Positions count code points
62
+ from the start of the text, across any non-Han runs in it. The list form takes
63
+ both kinds together.
64
+
65
+ Hints enter as lattice edges rather than as rules, because the rules layer
66
+ cannot invent a reading no source attests — which is exactly what a hint is,
67
+ the difference being that a caller attests it. Spacing is untouched, an
68
+ unmarked syllable is 轻声, and a hint that cannot be parsed throws rather than
69
+ being skipped: a correction that silently does nothing is worse than one that
70
+ fails.
71
+
72
+ ### Fixed
73
+
74
+ - **Polyphones defaulted to whichever reading a dictionary happened to print
75
+ first.** Unihan's `kTGHZ2013` ranked above `kMandarin`, and that field
76
+ prefixes every reading with its entry number in 《現代漢語規範詞典》 — 勒 is
77
+ `212.050:lè 212.100:lēi` — so its order is a page order, and taking the first
78
+ reading meant taking whichever one sorted earlier by pinyin. 殷 read `yān` and
79
+ 楷 `jiē`. The dictionary-indexed fields now rank last: they are a good
80
+ _source_ of readings and a meaningless _ranking_ of them.
81
+
82
+ `kHanyuPinlu` still leads where it lists more than one reading, because there
83
+ it has counted them against each other, but where it lists one it counted
84
+ nothing — it is saying "this occurred", not "this leads" — and a lone reading
85
+ now ranks last too. Only ten characters have one that disagrees with
86
+ `kMandarin`, and eight of those are simply pre-1985: the corpus behind the
87
+ field, 《現代漢語頻率詞典》, predates the 普通话异读词审音表, and the 132
88
+ occurrences carrying 绩 as `jī` are the old standard rather than a case for
89
+ it.
90
+
91
+ 354 readings fixed against 94 broken over 25 characters, the largest being 贾
92
+ `gǔ`→`jiǎ`, 苔 `tāi`→`tái`, 町 `dīng`→`tǐng`, 殷 `yān`→`yīn` and 芥
93
+ `gài`→`jiè`. CPP's polyphone score is 90.34%, up from 89.05%, and the gold
94
+ corpus does not move. What broke is mostly `kMandarin` being wrong with
95
+ nothing behind it — 媛 `yuán`→`yuàn`, 桔 `jú`→`jié`, 耶 `yē`→`yé`, 缪
96
+ `miào`→`móu` — which needs a third opinion with corpus evidence behind it
97
+ rather than an override, since here the sources disagree. 䘚 is now paired
98
+ with 卒, so `core` and `standard` each gained a key.
99
+
100
+ - **`full` read 特徵 as `tèzhǐ` where `standard` read it right.**
101
+ `large_pinyin.txt` is a 简体 corpus, and where it carries a 繁體 headword
102
+ anyway it has read it as though the characters were 简体, taking each at face
103
+ value instead of as the 简体 character it stands for: 徵 as itself rather than
104
+ as 征, 沈 as the surname rather than as 沉, so 沈溺 came out `shěnnì`. Of the
105
+ 2,854 corpus headwords carrying a 繁體-only character, 72 are ones CC-CEDICT
106
+ pairs with a 简体 form and 13 of those read wrongly.
107
+
108
+ The reading was only half of it. A corpus headword becomes an entry of its
109
+ own, and an entry outranks the 繁體 key that the traditional derivation hangs
110
+ on the 简体 entry — so the word was in the dictionary twice, and which copy
111
+ answered depended on the tier. `standard` excludes the phrase tail and was
112
+ right; `full`, the default, was wrong. Loading more dictionary made the answer
113
+ worse, which is also the upgrade path the docs describe, of converting with
114
+ `standard` and reloading as `full`.
115
+
116
+ A headword CC-CEDICT knows _only_ as another word's 繁體 spelling now
117
+ contributes no entry, and the word keeps the reading CC-CEDICT pairs it with.
118
+ 68 entries drop out and their keys are re-claimed from the 简体 entry, so
119
+ `full` loses 2 keys; 简→繁→简 round trips gain 38 against 5 lost on 繁→简→繁,
120
+ both unchanged as percentages. The 2,782 headwords CC-CEDICT does not pair are
121
+ rare or mixed-script words whose characters do read them, and are left alone.
122
+ Three of the words are build assertions now, this being the kind of defect
123
+ that comes back when a rolling source is refreshed and nothing is watching.
124
+
125
+ - **长 read `zhǎng` wherever no word covered it**, so 这篇文章不太长 came out
126
+ `bú tài zhǎng` and 要多长时间 `duō zhǎng shíjiān`. The stored default is not
127
+ the defect — Unihan ranks the readings `zhǎng(1879) cháng(1179)` and names
128
+ `zhǎng` in `kMandarin`, and that ranking is what reads 署长, 团长, 公安局长 and
129
+ 总会长 correctly off a bare 长 at the end of a title. What was missing is that
130
+ nothing could prefer the alternate at a position no word covers.
131
+
132
+ A degree adverb settles it, and only from the left: a growing 长 is a verb and
133
+ no 很, 太, 最 or 多 can modify one, while what follows an adjectival 长 is a
134
+ noun, a particle or the end of the sentence — which is what follows half the
135
+ verbs too. 得, 着 and the 越…越 correlative are the exceptions, and are guarded;
136
+ 了 and 的 are deliberately not, since after an adverb they are the sentence
137
+ particle and the attributive, making 时间太长了 and 很长的道路 both `cháng`.
138
+
139
+ Measured over the same 88,866 lines of Tatoeba and zh.wikipedia the other
140
+ edge rules were sized against, 260 长 decode as a word of their own and this
141
+ moves 75 to `cháng`, all 75 correctly. On CPP's 40 hand-labelled 长 the
142
+ character goes 85.00% to 87.50%; overall CPP is unmoved at 90.34%, as expected
143
+ of one character in 20,139.
144
+
145
+ - **越长越高 read `yuè cháng yuè gāo`.** 越长 is a key read `yuè cháng` and the
146
+ only one of its shape — 越大, 越高, 越好 and 越快 are all absent, so 越高越好
147
+ decodes as two words while 越长越高 reached for a word nothing else in the
148
+ paradigm has. It carries no part of speech, which is how a reading somebody
149
+ asserted is held rather than a word anybody counted, and only
150
+ `large_pinyin.txt` has it. Where the far half of the correlative names
151
+ something growing produces — 高, 大, 胖, 壮, 结实 — the edge is dropped and the
152
+ character's own `zhǎng` stands.
153
+
154
+ Unlike everything else here this is a heuristic with no corpus behind it: 越长
155
+ occurs three times in the 88,866 lines and all three are 越来越长 or 说的越长.
156
+ It forbids rather than forces, so `cháng` stays on as a rival one bucket
157
+ dearer and the syllable still reports itself uncertain — 越长越X is genuinely
158
+ ambiguous, 孩子越长越漂亮 growing where 头发越长越漂亮 lengthens, which is why
159
+ 漂亮 is not in the list. 时间越长越好 and 头发越长 are untouched.
160
+
12
161
  ## 1.5.3
13
162
 
14
163
  ### Fixed
@@ -460,14 +609,14 @@ decoder internals and the build pipeline.
460
609
 
461
610
  ### Accuracy at 1.0.0
462
611
 
463
- Over the 126-case gold corpus, which is committed to the repository rather than
612
+ Over the 130-case gold corpus, which is committed to the repository rather than
464
613
  to the published package, and scored by `pnpm accuracy`:
465
614
 
466
615
  | | lattice | greedy baseline |
467
616
  | ---------------- | --------: | --------------: |
468
- | exact match | **97.6%** | 88.9% |
469
- | reading accuracy | 99.7% | 98.4% |
470
- | spacing (F1) | **99.8%** | 96.5% |
617
+ | exact match | **97.7%** | 86.9% |
618
+ | reading accuracy | 99.7% | 97.9% |
619
+ | spacing (F1) | **99.8%** | 95.7% |
471
620
 
472
621
  Every figure in that table is asserted against the scorer by
473
622
  `src/changelog.test.ts`, because a number nothing executes goes stale, which is
package/README.md CHANGED
@@ -59,6 +59,7 @@ běijīng běijīng ㄅㄟˇ ㄐㄧㄥ pei³-ching¹ běijīng beeij
59
59
  | ------------ | --------------------------------------------------- |
60
60
  | `convert` | hanzi to pinyin |
61
61
  | `html` | the same, as HTML |
62
+ | `annotate` | hanzi with its pinyin above, as ruby HTML |
62
63
  | `slug` | hanzi to a URL-safe slug |
63
64
  | `script` | 简体 ↔ 繁體 conversion |
64
65
  | `explain` | each syllable, how settled it was, and what it beat |
@@ -143,9 +144,9 @@ Load the dictionary once and keep it; it is immutable and safe to share.
143
144
 
144
145
  | Tier | Entries | Download (brotli) | Contains |
145
146
  | ---------- | ------: | ----------------: | ---------------------- |
146
- | `core` | 16,730 | 70 KB | single characters only |
147
- | `standard` | 66,730 | 376 KB | the most common words |
148
- | `full` | 461,623 | 2,381 KB | every word |
147
+ | `core` | 16,727 | 70 KB | single characters only |
148
+ | `standard` | 66,727 | 376 KB | the most common words |
149
+ | `full` | 461,555 | 2,381 KB | every word |
149
150
 
150
151
  `full` is the default. The tiers are nested, so a page can load `standard`
151
152
  first, convert with it, and reload as `full` arrives.
@@ -349,6 +350,45 @@ included, so write your own:
349
350
  Takes any `convert` option, plus `toneClasses: false`, `markUncertain: false`
350
351
  and `lang: false`. `toHtml(pieces, options)` renders pieces you already have.
351
352
 
353
+ ## Annotated output: hanzi and pinyin together
354
+
355
+ `convertToHtml` writes the pinyin _instead of_ the hanzi. To keep both — a
356
+ learner's text, a subtitle, a dictionary entry — annotate it instead:
357
+
358
+ ```ts
359
+ import { convertToAnnotatedHtml } from "@kensio/pinyinjs";
360
+
361
+ convertToAnnotatedHtml(dictionary, "银行");
362
+ // <ruby lang="zh">银<rp>(</rp><rt><span class="py-syllable py-tone-2"
363
+ // lang="zh-Latn-CN-pinyin">yín</span></rt><rp>)</rp></ruby>…
364
+ ```
365
+
366
+ The markup is `<ruby>`, which browsers lay out natively — the reading sits
367
+ above the characters, and `<rp>` puts it in parentheses where it does not. Each
368
+ `<rt>` holds exactly what `convertToHtml` would have written, so tone colours
369
+ and uncertainty marking work inside an annotation as they do outside one.
370
+
371
+ **A base is not always one character**, and this is where per-character
372
+ annotation goes wrong. 玩儿 is two characters and the one syllable `wánr`, and a
373
+ read number reverses on the way, so both are annotated whole:
374
+
375
+ ```ts
376
+ convertToAnnotatedHtml(dictionary, "玩儿"); // one <ruby>, 玩儿 over wánr
377
+ convertToAnnotatedHtml(dictionary, "95%"); // one <ruby>, 95% over bǎifēnzhījiǔshíwǔ
378
+ ```
379
+
380
+ Ruby needs no styling to work, but the reading is small by default:
381
+
382
+ ```css
383
+ ruby rt {
384
+ font-size: 0.5em;
385
+ }
386
+ ```
387
+
388
+ `toAnnotatedHtml(pieces, options)` renders pieces you already have, and
389
+ `ConvertedPiece.source` is what makes that possible: the characters each piece
390
+ reads, or undefined where it reads on into the ones before it.
391
+
352
392
  ## Look words up
353
393
 
354
394
  ```ts
@@ -598,6 +638,7 @@ This is orthography and not translation — 软件 becomes 軟件, never 軟體.
598
638
  | `convert(dictionary, text, options?)` | hanzi → pinyin |
599
639
  | `convertPieces(dictionary, text, ...)` | the same, per syllable, with confidence |
600
640
  | `convertToHtml(dictionary, text, ...)` | the same, as HTML |
641
+ | `convertToAnnotatedHtml(dictionary, text, ...)` | hanzi and pinyin together, as ruby HTML |
601
642
  | `slug(dictionary, text, options?)` | hanzi → a URL-safe slug |
602
643
  | `joinPieces(pieces)` / `toHtml(pieces)` | render pieces |
603
644
  | `isUncertain(confidence)` | was this syllable a guess? |