@kensio/pinyinjs 1.6.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +211 -5
  2. package/data/core.entries +393 -392
  3. package/data/core.freq +0 -0
  4. package/data/core.keys +1 -0
  5. package/data/full.entries +10220 -10220
  6. package/data/manifest.json +12 -12
  7. package/data/script.map +7 -7
  8. package/data/standard.entries +957 -956
  9. package/data/standard.freq +0 -0
  10. package/data/standard.keys +1 -0
  11. package/dist/cli/arguments.d.ts +38 -0
  12. package/dist/cli/arguments.d.ts.map +1 -1
  13. package/dist/cli/arguments.js +46 -0
  14. package/dist/cli/arguments.js.map +1 -1
  15. package/dist/cli/commands.d.ts.map +1 -1
  16. package/dist/cli/commands.js +207 -1
  17. package/dist/cli/commands.js.map +1 -1
  18. package/dist/cli/help.d.ts.map +1 -1
  19. package/dist/cli/help.js +5 -0
  20. package/dist/cli/help.js.map +1 -1
  21. package/dist/decode/convert.d.ts +16 -0
  22. package/dist/decode/convert.d.ts.map +1 -1
  23. package/dist/decode/convert.js +3 -28
  24. package/dist/decode/convert.js.map +1 -1
  25. package/dist/decode/runs.d.ts +11 -0
  26. package/dist/decode/runs.d.ts.map +1 -1
  27. package/dist/decode/runs.js +19 -0
  28. package/dist/decode/runs.js.map +1 -1
  29. package/dist/decode/segment.d.ts +66 -0
  30. package/dist/decode/segment.d.ts.map +1 -0
  31. package/dist/decode/segment.js +79 -0
  32. package/dist/decode/segment.js.map +1 -0
  33. package/dist/dictionary/artifact.d.ts +10 -0
  34. package/dist/dictionary/artifact.d.ts.map +1 -1
  35. package/dist/dictionary/artifact.js +17 -0
  36. package/dist/dictionary/artifact.js.map +1 -1
  37. package/dist/dictionary/assertions.d.ts.map +1 -1
  38. package/dist/dictionary/assertions.js +38 -0
  39. package/dist/dictionary/assertions.js.map +1 -1
  40. package/dist/dictionary/corpus-mass.d.ts +68 -0
  41. package/dist/dictionary/corpus-mass.d.ts.map +1 -0
  42. package/dist/dictionary/corpus-mass.js +163 -0
  43. package/dist/dictionary/corpus-mass.js.map +1 -0
  44. package/dist/dictionary/dictionary.d.ts +56 -0
  45. package/dist/dictionary/dictionary.d.ts.map +1 -1
  46. package/dist/dictionary/dictionary.js +116 -1
  47. package/dist/dictionary/dictionary.js.map +1 -1
  48. package/dist/dictionary/erhua.d.ts +4 -0
  49. package/dist/dictionary/erhua.d.ts.map +1 -1
  50. package/dist/dictionary/erhua.js +6 -0
  51. package/dist/dictionary/erhua.js.map +1 -1
  52. package/dist/dictionary/merge.d.ts.map +1 -1
  53. package/dist/dictionary/merge.js +16 -8
  54. package/dist/dictionary/merge.js.map +1 -1
  55. package/dist/dictionary/tiers.d.ts +2 -1
  56. package/dist/dictionary/tiers.d.ts.map +1 -1
  57. package/dist/dictionary/tiers.js +77 -11
  58. package/dist/dictionary/tiers.js.map +1 -1
  59. package/dist/grade/check.d.ts +156 -0
  60. package/dist/grade/check.d.ts.map +1 -0
  61. package/dist/grade/check.js +356 -0
  62. package/dist/grade/check.js.map +1 -0
  63. package/dist/index.d.ts +11 -1
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +5 -0
  66. package/dist/index.js.map +1 -1
  67. package/dist/search/candidates.d.ts +84 -0
  68. package/dist/search/candidates.d.ts.map +1 -0
  69. package/dist/search/candidates.js +290 -0
  70. package/dist/search/candidates.js.map +1 -0
  71. package/dist/search/match.d.ts +72 -0
  72. package/dist/search/match.d.ts.map +1 -0
  73. package/dist/search/match.js +419 -0
  74. package/dist/search/match.js.map +1 -0
  75. package/dist/search/query.d.ts +49 -0
  76. package/dist/search/query.d.ts.map +1 -0
  77. package/dist/search/query.js +115 -0
  78. package/dist/search/query.js.map +1 -0
  79. package/dist/search/reverse-index.d.ts +141 -0
  80. package/dist/search/reverse-index.d.ts.map +1 -0
  81. package/dist/search/reverse-index.js +356 -0
  82. package/dist/search/reverse-index.js.map +1 -0
  83. package/dist/sources/unihan.d.ts +9 -0
  84. package/dist/sources/unihan.d.ts.map +1 -1
  85. package/dist/sources/unihan.js +12 -1
  86. package/dist/sources/unihan.js.map +1 -1
  87. package/package.json +2 -3
  88. package/CHANGELOG.md +0 -634
