@kensio/pinyinjs 1.5.3 → 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 (112) hide show
  1. package/README.md +252 -5
  2. package/data/core.entries +462 -460
  3. package/data/core.freq +0 -0
  4. package/data/core.keys +2 -0
  5. package/data/full.entries +13732 -13734
  6. package/data/full.freq +0 -0
  7. package/data/full.keys +0 -2
  8. package/data/manifest.json +15 -15
  9. package/data/script.map +34 -55
  10. package/data/standard.entries +1354 -1352
  11. package/data/standard.freq +0 -0
  12. package/data/standard.keys +2 -0
  13. package/dist/cli/arguments.d.ts +38 -0
  14. package/dist/cli/arguments.d.ts.map +1 -1
  15. package/dist/cli/arguments.js +46 -0
  16. package/dist/cli/arguments.js.map +1 -1
  17. package/dist/cli/commands.d.ts.map +1 -1
  18. package/dist/cli/commands.js +229 -2
  19. package/dist/cli/commands.js.map +1 -1
  20. package/dist/cli/help.d.ts.map +1 -1
  21. package/dist/cli/help.js +5 -0
  22. package/dist/cli/help.js.map +1 -1
  23. package/dist/decode/convert.d.ts +58 -0
  24. package/dist/decode/convert.d.ts.map +1 -1
  25. package/dist/decode/convert.js +135 -37
  26. package/dist/decode/convert.js.map +1 -1
  27. package/dist/decode/decode.d.ts +3 -2
  28. package/dist/decode/decode.d.ts.map +1 -1
  29. package/dist/decode/decode.js +15 -10
  30. package/dist/decode/decode.js.map +1 -1
  31. package/dist/decode/hints.d.ts +138 -0
  32. package/dist/decode/hints.d.ts.map +1 -0
  33. package/dist/decode/hints.js +214 -0
  34. package/dist/decode/hints.js.map +1 -0
  35. package/dist/decode/lattice.d.ts +9 -1
  36. package/dist/decode/lattice.d.ts.map +1 -1
  37. package/dist/decode/lattice.js +5 -3
  38. package/dist/decode/lattice.js.map +1 -1
  39. package/dist/decode/reading-rules.d.ts +28 -0
  40. package/dist/decode/reading-rules.d.ts.map +1 -1
  41. package/dist/decode/reading-rules.js +157 -0
  42. package/dist/decode/reading-rules.js.map +1 -1
  43. package/dist/decode/runs.d.ts +11 -0
  44. package/dist/decode/runs.d.ts.map +1 -1
  45. package/dist/decode/runs.js +19 -0
  46. package/dist/decode/runs.js.map +1 -1
  47. package/dist/decode/segment.d.ts +66 -0
  48. package/dist/decode/segment.d.ts.map +1 -0
  49. package/dist/decode/segment.js +79 -0
  50. package/dist/decode/segment.js.map +1 -0
  51. package/dist/dictionary/artifact.d.ts +10 -0
  52. package/dist/dictionary/artifact.d.ts.map +1 -1
  53. package/dist/dictionary/artifact.js +17 -0
  54. package/dist/dictionary/artifact.js.map +1 -1
  55. package/dist/dictionary/assertions.d.ts.map +1 -1
  56. package/dist/dictionary/assertions.js +8 -0
  57. package/dist/dictionary/assertions.js.map +1 -1
  58. package/dist/dictionary/corpus-mass.d.ts +68 -0
  59. package/dist/dictionary/corpus-mass.d.ts.map +1 -0
  60. package/dist/dictionary/corpus-mass.js +123 -0
  61. package/dist/dictionary/corpus-mass.js.map +1 -0
  62. package/dist/dictionary/dictionary.d.ts +56 -0
  63. package/dist/dictionary/dictionary.d.ts.map +1 -1
  64. package/dist/dictionary/dictionary.js +116 -1
  65. package/dist/dictionary/dictionary.js.map +1 -1
  66. package/dist/dictionary/erhua.d.ts +4 -0
  67. package/dist/dictionary/erhua.d.ts.map +1 -1
  68. package/dist/dictionary/erhua.js +6 -0
  69. package/dist/dictionary/erhua.js.map +1 -1
  70. package/dist/dictionary/frequency-table.js +1 -1
  71. package/dist/dictionary/locale.js +2 -2
  72. package/dist/dictionary/merge.d.ts.map +1 -1
  73. package/dist/dictionary/merge.js +46 -9
  74. package/dist/dictionary/merge.js.map +1 -1
  75. package/dist/dictionary/tiers.d.ts +2 -1
  76. package/dist/dictionary/tiers.d.ts.map +1 -1
  77. package/dist/dictionary/tiers.js +77 -11
  78. package/dist/dictionary/tiers.js.map +1 -1
  79. package/dist/format/html.d.ts +32 -0
  80. package/dist/format/html.d.ts.map +1 -1
  81. package/dist/format/html.js +144 -0
  82. package/dist/format/html.js.map +1 -1
  83. package/dist/grade/check.d.ts +156 -0
  84. package/dist/grade/check.d.ts.map +1 -0
  85. package/dist/grade/check.js +356 -0
  86. package/dist/grade/check.js.map +1 -0
  87. package/dist/index.d.ts +14 -3
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +7 -2
  90. package/dist/index.js.map +1 -1
  91. package/dist/search/candidates.d.ts +84 -0
  92. package/dist/search/candidates.d.ts.map +1 -0
  93. package/dist/search/candidates.js +290 -0
  94. package/dist/search/candidates.js.map +1 -0
  95. package/dist/search/match.d.ts +72 -0
  96. package/dist/search/match.d.ts.map +1 -0
  97. package/dist/search/match.js +419 -0
  98. package/dist/search/match.js.map +1 -0
  99. package/dist/search/query.d.ts +49 -0
  100. package/dist/search/query.d.ts.map +1 -0
  101. package/dist/search/query.js +115 -0
  102. package/dist/search/query.js.map +1 -0
  103. package/dist/search/reverse-index.d.ts +141 -0
  104. package/dist/search/reverse-index.d.ts.map +1 -0
  105. package/dist/search/reverse-index.js +356 -0
  106. package/dist/search/reverse-index.js.map +1 -0
  107. package/dist/sources/unihan.d.ts +22 -4
  108. package/dist/sources/unihan.d.ts.map +1 -1
  109. package/dist/sources/unihan.js +74 -13
  110. package/dist/sources/unihan.js.map +1 -1
  111. package/package.json +2 -3
  112. package/CHANGELOG.md +0 -485
