hanzi-chai 0.3.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 (52) hide show
  1. package/dist/affine.d.ts +8 -0
  2. package/dist/affine.js +153 -0
  3. package/dist/assembly.d.ts +63 -0
  4. package/dist/assembly.js +286 -0
  5. package/dist/bezier.d.ts +74 -0
  6. package/dist/bezier.js +140 -0
  7. package/dist/classifier.d.ts +109 -0
  8. package/dist/classifier.js +108 -0
  9. package/dist/component.d.ts +115 -0
  10. package/dist/component.js +278 -0
  11. package/dist/compound.d.ts +64 -0
  12. package/dist/compound.js +652 -0
  13. package/dist/config.d.ts +236 -0
  14. package/dist/config.js +48 -0
  15. package/dist/data/cjk.txt +20902 -0
  16. package/dist/data/data/cjk.txt +20902 -0
  17. package/dist/data/data/dictionary.txt +69429 -0
  18. package/dist/data/data/frequency.txt +263137 -0
  19. package/dist/data/data/key_distribution.txt +46 -0
  20. package/dist/data/data/pair_equivalence.txt +2116 -0
  21. package/dist/data/data/repertoire.json.deflate +0 -0
  22. package/dist/data/data/tygf.txt +8105 -0
  23. package/dist/data/dictionary.txt +69429 -0
  24. package/dist/data/frequency.txt +263137 -0
  25. package/dist/data/key_distribution.txt +46 -0
  26. package/dist/data/pair_equivalence.txt +2116 -0
  27. package/dist/data/repertoire.json.deflate +0 -0
  28. package/dist/data/tygf.txt +8105 -0
  29. package/dist/data.d.ts +195 -0
  30. package/dist/data.js +18 -0
  31. package/dist/degenerator.d.ts +21 -0
  32. package/dist/degenerator.js +134 -0
  33. package/dist/element.d.ts +56 -0
  34. package/dist/element.js +225 -0
  35. package/dist/equivalence.d.ts +21 -0
  36. package/dist/equivalence.js +74 -0
  37. package/dist/index.d.ts +45 -0
  38. package/dist/index.js +126 -0
  39. package/dist/index.mjs +3553 -0
  40. package/dist/mathjs.d.ts +10 -0
  41. package/dist/mathjs.js +9 -0
  42. package/dist/repertoire.d.ts +75 -0
  43. package/dist/repertoire.js +406 -0
  44. package/dist/selector.d.ts +110 -0
  45. package/dist/selector.js +310 -0
  46. package/dist/topology.d.ts +41 -0
  47. package/dist/topology.js +209 -0
  48. package/dist/transformer.d.ts +18 -0
  49. package/dist/transformer.js +111 -0
  50. package/dist/utils.d.ts +107 -0
  51. package/dist/utils.js +546 -0
  52. package/package.json +34 -0