package/README.md CHANGED
@@ -35,6 +35,11 @@ wo3-xiang3-xue2-zhong1wen2
35
35
  $ pinyinjs script 我们后来发现了头发问题 --to zh-Hant
36
36
  我們後來發現了頭髮問題
37
37
 
38
+ $ pinyinjs check 银行 yínxíng
39
+ 银行 yínháng 50%
40
+ 银 yín yín correct
41
+ 行 háng xíng wrong
42
+
38
43
  $ pinyinjs explain 银行
39
44
  银行 yínháng
40
45
  yín locked
@@ -43,6 +48,11 @@ $ pinyinjs explain 银行
43
48
  $ pinyinjs lookup 头发
44
49
  头发 tóu fa n
45
50
 
51
+ $ pinyinjs match --query bjdx 北京大学 我在北京大学学中文 上海大学
52
+ [北京大学] 7.00
53
+ 我在[北京大学]学中文 6.33
54
+ 上海大学 no match
55
+
46
56
  $ pinyinjs syllable nǐhǎo
47
57
  nǐhǎo nǐ hǎo
48
58
  nǐ n + i, tone 3 nǐ ni3 ni³
@@ -60,6 +70,9 @@ běijīng běijīng ㄅㄟˇ ㄐㄧㄥ pei³-ching¹ běijīng beeij
60
70
  | `convert` | hanzi to pinyin |
61
71
  | `html` | the same, as HTML |
62
72
  | `annotate` | hanzi with its pinyin above, as ruby HTML |
73
+ | `segment` | split text into words |
74
+ | `match` | filter text by a pinyin query, best first |
75
+ | `check` | mark typed pinyin against the text |
63
76
  | `slug` | hanzi to a URL-safe slug |
64
77
  | `script` | 简体 ↔ 繁體 conversion |
65
78
  | `explain` | each syllable, how settled it was, and what it beat |
@@ -144,9 +157,9 @@ Load the dictionary once and keep it; it is immutable and safe to share.
144
157
 
145
158
  | Tier | Entries | Download (brotli) | Contains |
146
159
  | ---------- | ------: | ----------------: | ---------------------- |
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 |
160
+ | `core` | 16,970 | 70 KB | single characters only |
161
+ | `standard` | 66,970 | 377 KB | the most common words |
162
+ | `full` | 461,555 | 2,378 KB | every word |
150
163
 
151
164
  `full` is the default. The tiers are nested, so a page can load `standard`
152
165
  first, convert with it, and reload as `full` arrives.