@@ -37,7 +37,8 @@ export declare const STANDARD_TIER_WORDS = 50000;
37
37
  * makes a dictionary usable at all: they cover roughly half of running text on
38
38
  * their own, and they are the fallback for every position no word matches.
39
39
  * Tiering cuts the phrase tail and the unused characters, never a character
40
- * that some word is written with.
40
+ * that some word is written with — and never one the tier is keyed for anyway,
41
+ * which is what {@link charactersHeld} adds on top.
41
42
  *
42
43
  * `full` holds everything, including the characters no source uses in a word.
43
44
  * They cost little there and are the difference between reading a rare
@@ -1 +1 @@
1
- {"version":3,"file":"tiers.d.ts","sourceRoot":"","sources":["../../src/dictionary/tiers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;;;;GAOG;AACH,eAAO,MAAM,KAAK,YAAI,MAAM,EAAE,UAAU,EAAE,MAAM,CAAU,CAAC;AAE3D;;GAEG;AACH,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1C;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,SAAwB,CAAC;AAElD;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,QAAS,CAAC;AA6C1C;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,SAAS,eAAe,EAAE,EACnC,IAAI,EAAE,IAAI,GACT,SAAS,eAAe,EAAE,CAkB5B"}
1
+ {"version":3,"file":"tiers.d.ts","sourceRoot":"","sources":["../../src/dictionary/tiers.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;;;;GAOG;AACH,eAAO,MAAM,KAAK,YAAI,MAAM,EAAE,UAAU,EAAE,MAAM,CAAU,CAAC;AAE3D;;GAEG;AACH,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1C;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,SAAwB,CAAC;AAElD;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAqH1C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,SAAS,eAAe,EAAE,EACnC,IAAI,EAAE,IAAI,GACT,SAAS,eAAe,EAAE,CAkB5B"}
@@ -1,4 +1,5 @@
1
1
  import { isSingleCharacter, toCharacters } from "../script/characters.js";
2
+ import { traditionalForms } from "./artifact.js";
2
3
  /**
3
4
  * The dictionary tiers, smallest first and each contained in the next.
4
5
  *
@@ -65,6 +66,71 @@ function charactersInUse(entries) {
65
66
  }
66
67
  return inUse;
67
68
  }
69
+ /**
70
+ * The characters a tier holds: the ones in use, and the ones it has to answer.
71
+ *
72
+ * "In use" alone is not enough, because a tier is keyed by more than the
73
+ * headwords it holds — an entry claims every 繁體 spelling attested for it, and
74
+ * some of those spellings are rare characters with entries of their own. A tier
75
+ * holding 卒 is keyed for 䘚 whether or not it holds 䘚, so without the
76
+ * character's own entry it answers that key from the 卒 that reached it
77
+ * sideways and reads `zú`, where `full` lets 䘚's own entry win the key and
78
+ * reads `zhú`. Neither reading is wrong — 䘚 written for 卒 really is `zú` —
79
+ * but which one you get must not depend on how much of the dictionary has
80
+ * loaded, or the documented upgrade path contradicts itself mid-paragraph.
81
+ *
82
+ * Admitting the entry is all it takes: the artifact's `claimKeys` puts
83
+ * headwords ahead of 繁體 aliases in every tier, so once 䘚 is present it wins
84
+ * its own key exactly as it does in `full`.
85
+ *
86
+ * It repeats to a fixpoint because an admitted character claims spellings of
87
+ * its own, and one of those can be another character with an entry behind it.
88
+ * On the current sources it settles at once — 242 characters admitted, then a
89
+ * confirming pass that admits none — but nothing in the merge rules a chain
90
+ * out, and the pass is a set lookup per entry against a build that reads four
91
+ * source files.
92
+ */
93
+ function charactersHeld(entries, words) {
94
+ const headed = new Set();
95
+ for (const entry of entries) {
96
+ if (isSingleCharacter(entry.hans)) {
97
+ headed.add(entry.hans);
98
+ }
99
+ }
100
+ const inUse = charactersInUse(entries);
101
+ const held = new Set();
102
+ for (const character of headed) {
103
+ if (inUse.has(character)) {
104
+ held.add(character);
105
+ }
106
+ }
107
+ /**
108
+ * Admit the characters this entry claims the key of, reporting any news.
109
+ */
110
+ const admitClaims = (entry) => {
111
+ let admitted = false;
112
+ for (const form of traditionalForms(entry)) {
113
+ if (headed.has(form) && !held.has(form)) {
114
+ held.add(form);
115
+ admitted = true;
116
+ }
117
+ }
118
+ return admitted;
119
+ };
120
+ for (const entry of words) {
121
+ admitClaims(entry);
122
+ }
123
+ let growing = true;
124
+ while (growing) {
125
+ growing = false;
126
+ for (const entry of entries) {
127
+ if (isSingleCharacter(entry.hans) && held.has(entry.hans)) {
128
+ growing = admitClaims(entry) || growing;
129
+ }
130
+ }
131
+ }
132
+ return held;
133
+ }
68
134
  /**
69
135
  * The entries a tier contains.
70
136
  *
@@ -72,7 +138,8 @@ function charactersInUse(entries) {
72
138
  * makes a dictionary usable at all: they cover roughly half of running text on
73
139
  * their own, and they are the fallback for every position no word matches.
74
140
  * Tiering cuts the phrase tail and the unused characters, never a character
75
- * that some word is written with.
141
+ * that some word is written with — and never one the tier is keyed for anyway,
142
+ * which is what {@link charactersHeld} adds on top.
76
143
  *
77
144
  * `full` holds everything, including the characters no source uses in a word.
78
145
  * They cost little there and are the difference between reading a rare
@@ -82,15 +149,14 @@ export function selectTier(entries, tier) {
82
149
  if (tier === "full") {
83
150
  return entries;
84
151
  }
85
- const inUse = charactersInUse(entries);
86
- const characters = entries.filter((entry) => isSingleCharacter(entry.hans) && inUse.has(entry.hans));
87
- if (tier === "core") {
88
- return characters;
89
- }
90
- const words = entries
91
- .filter((entry) => !isSingleCharacter(entry.hans))
92
- .toSorted(byFrequency)
93
- .slice(0, STANDARD_TIER_WORDS);
94
- return [...characters, ...words];
152
+ const words = tier === "core"
153
+ ? []
154
+ : entries
155
+ .filter((entry) => !isSingleCharacter(entry.hans))
156
+ .toSorted(byFrequency)
157
+ .slice(0, STANDARD_TIER_WORDS);
158
+ const held = charactersHeld(entries, words);
159
+ const characters = entries.filter((entry) => isSingleCharacter(entry.hans) && held.has(entry.hans));
160
+ return tier === "core" ? characters : [...characters, ...words];
95
161
  }
96
162
  //# sourceMappingURL=tiers.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tiers.js","sourceRoot":"","sources":["../../src/dictionary/tiers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAG1E;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,CAAU,CAAC;AAO3D;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,MAAqB,CAAC;AAElD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAE1C;;GAEG;AACH,SAAS,WAAW,CAAC,IAAqB,EAAE,KAAsB;IAChE,IAAI,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,SAAS,EAAE,CAAC;QACvC,OAAO,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IAC1C,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACzC,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAS,eAAe,CACtB,OAAmC;IAEnC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,iBAAiB,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YAClC,IAAI,KAAK,CAAC,SAAS,GAAG,CAAC,EAAE,CAAC;gBACxB,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACxB,CAAC;YACD,SAAS;QACX,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5C,KAAK,MAAM,SAAS,IAAI,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC3C,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACvB,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,UAAU,CACxB,OAAmC,EACnC,IAAU;IAEV,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,MAAM,KAAK,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAC/B,CAAC,KAAK,EAAE,EAAE,CAAC,iBAAiB,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAClE,CAAC;IACF,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,MAAM,KAAK,GAAG,OAAO;SAClB,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,iBAAiB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;SACjD,QAAQ,CAAC,WAAW,CAAC;SACrB,KAAK,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAC;IACjC,OAAO,CAAC,GAAG,UAAU,EAAE,GAAG,KAAK,CAAC,CAAC;AACnC,CAAC"}
1
+ {"version":3,"file":"tiers.js","sourceRoot":"","sources":["../../src/dictionary/tiers.ts"],"names":[],"mappings":"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"}
@@ -35,6 +35,38 @@ export interface HtmlOptions extends ConvertOptions {
35
35
  * once and render more than one way.
36
36
  */
37
37
  export declare function toHtml(pieces: readonly ConvertedPiece[], options?: HtmlOptions): string;
38
+ /**
39
+ * Mark up a converted text as hanzi with its reading above, syllable by
40
+ * syllable.
41
+ *
42
+ * The output every other mode cannot give: `convert` and {@link toHtml} write
43
+ * the pinyin *instead of* the hanzi, and a learner's text, a subtitle and a
44
+ * dictionary entry all want both at once. The markup is `<ruby>`, which every
45
+ * current browser lays out natively and which degrades to parentheses through
46
+ * `<rp>` where it is not supported.
47
+ *
48
+ * ```html
49
+ * <ruby lang="zh">银<rp>(</rp><rt><span …>yín</span></rt><rp>)</rp></ruby>
50
+ * ```
51
+ *
52
+ * Each `<rt>` holds whatever {@link toHtml} would have written for those
53
+ * syllables, so tone classes and uncertainty marking work inside an annotation
54
+ * exactly as they do outside one.
55
+ *
56
+ * **A base is not always one character.** 玩儿 is two characters and the one
57
+ * syllable `wánr`, and a read number reverses on the way — 95% is
58
+ * `bǎifēnzhījiǔshíwǔ` — so both are annotated whole. Splitting them per
59
+ * character is what everything that emits ruby from a pinyin library gets
60
+ * wrong, and it is why {@link ConvertedPiece.source} exists.
61
+ */
62
+ export declare function toAnnotatedHtml(pieces: readonly ConvertedPiece[], options?: HtmlOptions): string;
63
+ /**
64
+ * Convert hanzi to pinyin as HTML, hanzi and reading together.
65
+ *
66
+ * {@link toAnnotatedHtml} applied to a fresh conversion; see it for what the
67
+ * markup is and why a base is not always one character.
68
+ */
69
+ export declare function convertToAnnotatedHtml(dictionary: Dictionary, text: string, options?: HtmlOptions): string;
38
70
  /**
39
71
  * Convert hanzi to pinyin as HTML, one element per syllable.
40
72
  *
@@ -1 +1 @@
1
- {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/format/html.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,cAAc,EAEpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAI9D;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,cAAc;IACjD;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AA+HD;;;;;GAKG;AACH,wBAAgB,MAAM,CACpB,MAAM,EAAE,SAAS,cAAc,EAAE,EACjC,OAAO,GAAE,WAAgB,GACxB,MAAM,CAER;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,UAAU,EACtB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,WAAgB,GACxB,MAAM,CAER"}
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/format/html.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,cAAc,EAEpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAI9D;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,cAAc;IACjD;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AA+HD;;;;;GAKG;AACH,wBAAgB,MAAM,CACpB,MAAM,EAAE,SAAS,cAAc,EAAE,EACjC,OAAO,GAAE,WAAgB,GACxB,MAAM,CAER;AA+HD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,SAAS,cAAc,EAAE,EACjC,OAAO,GAAE,WAAgB,GACxB,MAAM,CAIR;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,UAAU,EACtB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,WAAgB,GACxB,MAAM,CAER;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,UAAU,EACtB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,WAAgB,GACxB,MAAM,CAER"}
@@ -122,6 +122,150 @@ function writePiece(piece, options) {
122
122
  export function toHtml(pieces, options = {}) {
123
123
  return pieces.map((piece) => writePiece(piece, options)).join("");
124
124
  }
125
+ /**
126
+ * What the characters being annotated declare themselves to be.
127
+ *
128
+ * The mirror of {@link languageTag}: inside an annotation the base is hanzi and
129
+ * only the reading is pinyin, so on an English page the two need different tags
130
+ * and neither can be left to inherit.
131
+ *
132
+ * **`zh`, with no script subtag, and that is deliberate.** `Hans` or `Hant`
133
+ * would be better for font selection, and neither can be known here. The
134
+ * conversion's locale does not settle it — script and locale are separate axes,
135
+ * and a mainland edition of a classical text is 繁體 read as `zh-CN` — and
136
+ * detecting it from the characters needs the script tables, which load
137
+ * separately from the dictionary precisely so that converting does not pay for
138
+ * them. A tag that is merely unspecific is worth more than one that is wrong,
139
+ * so a caller who knows the script should wrap the output and say so there.
140
+ */
141
+ const BASE_TAG = "zh";
142
+ /**
143
+ * Gather pieces into the bases they are readings of.
144
+ *
145
+ * A piece naming characters opens a group and every piece after it that names
146
+ * none reads on into the same one, which is how 玩儿 keeps `wánr` over both
147
+ * characters and 95% keeps `bǎifēnzhījiǔshíwǔ` over all three. Pieces writing
148
+ * no reading at all — the spaces between words, and runs that were never Han —
149
+ * are their own groups, with nothing to annotate.
150
+ */
151
+ function annotationsOf(pieces) {
152
+ const groups = [];
153
+ for (const [at, piece] of pieces.entries()) {
154
+ const open = groups.at(-1);
155
+ if (open?.source !== undefined && continues(piece, pieces[at + 1])) {
156
+ groups[groups.length - 1] = {
157
+ source: open.source,
158
+ pieces: [...open.pieces, piece],
159
+ };
160
+ continue;
161
+ }
162
+ groups.push({ source: piece.source, pieces: [piece] });
163
+ }
164
+ return groups;
165
+ }
166
+ /**
167
+ * Whether a piece belongs to the annotation the group before it opened.
168
+ *
169
+ * Three kinds do:
170
+ *
171
+ * - A **syllable naming no characters** is reading on into the ones already
172
+ * named, which is 儿化 and every syllable of a read number after the first.
173
+ * - A **separator that is not a space** is a mark *inside* one orthographic
174
+ * word rather than a gap between two — the hyphen of `gāngān-jìngjìng`, the
175
+ * only one GB/T 16159 writes — so it belongs beside the syllables it divides.
176
+ * - A **space with more of the same reading after it**. 1988 is read
177
+ * `yī jiǔ bā bā`, four syllables over one base, and the spaces between them
178
+ * are inside that reading rather than between two of them.
179
+ *
180
+ * That last one is why this needs the piece after it. A space is written the
181
+ * same way whether it separates two words or two syllables of one number, and
182
+ * what tells them apart is whether what follows is still reading the same
183
+ * characters. Told apart wrongly, the reading escapes its own annotation:
184
+ * 1988年 came out as `1988` over `yī` with `jiǔ bā bā` loose beside it.
185
+ *
186
+ * A space between two words is the case that does not continue, and dropping it
187
+ * is the point: the hanzi it annotates has no space in it, and each base is
188
+ * already its own group on the page.
189
+ */
190
+ function continues(piece, next) {
191
+ if (piece.syllable !== undefined) {
192
+ return piece.source === undefined;
193
+ }
194
+ if (piece.source !== undefined) {
195
+ return false;
196
+ }
197
+ return (piece.text.trim() !== "" ||
198
+ (next?.syllable !== undefined && next.source === undefined));
199
+ }
200
+ /**
201
+ * Mark up one base and its reading.
202
+ */
203
+ function writeAnnotation(annotation, options) {
204
+ const { lang = true } = options;
205
+ const { source, pieces } = annotation;
206
+ if (pieces.every((piece) => piece.syllable === undefined)) {
207
+ // Nothing is read here. Either it is source text that simply has no
208
+ // reading — punctuation, a Latin word — and is written as the author wrote
209
+ // it, or it is pinyin orthography with no source behind it at all: the
210
+ // space between two words, and the hyphen in `gāngān-jìngjìng`. Writing
211
+ // those into the hanzi would annotate 干干净净 as 干干-净净.
212
+ return source === undefined ? "" : escape(source);
213
+ }
214
+ if (source === undefined) {
215
+ // A reading whose piece never named what it reads. Nothing a conversion
216
+ // builds looks like this — every group is opened by a piece carrying its
217
+ // characters — but {@link toAnnotatedHtml} takes pieces from the caller,
218
+ // and there is no base to hang an annotation on, so it is written as the
219
+ // plain markup it would have got from {@link toHtml}.
220
+ return pieces.map((piece) => writePiece(piece, options)).join("");
221
+ }
222
+ const reading = pieces.map((piece) => writePiece(piece, options)).join("");
223
+ // The tag goes on the <ruby> rather than around the base, and the syllables
224
+ // inside the <rt> override it with their own. `<rb>` would be the obvious
225
+ // place for it and is not an element any more: the WHATWG parser drops it,
226
+ // leaving the base as bare text, so writing one would mean emitting markup
227
+ // that no browser keeps.
228
+ const opening = lang ? `<ruby lang="${BASE_TAG}">` : "<ruby>";
229
+ return `${opening}${escape(source)}<rp>(</rp><rt>${reading}</rt><rp>)</rp></ruby>`;
230
+ }
231
+ /**
232
+ * Mark up a converted text as hanzi with its reading above, syllable by
233
+ * syllable.
234
+ *
235
+ * The output every other mode cannot give: `convert` and {@link toHtml} write
236
+ * the pinyin *instead of* the hanzi, and a learner's text, a subtitle and a
237
+ * dictionary entry all want both at once. The markup is `<ruby>`, which every
238
+ * current browser lays out natively and which degrades to parentheses through
239
+ * `<rp>` where it is not supported.
240
+ *
241
+ * ```html
242
+ * <ruby lang="zh">银<rp>(</rp><rt><span …>yín</span></rt><rp>)</rp></ruby>
243
+ * ```
244
+ *
245
+ * Each `<rt>` holds whatever {@link toHtml} would have written for those
246
+ * syllables, so tone classes and uncertainty marking work inside an annotation
247
+ * exactly as they do outside one.
248
+ *
249
+ * **A base is not always one character.** 玩儿 is two characters and the one
250
+ * syllable `wánr`, and a read number reverses on the way — 95% is
251
+ * `bǎifēnzhījiǔshíwǔ` — so both are annotated whole. Splitting them per
252
+ * character is what everything that emits ruby from a pinyin library gets
253
+ * wrong, and it is why {@link ConvertedPiece.source} exists.
254
+ */
255
+ export function toAnnotatedHtml(pieces, options = {}) {
256
+ return annotationsOf(pieces)
257
+ .map((annotation) => writeAnnotation(annotation, options))
258
+ .join("");
259
+ }
260
+ /**
261
+ * Convert hanzi to pinyin as HTML, hanzi and reading together.
262
+ *
263
+ * {@link toAnnotatedHtml} applied to a fresh conversion; see it for what the
264
+ * markup is and why a base is not always one character.
265
+ */
266
+ export function convertToAnnotatedHtml(dictionary, text, options = {}) {
267
+ return toAnnotatedHtml(convertPieces(dictionary, text, options), options);
268
+ }
125
269
  /**
126
270
  * Convert hanzi to pinyin as HTML, one element per syllable.
127
271
  *
@@ -1 +1 @@
1
- {"version":3,"file":"html.js","sourceRoot":"","sources":["../../src/format/html.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"html.js","sourceRoot":"","sources":["../../src/format/html.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,156 @@
1
+ import { type ConvertOptions } from "../decode/convert.js";
2
+ import type { Dictionary } from "../dictionary/dictionary.js";
3
+ import { type Syllable } from "../syllable/syllable.js";
4
+ /**
5
+ * What one syllable of a typed transcription turned out to be.
6
+ *
7
+ * `toneless` is deliberately separate from `tone`: {@link Syllable.tone} says
8
+ * whether a tone was written at all, so "you left the tones off" is a different
9
+ * thing to report than "you wrote the wrong one", and only the second is a
10
+ * mistake about the language. {@link CheckOptions.tones} decides which of them
11
+ * counts against the score.
12
+ */
13
+ export type PinyinVerdict = "correct" | "toneless" | "tone" | "wrong" | "missing" | "extra";
14
+ /**
15
+ * Where a typed syllable falls against the word boundaries the reading has.
16
+ *
17
+ * `split` is a word written as two — `yín háng` for 银行 — and `joined` is two
18
+ * words run into one. Both are reported whatever {@link CheckOptions.spacing}
19
+ * says, so an application can show them without having to grade them.
20
+ */
21
+ export type SpacingVerdict = "correct" | "split" | "joined";
22
+ /**
23
+ * Whether one dimension of the writing has to be right for a perfect score.
24
+ *
25
+ * `optional` still reports the verdict and counts the syllable correct anyway;
26
+ * `required` counts it wrong. Both dimensions this applies to — tones and word
27
+ * spacing — are things an exercise may or may not be teaching yet.
28
+ */
29
+ export type CheckRequirement = "optional" | "required";
30
+ /**
31
+ * How a transcription should be checked.
32
+ *
33
+ * Every {@link ConvertOptions} field is accepted and passed to the conversion
34
+ * the answer is read from, which is what `readings` is worth having here for:
35
+ * an exercise knows which sense of 长 its own sentence uses, where the decoder
36
+ * can only weigh the evidence.
37
+ */
38
+ export interface CheckOptions extends ConvertOptions {
39
+ /** Whether a tone has to be written. Defaults to `optional`. */
40
+ readonly tones?: CheckRequirement;
41
+ /**
42
+ * Whether the word spacing has to be right. Defaults to `optional`.
43
+ *
44
+ * Off by default because the 分词连写 this grades against is not a complete
45
+ * 正词法 implementation — see [orthography](../../docs/orthography/) for
46
+ * where it stops — so a learner can write a word the standard writes
47
+ * differently from the way this does. On for an exercise teaching the
48
+ * spacing, where getting it right is the point.
49
+ */
50
+ readonly spacing?: CheckRequirement;
51
+ }
52
+ /**
53
+ * One syllable of the check: what was expected there, and what was typed.
54
+ */
55
+ export interface CheckedSyllable {
56
+ readonly verdict: PinyinVerdict;
57
+ /**
58
+ * Where this syllable falls against the word boundaries, where both a reading
59
+ * and a typed syllable met here.
60
+ *
61
+ * A separate axis from {@link CheckedSyllable.verdict}, because it is a
62
+ * separate mistake: `yín háng` reads 银行 perfectly and writes it as two
63
+ * words. Undefined for a syllable that was only expected or only typed.
64
+ */
65
+ readonly spacing: SpacingVerdict | undefined;
66
+ /** Whether this counts as right in {@link PinyinCheck.score}. */
67
+ readonly isCorrect: boolean;
68
+ /** The reading expected here, or undefined where nothing was expected. */
69
+ readonly expected: Syllable | undefined;
70
+ /**
71
+ * What was typed here, parsed, or undefined where nothing was typed.
72
+ *
73
+ * Also undefined for something typed that is not a syllable at all, which
74
+ * {@link CheckedSyllable.text} still reports as written.
75
+ */
76
+ readonly actual: Syllable | undefined;
77
+ /** What was typed here, exactly as written, or empty where nothing was. */
78
+ readonly text: string;
79
+ /**
80
+ * The characters the expected syllable reads.
81
+ *
82
+ * Undefined where a syllable reads on into the characters named before it,
83
+ * exactly as {@link ConvertedPiece.source} is, and where nothing was
84
+ * expected at all.
85
+ */
86
+ readonly source: string | undefined;
87
+ /**
88
+ * Where those characters start, in code points from the start of the text.
89
+ *
90
+ * What highlighting the mistake in the text itself needs, and counted the way
91
+ * {@link import("../decode/segment.js").Segment.at} counts.
92
+ */
93
+ readonly at: number | undefined;
94
+ }
95
+ /**
96
+ * A typed transcription marked against the text it was written for.
97
+ */
98
+ export interface PinyinCheck {
99
+ /** One entry per syllable expected or typed, in order. */
100
+ readonly syllables: readonly CheckedSyllable[];
101
+ /** Whether every syllable counted as correct. */
102
+ readonly isCorrect: boolean;
103
+ /**
104
+ * The share of the syllables that counted as correct, from 0 to 1.
105
+ *
106
+ * Over the entries reported rather than over the expected reading, so that
107
+ * inventing a syllable costs as much as dropping one: an answer with an extra
108
+ * syllable in it has more entries than the reading has syllables.
109
+ */
110
+ readonly score: number;
111
+ /** The reading that was expected, as the conversion writes it. */
112
+ readonly reading: readonly Syllable[];
113
+ }
114
+ /**
115
+ * Mark a typed pinyin transcription against the text it was written for.
116
+ *
117
+ * ```ts
118
+ * check(dictionary, "北京", "bei3jing3").syllables.map((one) => one.verdict);
119
+ * // ["correct", "tone"]
120
+ * ```
121
+ *
122
+ * **The point of doing this with a dictionary is being fair in the ways a
123
+ * string comparison cannot be**, and every one of these is a learner being
124
+ * marked wrong for being right:
125
+ *
126
+ * - **Either notation, mixed freely.** `běi` and `bei3` are the same syllable
127
+ * and both parse, so `bei3jīng` is not a spelling mistake.
128
+ * - **A reading the decoder itself was unsure of.** Where another reading of
129
+ * the same character was there for the taking, it is accepted — see
130
+ * {@link rejectedReadings}. 银行 is not one of those: the word settles both
131
+ * syllables, so `yínxíng` really is wrong.
132
+ * - **Sandhi either way.** 你好 is written `nǐ hǎo` and said `ní hǎo`, and 不是
133
+ * is `bú shì` written and `bù shì` underneath. Both pass.
134
+ * - **Tones written or not.** A syllable typed with no tone is `toneless`
135
+ * rather than `tone`, and {@link CheckOptions.tones} says whether that counts
136
+ * against the score. A neutral tone typed without a mark is simply correct,
137
+ * because that is how pinyin writes it.
138
+ * - **Apostrophes.** Not a word boundary and not a sound: `nǐhǎo`, `nǐ hǎo` and
139
+ * `nǐ'hǎo` all read the same, and check the same.
140
+ *
141
+ * Word spacing is graded on its own axis, reported as
142
+ * {@link CheckedSyllable.spacing} and counted only where
143
+ * {@link CheckOptions.spacing} asks for it — and tolerant in the same spirit,
144
+ * since 分词连写 and the words the dictionary knows are two conventions this
145
+ * package writes and a learner may have been taught either.
146
+ *
147
+ * What comes back is one {@link CheckedSyllable} per syllable expected or
148
+ * typed, in order, each naming the characters it reads and where they are, so
149
+ * that a mistake can be shown against the text rather than against the answer.
150
+ *
151
+ * This is {@link import("../accuracy/score.js").scoreCase} — the scorer that
152
+ * marks the decoder against the gold corpus — pointed at a person instead, and
153
+ * it costs three conversions of the text.
154
+ */
155
+ export declare function check(dictionary: Dictionary, text: string, typed: string, options?: CheckOptions): PinyinCheck;
156
+ //# sourceMappingURL=check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/grade/check.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,cAAc,EAGpB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,yBAAyB,CAAC;AAGjC;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GACrB,SAAS,GACT,UAAU,GACV,MAAM,GACN,OAAO,GACP,SAAS,GACT,OAAO,CAAC;AAEZ;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE5D;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,UAAU,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD,gEAAgE;IAChE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,SAAS,CAAC;IAC7C,iEAAiE;IACjE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,CAAC;IACtC,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,EAAE,SAAS,eAAe,EAAE,CAAC;IAC/C,iDAAiD;IACjD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,CAAC;CACvC;AAqaD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,KAAK,CACnB,UAAU,EAAE,UAAU,EACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,YAAiB,GACzB,WAAW,CAuBb"}