@@ -0,0 +1,652 @@
1
+ import { affineMerge } from "./affine.js";
2
+ import { InvalidGlyphError } from "./component.js";
3
+ import { range, sortBy } from "lodash-es";
4
+ import { classifier } from "./classifier.js";
5
+ import { isMerge } from "./config.js";
6
+ export const getGlyphBoundingBox = (glyph) => {
7
+ let [xmin, ymin, xmax, ymax] = [
8
+ Number.POSITIVE_INFINITY,
9
+ Number.POSITIVE_INFINITY,
10
+ Number.NEGATIVE_INFINITY,
11
+ Number.NEGATIVE_INFINITY,
12
+ ];
13
+ for (const { start, curveList } of glyph) {
14
+ let [x, y] = start;
15
+ xmin = Math.min(xmin, x);
16
+ ymin = Math.min(ymin, y);
17
+ xmax = Math.max(xmax, x);
18
+ ymax = Math.max(ymax, y);
19
+ for (const { command, parameterList } of curveList) {
20
+ switch (command) {
21
+ case "h":
22
+ x += parameterList[0];
23
+ break;
24
+ case "v":
25
+ y += parameterList[0];
26
+ break;
27
+ case "a":
28
+ xmin = Math.min(xmin, x - parameterList[0]);
29
+ xmax = Math.max(xmax, x + parameterList[0]);
30
+ ymax = Math.max(ymax, y + 2 * parameterList[0]);
31
+ break;
32
+ default: {
33
+ const [_x1, _y1, _x2, _y2, x3, y3] = parameterList;
34
+ x += x3;
35
+ y += y3;
36
+ break;
37
+ }
38
+ }
39
+ xmin = Math.min(xmin, x);
40
+ ymin = Math.min(ymin, y);
41
+ xmax = Math.max(xmax, x);
42
+ ymax = Math.max(ymax, y);
43
+ }
44
+ }
45
+ const bb = { x: [xmin, xmax], y: [ymin, ymax] };
46
+ return bb;
47
+ };
48
+ /**
49
+ * 将复合体递归渲染为 SVG 图形
50
+ *
51
+ * @param compound - 复合体
52
+ * @param repertoire - 原始字符集
53
+ *
54
+ * @returns SVG 图形
55
+ * @throws InvalidGlyphError 无法渲染
56
+ */
57
+ export const recursiveRenderCompound = (compound, repertoire, glyphCache = new Map()) => {
58
+ const glyphs = [];
59
+ for (const char of compound.operandList) {
60
+ const glyph = repertoire[char]?.glyph;
61
+ if (glyph === undefined)
62
+ return new InvalidGlyphError();
63
+ if (glyph.type === "basic_component") {
64
+ let box = glyphCache.get(char)?.box;
65
+ if (box === undefined) {
66
+ box = getGlyphBoundingBox(glyph.strokes);
67
+ glyphCache.set(char, { strokes: glyph.strokes, box: box });
68
+ }
69
+ glyphs.push({ strokes: glyph.strokes, box });
70
+ }
71
+ else {
72
+ const cache = glyphCache.get(char);
73
+ if (cache !== undefined) {
74
+ glyphs.push(cache);
75
+ continue;
76
+ }
77
+ const rendered = recursiveRenderCompound(glyph, repertoire, glyphCache);
78
+ if (rendered instanceof Error)
79
+ return rendered;
80
+ glyphs.push(rendered);
81
+ glyphCache.set(char, rendered);
82
+ }
83
+ }
84
+ return affineMerge(compound, glyphs);
85
+ };
86
+ /**
87
+ * 将复合体递归渲染为 SVG 图形
88
+ *
89
+ * @param compound - 复合体
90
+ * @param repertoire - 原始字符集
91
+ *
92
+ * @returns SVG 图形
93
+ * @throws InvalidGlyphError 无法渲染
94
+ */
95
+ export const recursiveRenderStrokeSequence = (compound, repertoire, sequenceCache = new Map()) => {
96
+ const sequences = [];
97
+ for (const char of compound.operandList) {
98
+ const glyph = repertoire[char]?.glyph;
99
+ if (glyph === undefined)
100
+ return new InvalidGlyphError();
101
+ if (glyph.type === "basic_component") {
102
+ sequences.push(glyph.strokes.map((x) => classifier[x.feature]).join(""));
103
+ }
104
+ else {
105
+ const cache = sequenceCache.get(char);
106
+ if (cache !== undefined) {
107
+ sequences.push(cache);
108
+ continue;
109
+ }
110
+ const rendered = recursiveRenderStrokeSequence(glyph, repertoire, sequenceCache);
111
+ if (rendered instanceof Error)
112
+ return rendered;
113
+ sequences.push(rendered);
114
+ sequenceCache.set(char, rendered);
115
+ }
116
+ }
117
+ const { order } = compound;
118
+ if (order === undefined) {
119
+ return sequences.join("");
120
+ }
121
+ else {
122
+ const merged = [];
123
+ for (const { index, strokes } of order) {
124
+ const seq = sequences[index];
125
+ if (seq === undefined)
126
+ continue;
127
+ if (strokes === 0) {
128
+ merged.push(seq);
129
+ }
130
+ else {
131
+ merged.push(seq.slice(0, strokes));
132
+ sequences[index] = seq.slice(strokes);
133
+ }
134
+ }
135
+ return merged.join("");
136
+ }
137
+ };
138
+ /**
139
+ * 对字符集进行拓扑排序,得到复合体的拆分顺序
140
+ *
141
+ * @param repertoire - 字符集
142
+ *
143
+ * @returns 拓扑排序后的复合体
144
+ *
145
+ * @remarks 这个实现目前比较低效,需要改进
146
+ */
147
+ export const topologicalSort = (repertoire, required, config) => {
148
+ let compounds = new Map();
149
+ for (let i = 0; i !== 10; ++i) {
150
+ const thisLevelCompound = new Map();
151
+ for (const [name, character] of Object.entries(repertoire)) {
152
+ if (!required.has(name) || compounds.get(name))
153
+ continue;
154
+ const { glyph } = character;
155
+ if (glyph === undefined || glyph.type !== "compound")
156
+ continue;
157
+ const wellKnown = glyph.operandList.every((x) => repertoire[x]?.glyph?.type === "basic_component" ||
158
+ compounds.get(x) !== undefined);
159
+ const isRoot = config.roots.has(name) && !config.optionalRoots.has(name);
160
+ if (wellKnown || isRoot) {
161
+ thisLevelCompound.set(name, character);
162
+ }
163
+ }
164
+ compounds = new Map([...compounds, ...thisLevelCompound]);
165
+ }
166
+ return compounds;
167
+ };
168
+ const sequentialSerializer = (operandResults, glyph) => {
169
+ const rest = {
170
+ operator: glyph.operator,
171
+ operandResults,
172
+ };
173
+ if (glyph.order === undefined) {
174
+ const full = operandResults.flatMap((x) => x.full);
175
+ return { sequence: [...full], full, ...rest };
176
+ }
177
+ const full = [];
178
+ const subsequences = operandResults.map((x) => ({
179
+ rest: x.full,
180
+ taken: 0,
181
+ }));
182
+ for (const { index, strokes } of glyph.order) {
183
+ const data = subsequences[index];
184
+ if (data === undefined) {
185
+ continue;
186
+ }
187
+ if (strokes === 0) {
188
+ full.push(...data.rest);
189
+ data.rest = [];
190
+ }
191
+ else {
192
+ const partitionResult = operandResults[index];
193
+ if ("schemes" in partitionResult) {
194
+ const { best, strokes: totalStrokes } = partitionResult;
195
+ const upperBound = 1 << (totalStrokes - data.taken);
196
+ const lowerBound = 1 << (totalStrokes - data.taken - strokes);
197
+ const toTake = best.scheme.filter((binary) => binary >= lowerBound && binary < upperBound).length;
198
+ full.push(...data.rest.slice(0, toTake));
199
+ data.rest = data.rest.slice(toTake);
200
+ }
201
+ else {
202
+ full.push(...data.rest);
203
+ data.rest = [];
204
+ }
205
+ }
206
+ }
207
+ return { sequence: [...full], full, ...rest };
208
+ };
209
+ const zhenmaSerializer = (operandResults, glyph) => {
210
+ const sequence = [];
211
+ if (glyph.operator === "⿶") {
212
+ sequence.push(...operandResults[1].sequence);
213
+ sequence.push(...operandResults[0].sequence);
214
+ }
215
+ else {
216
+ operandResults.map((x) => x.sequence).forEach((x) => sequence.push(...x));
217
+ }
218
+ return {
219
+ sequence,
220
+ full: [],
221
+ operator: glyph.operator,
222
+ operandResults,
223
+ };
224
+ };
225
+ const regularize = (x, direction) => {
226
+ const result = [];
227
+ const regex = direction === "⿰" ? /[⿰⿲]/ : /[⿱⿳]/;
228
+ for (const part of x) {
229
+ if (result.length < 2 &&
230
+ "operandResults" in part &&
231
+ regex.test(part.operator)) {
232
+ result.push(...part.operandResults);
233
+ }
234
+ else {
235
+ result.push(part);
236
+ }
237
+ }
238
+ return result;
239
+ };
240
+ // eslint-disable-next-line complexity
241
+ const zhangmaSerializer = (operandResults, glyph) => {
242
+ const sequence = [];
243
+ const full = [];
244
+ if (glyph.operator === "⿶" ||
245
+ (glyph.operator === "⿺" && glyph.operandList[0] === "\ue09d")) {
246
+ for (const part of operandResults.reverse())
247
+ full.push(...part.full);
248
+ }
249
+ else {
250
+ for (const part of operandResults)
251
+ full.push(...part.full);
252
+ }
253
+ const getShouMo = (x) => (x.length === 1 ? x : [x[0], x.at(-1)]);
254
+ const regularizedResults = operandResults.length === 2 && /[⿰⿱]/.test(glyph.operator)
255
+ ? regularize(operandResults, glyph.operator)
256
+ : operandResults;
257
+ if (/[⿱⿳]/.test(glyph.operator)) {
258
+ const dieyanIndex = regularizedResults.findIndex((x) => "operandResults" in x && /[⿰⿲]/.test(x.operator));
259
+ const dieyan = regularizedResults.at(dieyanIndex);
260
+ const aboveDieyanSequence = [];
261
+ switch (dieyanIndex) {
262
+ case -1: // 没有叠眼
263
+ for (const part of regularizedResults)
264
+ sequence.push(...part.full);
265
+ break;
266
+ case 0: // 叠眼在开头
267
+ for (const part of dieyan.operandResults)
268
+ sequence.push(...part.full[0]);
269
+ if (regularizedResults.length > 2) {
270
+ sequence.push(regularizedResults[1]?.full[0]);
271
+ sequence.push(regularizedResults.at(-1)?.full.at(-1));
272
+ }
273
+ else {
274
+ sequence.push(...getShouMo(regularizedResults[1].full));
275
+ }
276
+ break;
277
+ default: // 叠眼在中间或末尾
278
+ for (let index = 0; index < dieyanIndex; ++index) {
279
+ aboveDieyanSequence.push(...regularizedResults[index].full);
280
+ }
281
+ sequence.push(...aboveDieyanSequence.slice(0, 2));
282
+ if (dieyanIndex + 1 < regularizedResults.length) {
283
+ // 叠眼在中间
284
+ sequence.push(dieyan.operandResults[0]?.full[0]);
285
+ if (sequence.length === 2) {
286
+ sequence.push(dieyan.operandResults.at(-1)?.full[0]);
287
+ }
288
+ sequence.push(regularizedResults.at(-1)?.full.at(-1));
289
+ }
290
+ else {
291
+ sequence.push(...dieyan.full);
292
+ }
293
+ }
294
+ }
295
+ else if (/[⿰⿲]/.test(glyph.operator)) {
296
+ const left = regularizedResults[0];
297
+ if ("operandResults" in left && /[⿱⿳]/.test(left.operator)) {
298
+ // 左部是叠型
299
+ const dieyanIndex = left.operandResults.findIndex((x) => "operandResults" in x && /[⿰⿲]/.test(x.operator));
300
+ const dieyan = left.operandResults.at(dieyanIndex);
301
+ if (dieyanIndex !== -1) {
302
+ sequence.push(left.operandResults[0]?.full[0]);
303
+ if (dieyanIndex === 0)
304
+ sequence.push(dieyan.operandResults.at(-1)?.full[0]);
305
+ else
306
+ sequence.push(dieyan.operandResults[0]?.full[0]);
307
+ sequence.push(left.operandResults.at(-1)?.full.at(-1));
308
+ }
309
+ }
310
+ if (sequence.length > 0) {
311
+ // 已经处理完左部,直接取右部末码即可
312
+ sequence.push(regularizedResults.at(-1)?.full.at(-1));
313
+ }
314
+ else {
315
+ // 一般情况,左部、中部最多各取首尾两根
316
+ sequence.push(...getShouMo(left.full));
317
+ if (regularizedResults.length > 2) {
318
+ sequence.push(...getShouMo(regularizedResults[1].full));
319
+ }
320
+ // 如果左部和中部已经有 4 根,舍弃一根
321
+ if (sequence.length === 4)
322
+ sequence.pop();
323
+ for (let index = Math.min(2, regularizedResults.length - 1); index < regularizedResults.length; ++index) {
324
+ sequence.push(...regularizedResults[index].full);
325
+ }
326
+ }
327
+ }
328
+ else {
329
+ // 围型:先分解围框,再分解围芯
330
+ sequence.push(...full);
331
+ }
332
+ // 不考虑四角
333
+ return {
334
+ sequence,
335
+ full,
336
+ operator: glyph.operator,
337
+ operandResults: regularizedResults,
338
+ };
339
+ };
340
+ const snow2Serializer = (operandResults, glyph) => {
341
+ const order = glyph.order ?? glyph.operandList.map((_, i) => ({ index: i, strokes: 0 }));
342
+ const sortedOperandResults = sortBy(range(operandResults.length), (i) => order.findIndex((b) => b.index === i)).map((i) => operandResults[i]);
343
+ const first = sortedOperandResults[0];
344
+ const second = sortedOperandResults[1];
345
+ const marker = first.full.length == 1
346
+ ? second.full.length == 1
347
+ ? "q"
348
+ : "e"
349
+ : second.full.length == 1
350
+ ? "w"
351
+ : "r";
352
+ const full = [first.sequence[0], second.sequence[0]];
353
+ const sequence = [...full, marker];
354
+ return {
355
+ sequence,
356
+ full,
357
+ operator: glyph.operator,
358
+ operandResults,
359
+ };
360
+ };
361
+ const limit = (sequence, maximum, config) => {
362
+ let codes = 0; // 记录整体的取码数
363
+ const { roots } = config;
364
+ const final = [];
365
+ for (const x of sequence) {
366
+ if (x === "·") {
367
+ final.push(x);
368
+ continue;
369
+ }
370
+ // FIXME: 这里的规则需要根据实际情况调整
371
+ const length = 1;
372
+ codes += length;
373
+ final.push(x);
374
+ if (codes >= maximum)
375
+ break;
376
+ }
377
+ return final;
378
+ };
379
+ const xkjdSerializer = (operandResults, glyph, config) => {
380
+ const { roots: primaryRoots } = config;
381
+ const order = glyph.order ?? glyph.operandList.map((_, i) => ({ index: i, strokes: 0 }));
382
+ const sequence = [];
383
+ const full = [];
384
+ const sequential = sequentialSerializer(operandResults, glyph, config);
385
+ full.push(...sequential.full);
386
+ if (order[0]?.strokes !== 1) {
387
+ // 第一个书写的部分并不是只写了一笔,此时不考虑笔顺交错,依次取各个部分
388
+ const sortedOperandResults = sortBy(range(operandResults.length), (i) => order.findIndex((b) => b.index === i)).map((i) => operandResults[i]);
389
+ for (const [index, part] of sortedOperandResults.entries()) {
390
+ if (index === 0) {
391
+ let codes = 0; // 记录部分的取码数
392
+ for (const x of part.full) {
393
+ // FIXME: 这里的规则需要根据实际情况调整
394
+ const length = 1;
395
+ if (codes === 1 && length > 1) {
396
+ const equivalent = {
397
+ 土: "1",
398
+ 艹: "2",
399
+ 金: "2",
400
+ 手: "2",
401
+ 十: "3",
402
+ 日: "4",
403
+ 贝: "5",
404
+ };
405
+ sequence.push(equivalent[x] ?? "1");
406
+ }
407
+ else {
408
+ sequence.push(x);
409
+ }
410
+ codes += length;
411
+ if (codes >= 2)
412
+ break;
413
+ }
414
+ // sequence.push("·"); // 二分标记
415
+ }
416
+ else {
417
+ sequence.push(...part.full);
418
+ }
419
+ }
420
+ }
421
+ else {
422
+ // 第一个书写的部分只写了一笔,此时考虑笔顺交错
423
+ sequence.push(...sequential.sequence);
424
+ }
425
+ const finalSequence = limit(sequence, 4, config);
426
+ return {
427
+ sequence: finalSequence,
428
+ full,
429
+ operator: glyph.operator,
430
+ operandResults,
431
+ };
432
+ };
433
+ const shouyouSerializer = (operandResults, glyph, config) => {
434
+ const order = glyph.order ?? glyph.operandList.map((_, i) => ({ index: i, strokes: 0 }));
435
+ const sortedOperandResults = sortBy(range(operandResults.length), (i) => order.findIndex((b) => b.index === i)).map((i) => operandResults[i]);
436
+ const first = sortedOperandResults[0];
437
+ const second = sortedOperandResults[1];
438
+ let sequence;
439
+ const full = sequentialSerializer(operandResults, glyph, config).full;
440
+ if (/[⿰⿲]/.test(glyph.operator) ||
441
+ (glyph.operator === "⿺" && order[0].index === 0))
442
+ sequence = [first.full[0], second.full[0]];
443
+ else {
444
+ sequence = [...full];
445
+ }
446
+ return {
447
+ sequence,
448
+ full,
449
+ operator: glyph.operator,
450
+ operandResults,
451
+ };
452
+ };
453
+ const feihuaSerializer = (operandResults, glyph, config) => {
454
+ const order = glyph.order ?? glyph.operandList.map((_, i) => ({ index: i, strokes: 0 }));
455
+ const sortedOperandResults = sortBy(range(operandResults.length), (i) => order.findIndex((b) => b.index === i)).map((i) => operandResults[i]);
456
+ const sequence = [];
457
+ const first = sortedOperandResults[0];
458
+ const last = sortedOperandResults.at(-1);
459
+ const handleResidual = (sequence, parts) => {
460
+ if (parts.length > 1) {
461
+ for (const x of parts) {
462
+ // @ts-ignore
463
+ sequence.push(...x.full2[0]);
464
+ }
465
+ }
466
+ else {
467
+ const part = parts[0];
468
+ if ("operandResults" in part) {
469
+ for (const x of part.operandResults) {
470
+ // @ts-ignore
471
+ sequence.push(...x.full2[0]);
472
+ }
473
+ }
474
+ else {
475
+ // @ts-ignore
476
+ sequence.push(...part.full2);
477
+ }
478
+ }
479
+ };
480
+ if (/[⿴⿵⿶⿷⿸⿹⿺⿼⿽⿻]/.test(glyph.operator) &&
481
+ "strokes" in operandResults[0]) {
482
+ sequence.push(operandResults[0].full[0]);
483
+ handleResidual(sequence, operandResults.slice(1));
484
+ }
485
+ else if (first.full.length === 1 &&
486
+ /[又]/.test(first.full[0]) &&
487
+ last.full.length === 1) {
488
+ sequence.push(last.full[0]);
489
+ handleResidual(sequence, sortedOperandResults.slice(0, -1));
490
+ }
491
+ else if (first.full.length !== 1 && last.full.length === 1) {
492
+ sequence.push(last.full[0]);
493
+ handleResidual(sequence, sortedOperandResults.slice(0, -1));
494
+ }
495
+ else {
496
+ sequence.push(first.full[0]);
497
+ handleResidual(sequence, sortedOperandResults.slice(1));
498
+ }
499
+ const full = sequentialSerializer(operandResults, glyph, config).full;
500
+ const backups = operandResults.map((x) => x.full);
501
+ operandResults.forEach((part) => {
502
+ // @ts-ignore
503
+ part.full = [...part.full2];
504
+ });
505
+ const full2 = sequentialSerializer(operandResults, glyph, config).full;
506
+ operandResults.forEach((part, i) => {
507
+ // @ts-ignore
508
+ part.full = backups[i];
509
+ });
510
+ return {
511
+ sequence,
512
+ full,
513
+ full2,
514
+ operator: glyph.operator,
515
+ operandResults,
516
+ };
517
+ };
518
+ const erbiSerializer = (operandResults, glyph, config) => {
519
+ const order = glyph.order ?? glyph.operandList.map((_, i) => ({ index: i, strokes: 0 }));
520
+ const sortedOperandResults = sortBy(range(operandResults.length), (i) => order.findIndex((b) => b.index === i)).map((i) => operandResults[i]);
521
+ const sequence = [];
522
+ for (const [index, part] of sortedOperandResults.entries()) {
523
+ if (index === sortedOperandResults.length - 1)
524
+ sequence.push(...part.sequence);
525
+ else
526
+ sequence.push(part.sequence[0]);
527
+ }
528
+ return {
529
+ sequence: sequence,
530
+ full: sequence,
531
+ operator: glyph.operator,
532
+ operandResults,
533
+ };
534
+ };
535
+ export const serializerTypes = [
536
+ "sequential",
537
+ "zhangma",
538
+ "zhenma",
539
+ "snow2",
540
+ "xkjd",
541
+ "shouyou",
542
+ "feihua",
543
+ "erbi",
544
+ ];
545
+ export const serializerMap = {
546
+ sequential: sequentialSerializer,
547
+ zhangma: zhangmaSerializer,
548
+ zhenma: zhenmaSerializer,
549
+ snow2: snow2Serializer,
550
+ xkjd: xkjdSerializer,
551
+ shouyou: shouyouSerializer,
552
+ feihua: feihuaSerializer,
553
+ erbi: erbiSerializer,
554
+ };
555
+ /**
556
+ * 对复合体进行拆分
557
+ *
558
+ * @param repertoire - 字符集
559
+ * @param config - 配置
560
+ * @param componentResults - 部件拆分结果
561
+ */
562
+ export const disassembleCompounds = (repertoire, config, componentResults, characters, requiredCompounds) => {
563
+ const knownCharacters = new Set(characters);
564
+ const compounds = topologicalSort(repertoire, requiredCompounds, config);
565
+ const compoundResults = new Map();
566
+ const compoundError = [];
567
+ const getResult = (s) => componentResults.get(s) || compoundResults.get(s);
568
+ const serializerName = config.analysis.serializer ?? "sequential";
569
+ const serializer = serializerMap[serializerName] ?? sequentialSerializer;
570
+ if (serializerName === "xkjd") {
571
+ for (const [_, result] of componentResults.entries()) {
572
+ result.sequence = limit(result.sequence, 4, config);
573
+ }
574
+ }
575
+ else if (serializerName === "snow2") {
576
+ for (const [key, result] of componentResults.entries()) {
577
+ result.sequence = result.sequence.slice(0, 1);
578
+ if (result.sequence[0] !== key)
579
+ result.sequence.push("");
580
+ }
581
+ }
582
+ else if (serializerName === "feihua") {
583
+ for (const [key, result] of componentResults.entries()) {
584
+ if ("schemes" in result) {
585
+ const dc = config.analysis?.dynamic_customize ?? {};
586
+ const schemeList = dc[key] ??
587
+ result.schemes.filter((x) => x.optional).map((x) => x.roots);
588
+ const scheme = schemeList.find((x) => x.every((r) => {
589
+ let value = config.roots.get(r);
590
+ while (isMerge(value)) {
591
+ value = config.roots.get(value.element);
592
+ }
593
+ return /[aoeiuv;/]/.test(value);
594
+ }));
595
+ // @ts-ignore
596
+ result.full2 = scheme;
597
+ result.sequence = scheme.slice(0, 3);
598
+ }
599
+ else {
600
+ // @ts-ignore
601
+ result.full2 = [...result.full];
602
+ }
603
+ }
604
+ }
605
+ for (const [char, { glyph }] of compounds.entries()) {
606
+ if (config.roots.has(char)) {
607
+ if (serializerName === "feihua") {
608
+ const rawOperandResults = glyph.operandList.map(getResult);
609
+ const operandResults = rawOperandResults;
610
+ const serialization = serializer(operandResults, glyph, config);
611
+ serialization.full = [char];
612
+ let value = config.roots.get(char);
613
+ while (isMerge(value)) {
614
+ value = config.roots.get(value.element);
615
+ }
616
+ if (/[aoeiuv;/]/.test(value)) {
617
+ // @ts-ignore
618
+ serialization.full2 = [char];
619
+ }
620
+ compoundResults.set(char, serialization);
621
+ continue;
622
+ }
623
+ // 复合体本身是一个字根
624
+ const sequence = [char];
625
+ if (serializerName === "snow2") {
626
+ sequence.push("q");
627
+ }
628
+ compoundResults.set(char, {
629
+ sequence: sequence,
630
+ full: [char],
631
+ // @ts-ignore
632
+ full2: [char],
633
+ strokes: 0,
634
+ operator: undefined,
635
+ });
636
+ continue;
637
+ }
638
+ const rawOperandResults = glyph.operandList.map(getResult);
639
+ if (rawOperandResults.every((x) => x !== undefined)) {
640
+ // this is safe!
641
+ const operandResults = rawOperandResults;
642
+ const serialization = serializer(operandResults, glyph, config);
643
+ compoundResults.set(char, serialization);
644
+ }
645
+ else {
646
+ if (knownCharacters.has(char)) {
647
+ compoundError.push(char);
648
+ }
649
+ }
650
+ }
651
+ return [compoundResults, compoundError];
652
+ };