@@ -389,6 +402,192 @@ ruby rt {
389
402
  `ConvertedPiece.source` is what makes that possible: the characters each piece
390
403
  reads, or undefined where it reads on into the ones before it.
391
404
 
405
+ ## Segment
406
+
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 行长
409
+ say which. `segment` returns that answer instead of throwing it away.
410
+
411
+ ```ts
412
+ import { segment } from "@kensio/pinyinjs";
413
+
414
+ segment(dictionary, "南京市长江大桥").map((found) => found.text);
415
+ // ["南京市", "长江", "大桥"], not ["南京", "市长", "江大桥"]
416
+
417
+ const found = segment(dictionary, "我要去北京。");
418
+ found.map((one) => one.text); // ["我", "要", "去", "北京", "。"]
419
+ found[3]?.partOfSpeech; // "ns", jieba's tag
420
+ found[3]?.isProperNoun; // true
421
+ found[3]?.at; // 3, in code points from the start of the text
422
+ ```
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
427
+ `isKnown` for the stretches the dictionary recognised.
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
432
+ words are.
433
+
434
+ ## Match a pinyin query
435
+
436
+ A search box on a Latin keyboard, filtering Chinese text. `match` returns where
437
+ a query landed, or undefined where it did not land at all.
438
+
439
+ ```ts
440
+ import { match } from "@kensio/pinyinjs";
441
+
442
+ match(dictionary, "北京大学", "bjdx")?.ranges; // [{ at: 0, length: 4 }]
443
+ match(dictionary, "北京大学", "beijing")?.ranges; // [{ at: 0, length: 2 }]
444
+ match(dictionary, "北京大学", "nanjing"); // undefined
445
+ ```
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`.
451
+
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:
456
+
457
+ ```ts
458
+ match(dictionary, "银行", "yh")?.score; // 7, and 银行 is yínháng
459
+ match(dictionary, "银行", "yx")?.score; // 5 — a reading 行 has, but not here
460
+ ```
461
+
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.
467
+
468
+ The `core` tier is enough, so a page that never loads a word list can still
469
+ filter. See [matching](docs/matching/).
470
+
471
+ ## Go the other way: pinyin to hanzi
472
+
473
+ `candidates` answers a pinyin query with the words it could be spelling, and
474
+ `homophonesOf` lists the words read exactly like one you already have. Both run
475
+ off a reverse index derived from the dictionary in memory.
476
+
477
+ ```ts
478
+ import { candidates, homophonesOf, ReverseIndex } from "@kensio/pinyinjs";
479
+
480
+ const index = ReverseIndex.of(dictionary);
481
+
482
+ candidates(index, "shi", { limit: 5 }); // ["是", "时", "事", "使", "市"]
483
+ candidates(index, "yinhang"); // ["銀行", "银行", "引吭", "引航", "印航"]
484
+ candidates(index, "yínháng"); // ["銀行", "银行"], narrowed by tone
485
+ homophonesOf(index, "长城"); // ["長城", "長程", "长程", "常程"]
486
+ ```
487
+
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
+
493
+ **Nothing is downloaded for it.** Shipping a reverse index of the `full` tier
494
+ 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
496
+ `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.
499
+
500
+ Candidates come back likeliest first, because a posting is a dictionary position
501
+ and a position indexes the frequency table. Tones may be written or left off,
502
+ `v` and `u:` reach ü, and the r of 儿化 is optional, so 玩儿 answers to `wanr`
503
+ 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
505
+ [candidates](docs/candidates/).
506
+
507
+ ## Check what somebody typed
508
+
509
+ `check` marks a typed pinyin transcription against the Chinese it was written
510
+ for, syllable by syllable.
511
+
512
+ ```ts
513
+ import { check } from "@kensio/pinyinjs";
514
+
515
+ const marked = check(dictionary, "银行", "yínxíng");
516
+ marked.syllables.map((one) => one.verdict); // ["correct", "wrong"]
517
+ marked.syllables[1]?.source; // "行", the character that was misread
518
+ marked.score; // 0.5
519
+ ```
520
+
521
+ 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
523
+ against the answer:
524
+
525
+ | Verdict | Means |
526
+ | ---------- | ---------------------------------------------- |
527
+ | `correct` | right syllable, right tone |
528
+ | `toneless` | right syllable, no tone written |
529
+ | `tone` | right syllable, wrong tone |
530
+ | `wrong` | wrong syllable |
531
+ | `missing` | a syllable of the reading that was not typed |
532
+ | `extra` | a syllable typed that the reading does not use |
533
+
534
+ **The point is being fair in the ways a string comparison cannot be.** All of
535
+ these are a learner being marked wrong for being right, and all of them pass:
536
+
537
+ ```ts
538
+ check(dictionary, "北京", "bei3jīng").isCorrect; // true, either notation, mixed
539
+ check(dictionary, "行", "háng").isCorrect; // true, the decoder was guessing too
540
+ check(dictionary, "你好", "ní hǎo").isCorrect; // true, said with sandhi
541
+ check(dictionary, "不是", "bù shì").isCorrect; // true, written without it
542
+ check(dictionary, "海鸥", "hǎiōu").isCorrect; // true, an apostrophe is not a sound
543
+ check(dictionary, "我的书", "wǒ de shū").isCorrect; // true, pinyin marks no neutral tone
544
+ ```
545
+
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/).
549
+
550
+ ### Tones and word spacing
551
+
552
+ Two axes an exercise may or may not be teaching yet, each reported always and
553
+ each counted only when asked for.
554
+
555
+ Tones left off are `toneless` rather than `tone`, because `Syllable.tone` knows
556
+ the difference:
557
+
558
+ ```ts
559
+ check(dictionary, "北京", "bei jing").isCorrect; // true
560
+ check(dictionary, "北京", "bei jing", { tones: "required" }).isCorrect; // false
561
+ ```
562
+
563
+ 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
565
+ as two words.
566
+
567
+ ```ts
568
+ const split = check(dictionary, "银行", "yín háng");
569
+ split.syllables.map((one) => one.spacing); // ["correct", "split"]
570
+ split.isCorrect; // true, spacing is not counted by default
571
+ check(dictionary, "银行", "yín háng", { spacing: "required" }).isCorrect; // false
572
+ ```
573
+
574
+ 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
576
+ dictionary knows are two conventions this package writes, and a learner may have
577
+ been taught either:
578
+
579
+ ```ts
580
+ const graded = { spacing: "required" } as const;
581
+ check(dictionary, "他看了", "tā kànle", graded).isCorrect; // true, 分词连写
582
+ check(dictionary, "他看了", "tā kàn le", graded).isCorrect; // true, the words
583
+ check(dictionary, "干干净净", "gāngān jìngjìng", graded).isCorrect; // true, a hyphen is a boundary
584
+ check(dictionary, "我要去北京。", "wǒyàoqùběijīng", graded).isCorrect; // false
585
+ ```
586
+
587
+ Takes every `convert` option besides, so an exercise that knows which sense of 长
588
+ its own sentence uses can say so with `readings`. More in
589
+ [checking](docs/checking/).
590
+
392
591
  ## Look words up
393
592
 
394
593
  ```ts
@@ -639,6 +838,12 @@ This is orthography and not translation — 软件 becomes 軟件, never 軟體.
639
838
  | `convertPieces(dictionary, text, ...)` | the same, per syllable, with confidence |
640
839
  | `convertToHtml(dictionary, text, ...)` | the same, as HTML |
641
840
  | `convertToAnnotatedHtml(dictionary, text, ...)` | hanzi and pinyin together, as ruby HTML |
841
+ | `segment(dictionary, text)` | split text into words |
842
+ | `match(dictionary, haystack, query)` | where a pinyin query matches a text |
843
+ | `candidates(index, query, options?)` | pinyin → the hanzi it could be |
844
+ | `homophonesOf(index, word, options?)` | the words read exactly like this one |
845
+ | `ReverseIndex.of(dictionary)` | derive the reading → words index |
846
+ | `check(dictionary, text, typed, options?)` | mark typed pinyin against the text |
642
847
  | `slug(dictionary, text, options?)` | hanzi → a URL-safe slug |
643
848
  | `joinPieces(pieces)` / `toHtml(pieces)` | render pieces |
644
849
  | `isUncertain(confidence)` | was this syllable a guess? |
@@ -654,8 +859,9 @@ This is orthography and not translation — 软件 becomes 軟件, never 軟體.
654
859
  | `convertGreedily(...)` | the old longest-match decoder, kept as a baseline |
655
860
 
656
861
  Types (`Syllable`, `Tone`, `ConvertOptions`, `ConvertedPiece`,
657
- `ReadingConfidence`, `HtmlOptions`, `WordEntry`, `Tier`, `Locale`, `Script`) are
658
- exported alongside them.
862
+ `ReadingConfidence`, `HtmlOptions`, `Segment`, `PinyinMatch`, `MatchRange`,
863
+ `ReadingConfidence`, `HtmlOptions`, `Segment`, `PinyinCheck`, `CheckedSyllable`,
864
+ `WordEntry`, `Tier`, `Locale`, `Script`) are exported alongside them.
659
865
 
660
866
  ## Development
661
867