@sushichan044/kg-core 0.8.0 → 0.9.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.
package/README.md CHANGED
@@ -84,10 +84,18 @@ a closing bracket, comma, or full stop, a quarter em on either side of a middle
84
84
  a quarter em between Japanese and a numeral, unit symbol, or Western character, and
85
85
  solid where two of those punctuation marks meet.
86
86
 
87
- An overflowing line is answered by reducing that space or by hanging a comma or full
88
- stop; a short line is answered by opening up space where JLReq 3.8.4 and appendix 表6
89
- allow it — the Japanese-to-Western quarter em first, up to a half, then a solid pair
90
- between kana, kanji, and the marks that go with them, up to a quarter. Space around
87
+ A western word space is not a character with a box but an アキ of its own: a third em
88
+ by rule rather than by measurement, nothing at all at a line head or a line end, and
89
+ the first _visible_ space either direction of the adjustment reaches for — only the
90
+ line-end space, which the reader cannot see at all, is spent ahead of it. It leaves the
91
+ composer as a `glue` item carrying the source text it came from.
92
+
93
+ An overflowing line is answered by reducing the space between characters, stage by
94
+ stage, or by hanging a comma or full stop; a short line is answered by opening up space
95
+ where JLReq 3.8.4 and appendix 表6
96
+ allow it — the word space first, up to a half, then the Japanese-to-Western quarter em,
97
+ also up to a half, then a solid pair between kana, kanji, and the marks that go with
98
+ them, up to a quarter. Space around
91
99
  brackets, commas, full stops, middle dots, hyphens, and an ideographic space is never
92
100
  opened up. A line that admits none of these is forced. The choice is made for the
93
101
  paragraph rather than line by line,
@@ -95,11 +103,33 @@ minimizing in order the number of forced lines, then stretched, then hanging, th
95
103
  shrunk lines that spent visible space. Which space may be reduced, and in which
96
104
  order, follows JLReq 3.8.3 and appendix 表3: the space at the line end goes first and
97
105
  costs the paragraph nothing, because it is invisible once that character lands there;
98
- then a middle dot's quarters, then the half em before an opening bracket or after a
106
+ then the word space, down to a quarter em; then a middle dot's quarters, then the half
107
+ em before an opening bracket or after a
99
108
  closing bracket or comma, then the Japanese-to-Western quarter down to an eighth. The
100
109
  half em after a mid-line full stop marks the end of a sentence and is never reduced,
101
110
  and neither is the half em that keeps a line-head opening bracket off the edge.
102
111
 
112
+ JLReq puts the word space ahead of the line end; the composer swaps the two, because
113
+ a stage here says how visible an adjustment is and the line end is the only space the
114
+ reader cannot see at all.
115
+
116
+ The アキ at a line end is the one exception to spending a space in part. JLReq 3.1.9
117
+ admits the full アキ or ベタ組 and forbids anything between, so a line whose overflow is
118
+ smaller than it leaves it whole and finds the difference elsewhere — or breaks
119
+ elsewhere. A 中点類 at a line end gives up the quarter em before it together with the
120
+ quarter after, as one amount.
121
+
122
+ Every stage is spent 文字サイズ比で均等に, as JLReq 3.8.3 and 3.8.4 both put it: the amount
123
+ is split across all the spaces of that stage in proportion to what each of them can
124
+ give, rather than taken out of the earliest space until it runs dry. A stage opens
125
+ only once every stage before it is spent out.
126
+
127
+ JLReq 3.1.5 states the white before a line-head opening bracket as a pair of amounts:
128
+ one for 改行行頭, the head of a line that starts a paragraph, and one for 折返し行頭, the
129
+ head of a line the composer turned over. The specification lists three such pairs, and
130
+ the composer takes the one Japanese novels are set with — scheme ③: a half em at
131
+ 改行行頭, and 天付き, flush against the edge, at 折返し行頭.
132
+
103
133
  The composer also owns Japanese line-start and line-end restrictions, inseparable
104
134
  punctuation, question/exclamation gap suppression, and ruby placement.
105
135
 
@@ -914,7 +914,7 @@ declare const ParsedManuscriptSchema: v.SchemaWithPipe<readonly [v.StrictObjectS
914
914
  }>;
915
915
  mark: string;
916
916
  }>]>], undefined>, undefined>, v.ReadonlyAction<(Readonly<{
917
- kind: "ruby";
917
+ kind: "italic";
918
918
  readonly range: Readonly<{
919
919
  source: Readonly<{
920
920
  start: number;
@@ -929,18 +929,8 @@ declare const ParsedManuscriptSchema: v.SchemaWithPipe<readonly [v.StrictObjectS
929
929
  end: number;
930
930
  }> & v.Brand<"GraphemeRange">;
931
931
  }>;
932
- reading: Readonly<{
933
- kind: "group";
934
- text: string;
935
- }> | Readonly<{
936
- kind: "mono";
937
- readonly segments: readonly string[];
938
- }> | Readonly<{
939
- kind: "jukugo";
940
- readonly segments: readonly string[];
941
- }>;
942
932
  }> | Readonly<{
943
- kind: "bold";
933
+ kind: "ruby";
944
934
  readonly range: Readonly<{
945
935
  source: Readonly<{
946
936
  start: number;
@@ -955,8 +945,18 @@ declare const ParsedManuscriptSchema: v.SchemaWithPipe<readonly [v.StrictObjectS
955
945
  end: number;
956
946
  }> & v.Brand<"GraphemeRange">;
957
947
  }>;
948
+ reading: Readonly<{
949
+ kind: "group";
950
+ text: string;
951
+ }> | Readonly<{
952
+ kind: "mono";
953
+ readonly segments: readonly string[];
954
+ }> | Readonly<{
955
+ kind: "jukugo";
956
+ readonly segments: readonly string[];
957
+ }>;
958
958
  }> | Readonly<{
959
- kind: "italic";
959
+ kind: "bold";
960
960
  readonly range: Readonly<{
961
961
  source: Readonly<{
962
962
  start: number;
@@ -1010,7 +1010,7 @@ declare const ParsedManuscriptSchema: v.SchemaWithPipe<readonly [v.StrictObjectS
1010
1010
  }>;
1011
1011
  }>[];
1012
1012
  readonly annotations: readonly (Readonly<{
1013
- kind: "ruby";
1013
+ kind: "italic";
1014
1014
  readonly range: Readonly<{
1015
1015
  source: Readonly<{
1016
1016
  start: number;
@@ -1025,18 +1025,8 @@ declare const ParsedManuscriptSchema: v.SchemaWithPipe<readonly [v.StrictObjectS
1025
1025
  end: number;
1026
1026
  }> & v.Brand<"GraphemeRange">;
1027
1027
  }>;
1028
- reading: Readonly<{
1029
- kind: "group";
1030
- text: string;
1031
- }> | Readonly<{
1032
- kind: "mono";
1033
- readonly segments: readonly string[];
1034
- }> | Readonly<{
1035
- kind: "jukugo";
1036
- readonly segments: readonly string[];
1037
- }>;
1038
1028
  }> | Readonly<{
1039
- kind: "bold";
1029
+ kind: "ruby";
1040
1030
  readonly range: Readonly<{
1041
1031
  source: Readonly<{
1042
1032
  start: number;
@@ -1051,8 +1041,18 @@ declare const ParsedManuscriptSchema: v.SchemaWithPipe<readonly [v.StrictObjectS
1051
1041
  end: number;
1052
1042
  }> & v.Brand<"GraphemeRange">;
1053
1043
  }>;
1044
+ reading: Readonly<{
1045
+ kind: "group";
1046
+ text: string;
1047
+ }> | Readonly<{
1048
+ kind: "mono";
1049
+ readonly segments: readonly string[];
1050
+ }> | Readonly<{
1051
+ kind: "jukugo";
1052
+ readonly segments: readonly string[];
1053
+ }>;
1054
1054
  }> | Readonly<{
1055
- kind: "italic";
1055
+ kind: "bold";
1056
1056
  readonly range: Readonly<{
1057
1057
  source: Readonly<{
1058
1058
  start: number;
@@ -1503,7 +1503,7 @@ declare const ParsedManuscript: {
1503
1503
  }>;
1504
1504
  mark: string;
1505
1505
  }>]>], undefined>, undefined>, v.ReadonlyAction<(Readonly<{
1506
- kind: "ruby";
1506
+ kind: "italic";
1507
1507
  readonly range: Readonly<{
1508
1508
  source: Readonly<{
1509
1509
  start: number;
@@ -1518,18 +1518,8 @@ declare const ParsedManuscript: {
1518
1518
  end: number;
1519
1519
  }> & v.Brand<"GraphemeRange">;
1520
1520
  }>;
1521
- reading: Readonly<{
1522
- kind: "group";
1523
- text: string;
1524
- }> | Readonly<{
1525
- kind: "mono";
1526
- readonly segments: readonly string[];
1527
- }> | Readonly<{
1528
- kind: "jukugo";
1529
- readonly segments: readonly string[];
1530
- }>;
1531
1521
  }> | Readonly<{
1532
- kind: "bold";
1522
+ kind: "ruby";
1533
1523
  readonly range: Readonly<{
1534
1524
  source: Readonly<{
1535
1525
  start: number;
@@ -1544,8 +1534,18 @@ declare const ParsedManuscript: {
1544
1534
  end: number;
1545
1535
  }> & v.Brand<"GraphemeRange">;
1546
1536
  }>;
1537
+ reading: Readonly<{
1538
+ kind: "group";
1539
+ text: string;
1540
+ }> | Readonly<{
1541
+ kind: "mono";
1542
+ readonly segments: readonly string[];
1543
+ }> | Readonly<{
1544
+ kind: "jukugo";
1545
+ readonly segments: readonly string[];
1546
+ }>;
1547
1547
  }> | Readonly<{
1548
- kind: "italic";
1548
+ kind: "bold";
1549
1549
  readonly range: Readonly<{
1550
1550
  source: Readonly<{
1551
1551
  start: number;
@@ -1599,7 +1599,7 @@ declare const ParsedManuscript: {
1599
1599
  }>;
1600
1600
  }>[];
1601
1601
  readonly annotations: readonly (Readonly<{
1602
- kind: "ruby";
1602
+ kind: "italic";
1603
1603
  readonly range: Readonly<{
1604
1604
  source: Readonly<{
1605
1605
  start: number;
@@ -1614,18 +1614,8 @@ declare const ParsedManuscript: {
1614
1614
  end: number;
1615
1615
  }> & v.Brand<"GraphemeRange">;
1616
1616
  }>;
1617
- reading: Readonly<{
1618
- kind: "group";
1619
- text: string;
1620
- }> | Readonly<{
1621
- kind: "mono";
1622
- readonly segments: readonly string[];
1623
- }> | Readonly<{
1624
- kind: "jukugo";
1625
- readonly segments: readonly string[];
1626
- }>;
1627
1617
  }> | Readonly<{
1628
- kind: "bold";
1618
+ kind: "ruby";
1629
1619
  readonly range: Readonly<{
1630
1620
  source: Readonly<{
1631
1621
  start: number;
@@ -1640,8 +1630,18 @@ declare const ParsedManuscript: {
1640
1630
  end: number;
1641
1631
  }> & v.Brand<"GraphemeRange">;
1642
1632
  }>;
1633
+ reading: Readonly<{
1634
+ kind: "group";
1635
+ text: string;
1636
+ }> | Readonly<{
1637
+ kind: "mono";
1638
+ readonly segments: readonly string[];
1639
+ }> | Readonly<{
1640
+ kind: "jukugo";
1641
+ readonly segments: readonly string[];
1642
+ }>;
1643
1643
  }> | Readonly<{
1644
- kind: "italic";
1644
+ kind: "bold";
1645
1645
  readonly range: Readonly<{
1646
1646
  source: Readonly<{
1647
1647
  start: number;
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as ManuscriptResult, c as DiagnosticSeverity, d as ManuscriptRangeInput, i as ParsedManuscript, l as DiagnosticOrigin, n as ValidationIssue, o as ManuscriptDiagnostic, r as NamespacedId, s as ManuscriptDiagnosticInput, t as ComposedManuscript, u as ManuscriptRange } from "./composed-manuscript-QgIdae8p.mjs";
2
- import { d as NovelCompositionSettings, f as ManuscriptComposer, i as NovelLayout, l as VerticalTextPresentation, p as ManuscriptParser, r as novelComposer, t as NovelComposedManuscript, u as FontPresetId } from "./novel-composer-BCt9g172.mjs";
1
+ import { a as ManuscriptResult, c as DiagnosticSeverity, d as ManuscriptRangeInput, i as ParsedManuscript, l as DiagnosticOrigin, n as ValidationIssue, o as ManuscriptDiagnostic, r as NamespacedId, s as ManuscriptDiagnosticInput, t as ComposedManuscript, u as ManuscriptRange } from "./composed-manuscript-C0f3mxKs.mjs";
2
+ import { d as NovelCompositionSettings, f as ManuscriptComposer, i as NovelLayout, l as VerticalTextPresentation, p as ManuscriptParser, r as novelComposer, t as NovelComposedManuscript, u as FontPresetId } from "./novel-composer-DhycV9B3.mjs";
3
3
  import * as v from "valibot";
4
4
  //#region src/parser/parse-error.d.ts
5
5
  /**
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { c as ManuscriptResult, d as NamespacedId, f as ManuscriptRange, h as readonlyObject, l as ManuscriptDiagnostic, o as graphemeSegmenter, s as ValidationIssue } from "./question-or-exclamation-spacing-B5o23ZLf.mjs";
2
- import { c as ManuscriptOffsets, d as FontSizePt, f as FontPresetId, i as PaperSize, l as ManuscriptAppearanceSettings, n as novelComposer, o as NovelCompositionSettings, p as ParsedManuscript, r as ManuscriptGeometry, s as NovelFlowSettings, u as PaperSizeId } from "./novel-composer-CROZLBqt.mjs";
2
+ import { c as ManuscriptOffsets, d as FontSizePt, f as FontPresetId, i as PaperSize, l as ManuscriptAppearanceSettings, n as novelComposer, o as NovelCompositionSettings, p as ParsedManuscript, r as ManuscriptGeometry, s as NovelFlowSettings, u as PaperSizeId } from "./novel-composer-CtovfhET.mjs";
3
3
  import { n as assertNever, t as describeCause } from "./describe-cause-BV8mZ3Um.mjs";
4
4
  import * as v from "valibot";
5
5
  const Rejection = { schema: readonlyObject({ reason: v.string() }) };
package/dist/lint.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as ManuscriptResult, i as ParsedManuscript, n as ValidationIssue, o as ManuscriptDiagnostic, r as NamespacedId, t as ComposedManuscript } from "./composed-manuscript-QgIdae8p.mjs";
1
+ import { a as ManuscriptResult, i as ParsedManuscript, n as ValidationIssue, o as ManuscriptDiagnostic, r as NamespacedId, t as ComposedManuscript } from "./composed-manuscript-C0f3mxKs.mjs";
2
2
  import * as v from "valibot";
3
3
  //#region src/proofreading/invalid-rule-options.d.ts
4
4
  /**
@@ -337,14 +337,17 @@ function classify({ value, presentation }) {
337
337
  * The stages JLReq 3.8.3 spends inter-character space in, as glue priorities: the lower the number,
338
338
  * the earlier the paragraph optimizer takes it.
339
339
  *
340
- * JLReq's own first stage is the western word space (cl-26), which is absent here because reducing
341
- * it means resizing the space's own advance rather than a space between two characters.
340
+ * JLReq puts the western word space first and the line-end reduction second; this profile swaps the
341
+ * two, because a priority here says how visible an adjustment is rather than where 3.8.3's list
342
+ * puts it. Priority 0 is capacity the paragraph optimizer spends for nothing, and a line that spent
343
+ * only priority 0 reads as tightly set as an unadjusted one. That is true of the line end and of
344
+ * nothing else: the half em after a closing bracket, full stop or comma — and the quarter em after
345
+ * a middle dot — falls outside the text area once that character sits at the line end, so taking it
346
+ * changes nothing on the page (3.1.9). A 欧文間隔 sits between two words the reader is looking at, so
347
+ * narrowing it does change the page. Spending the invisible space first can only leave the word
348
+ * spaces nearer their 三分アキ than JLReq's order would, never further from it.
342
349
  *
343
- * The line-end stage is priority 0, which the optimizer also charges nothing for. JLReq 3.1.9 is
344
- * what allows that: the half em after a closing bracket, full stop or comma — and the quarter em
345
- * after a middle dot — cannot be seen once that character sits at the line end. JLReq orders it
346
- * second rather than first, but only after the word space this profile does not adjust at all.
347
- * Stages two and three are one priority here because a line has a single line end, so the two never
350
+ * Stages two and three are one priority because a line has a single line end, so the two never
348
351
  * compete.
349
352
  */
350
353
  const REDUCTION_STAGE = {
@@ -353,37 +356,47 @@ const REDUCTION_STAGE = {
353
356
  */
354
357
  lineEnd: 0,
355
358
  /**
359
+ * Stage 1: the western word space, from its 三分アキ down to a 四分アキ.
360
+ */
361
+ wordSpace: 1,
362
+ /**
356
363
  * Stage 4: the quarter em on either side of a mid-line middle dot.
357
364
  */
358
- middleDot: 1,
365
+ middleDot: 2,
359
366
  /**
360
367
  * Stage 5: the half em before an opening bracket and after a closing bracket or comma.
361
368
  */
362
- punctuation: 2,
369
+ punctuation: 3,
363
370
  /**
364
371
  * Stage 6: the quarter em between Japanese and a numeral, unit symbol or western character.
365
372
  */
366
- mixedText: 3
373
+ mixedText: 4
367
374
  };
368
375
  /**
369
376
  * The stages JLReq 3.8.4 adds inter-character space in, as glue priorities on the same reading as
370
377
  * {@link REDUCTION_STAGE}: the lower the number, the earlier the paragraph optimizer takes it. A
371
378
  * line is either reduced or expanded, never both, so the two scales never meet.
372
379
  *
373
- * Stage 1 is the western word space (cl-26), absent for the same reason it is absent from the
374
- * reduction stages. Stage 4 spreads whatever a、b and c could not absorb across every character gap
375
- * that is not unbreakable, and is deliberately not implemented: it would justify every line that
376
- * currently falls short, which reads as prose combed out to the margin rather than a novel.
380
+ * Stage 4 spreads whatever a、b and c could not absorb across every character gap that is not
381
+ * unbreakable, and is deliberately not implemented: it would justify every line that currently
382
+ * falls short, which reads as prose combed out to the margin rather than a novel.
383
+ *
384
+ * Nothing here takes priority 0. Expanding is always visible, and priority 0 means an adjustment
385
+ * the optimizer is charged nothing for.
377
386
  */
378
387
  const EXPANSION_STAGE = {
388
+ /**
389
+ * Stage 1: the western word space, from its 三分アキ up to a 二分アキ.
390
+ */
391
+ wordSpace: 1,
379
392
  /**
380
393
  * Stage 2: the quarter em between Japanese and a numeral, unit symbol or western character.
381
394
  */
382
- mixedText: 1,
395
+ mixedText: 2,
383
396
  /**
384
397
  * Stage 3: everywhere else 表6 admits, from solid up to a quarter em.
385
398
  */
386
- solid: 2
399
+ solid: 3
387
400
  };
388
401
  /**
389
402
  * 無印 in 表1 with no colour in 表6: the pair is set solid and line adjustment leaves it alone.
@@ -400,7 +413,8 @@ const SOLID_EXPANDABLE = {
400
413
  naturalWidthEm: 0,
401
414
  stretch: {
402
415
  priority: EXPANSION_STAGE.solid,
403
- amountEm: .25
416
+ amountEm: .25,
417
+ granularity: "continuous"
404
418
  }
405
419
  };
406
420
  /**
@@ -411,7 +425,8 @@ const HALF = {
411
425
  naturalWidthEm: .5,
412
426
  shrink: {
413
427
  priority: REDUCTION_STAGE.punctuation,
414
- amountEm: .5
428
+ amountEm: .5,
429
+ granularity: "continuous"
415
430
  }
416
431
  };
417
432
  /**
@@ -429,7 +444,8 @@ const QUARTER = {
429
444
  naturalWidthEm: .25,
430
445
  shrink: {
431
446
  priority: REDUCTION_STAGE.middleDot,
432
- amountEm: .25
447
+ amountEm: .25,
448
+ granularity: "continuous"
433
449
  }
434
450
  };
435
451
  /**
@@ -448,11 +464,13 @@ const MIXED_TEXT_QUARTER = {
448
464
  naturalWidthEm: .25,
449
465
  shrink: {
450
466
  priority: REDUCTION_STAGE.mixedText,
451
- amountEm: .125
467
+ amountEm: .125,
468
+ granularity: "continuous"
452
469
  },
453
470
  stretch: {
454
471
  priority: EXPANSION_STAGE.mixedText,
455
- amountEm: .25
472
+ amountEm: .25,
473
+ granularity: "continuous"
456
474
  }
457
475
  };
458
476
  /**
@@ -464,7 +482,8 @@ const UNIT_SYMBOL_QUARTER = {
464
482
  naturalWidthEm: .25,
465
483
  shrink: {
466
484
  priority: REDUCTION_STAGE.mixedText,
467
- amountEm: .125
485
+ amountEm: .125,
486
+ granularity: "continuous"
468
487
  }
469
488
  };
470
489
  /**
@@ -475,7 +494,8 @@ const TWO_QUARTERS = {
475
494
  naturalWidthEm: .5,
476
495
  shrink: {
477
496
  priority: REDUCTION_STAGE.middleDot,
478
- amountEm: .5
497
+ amountEm: .5,
498
+ granularity: "continuous"
479
499
  }
480
500
  };
481
501
  /**
@@ -486,7 +506,8 @@ const FIXED_HALF_AND_QUARTER = {
486
506
  naturalWidthEm: .75,
487
507
  shrink: {
488
508
  priority: REDUCTION_STAGE.middleDot,
489
- amountEm: .25
509
+ amountEm: .25,
510
+ granularity: "continuous"
490
511
  }
491
512
  };
492
513
  /**
@@ -501,7 +522,8 @@ const HALF_AND_QUARTER = {
501
522
  naturalWidthEm: .75,
502
523
  shrink: {
503
524
  priority: REDUCTION_STAGE.punctuation,
504
- amountEm: .75
525
+ amountEm: .75,
526
+ granularity: "continuous"
505
527
  }
506
528
  };
507
529
  /**
@@ -512,6 +534,40 @@ const DASH_JOINT = {
512
534
  naturalWidthEm: 0
513
535
  };
514
536
  /**
537
+ * 欧文間隔(cl-26)の三分アキ. JLReq states the western word space as a third em by rule rather than by
538
+ * measurement, and makes it the first space a line adjustment reaches for: down to a 四分アキ when the
539
+ * line is over (3.8.3 a), up to a 二分アキ when it is short (3.8.4 a).
540
+ *
541
+ * The two capacities are written as differences from the limits rather than as `1/12` and `1/6`,
542
+ * because a third of an em is not representable in binary while the limits JLReq states are: this
543
+ * way `naturalWidthEm - shrink.amountEm` is exactly 0.25 and `naturalWidthEm + stretch.amountEm`
544
+ * exactly 0.5.
545
+ */
546
+ const WORD_SPACE = {
547
+ kind: "glue",
548
+ naturalWidthEm: 1 / 3,
549
+ shrink: {
550
+ priority: REDUCTION_STAGE.wordSpace,
551
+ amountEm: 1 / 3 - 1 / 4,
552
+ granularity: "continuous"
553
+ },
554
+ stretch: {
555
+ priority: EXPANSION_STAGE.wordSpace,
556
+ amountEm: 1 / 2 - 1 / 3,
557
+ granularity: "continuous"
558
+ }
559
+ };
560
+ /**
561
+ * 表1 注13 と 表3 注4: a word space at a line head or a line end has no width and is no adjustment
562
+ * opportunity, since the white would fall outside the text area where no reader can see it. 注13
563
+ * adds ただし,この部分が移動し,これとは異なる配置になった場合は,欧文間隔(cl-26)の空き量を確保する — which is why the answer is given per
564
+ * candidate line and not once per source line.
565
+ */
566
+ const EDGE_WORD_SPACE = {
567
+ kind: "glue",
568
+ naturalWidthEm: 0
569
+ };
570
+ /**
515
571
  * The classes that take a quarter em against a numeral, unit symbol or western character (3.2.6).
516
572
  * Dividing punctuation is on the list in that direction only, because the space before a `?` or `!`
517
573
  * is solid (3.1.6).
@@ -807,6 +863,10 @@ const defaultJapaneseTypesettingProfile = {
807
863
  * trailing half of its em, a middle dot in the centre, and the rest in the leading half.
808
864
  */
809
865
  boxMetrics: (characterClass, measuredAdvanceEm) => {
866
+ if (characterClass === "cl-26") return {
867
+ advanceEm: 0,
868
+ renderOffsetEm: 0
869
+ };
810
870
  const advanceEm = HALF_EM_CLASSES.has(characterClass) ? Math.min(.5, measuredAdvanceEm) : measuredAdvanceEm;
811
871
  const inkOverhangEm = measuredAdvanceEm - advanceEm;
812
872
  return {
@@ -815,16 +875,18 @@ const defaultJapaneseTypesettingProfile = {
815
875
  };
816
876
  },
817
877
  pairSpacing,
878
+ spacingCharacter: (characterClass, position) => characterClass !== "cl-26" ? null : position === "mid-line" ? WORD_SPACE : EDGE_WORD_SPACE,
818
879
  /**
819
- * An opening bracket at the line head keeps a half em of white, so a line of dialogue starts one
820
- * half em in rather than tight against the edge. JLReq 3.1.5 lists three schemes for that white
821
- * and 表1 注17 sets solid as the principle; the half em is the variant the publishers of Japanese
822
- * novels use for a line that begins a paragraph.
880
+ * The white before an opening bracket at a line head. JLReq 3.1.5 pairs 改行行頭 with 折返し行頭 and lists
881
+ * three schemes for the two together: ① 全角アキ and 天付き, ② 全角半アキ and 二分アキ, ③ 二分アキ and 天付き. This
882
+ * profile takes ③, the scheme 講談社, 新潮社, 文藝春秋, 中央公論新社 and 筑摩書房 set novels with; ① is what JIS X
883
+ * 4051 and 岩波書店 use. A line of dialogue therefore starts one half em in when it opens a
884
+ * paragraph, and flush against the edge when the composer turned it over.
823
885
  *
824
886
  * The white is not shrinkable. 表3 excludes the line head from line adjustment altogether, and
825
887
  * shrinking it is exactly what pushed a line-head bracket flush against the edge in #94.
826
888
  */
827
- lineStartSpacing: (first) => first === "cl-01" ? {
889
+ lineStartSpacing: (first, lineHead) => first === "cl-01" && lineHead === "paragraph-start" ? {
828
890
  kind: "glue",
829
891
  naturalWidthEm: .5
830
892
  } : null,
@@ -832,21 +894,33 @@ const defaultJapaneseTypesettingProfile = {
832
894
  * The half em trailing a closing bracket, full stop or comma at the line end, and the quarter em
833
895
  * trailing a middle dot there (JLReq 3.1.9). Priority 0: spending it changes nothing the reader
834
896
  * can see.
897
+ *
898
+ * Both are all-or-nothing. 3.1.9 admits the full アキ or ベタ組 and forbids anything in between, so a
899
+ * line whose overflow is smaller than the アキ has to find the difference somewhere else — or break
900
+ * elsewhere. A line-end middle dot takes the quarter before it into the same amount (表3 注5).
835
901
  */
836
902
  lineEndSpacing: (last) => last === "cl-02" || last === "cl-06" || last === "cl-07" ? {
837
- kind: "glue",
838
- naturalWidthEm: .5,
839
- shrink: {
840
- priority: REDUCTION_STAGE.lineEnd,
841
- amountEm: .5
842
- }
903
+ spacing: {
904
+ kind: "glue",
905
+ naturalWidthEm: .5,
906
+ shrink: {
907
+ priority: REDUCTION_STAGE.lineEnd,
908
+ amountEm: .5,
909
+ granularity: "all-or-nothing"
910
+ }
911
+ },
912
+ absorbsPrecedingEm: 0
843
913
  } : last === "cl-05" ? {
844
- kind: "glue",
845
- naturalWidthEm: .25,
846
- shrink: {
847
- priority: REDUCTION_STAGE.lineEnd,
848
- amountEm: .25
849
- }
914
+ spacing: {
915
+ kind: "glue",
916
+ naturalWidthEm: .25,
917
+ shrink: {
918
+ priority: REDUCTION_STAGE.lineEnd,
919
+ amountEm: .25,
920
+ granularity: "all-or-nothing"
921
+ }
922
+ },
923
+ absorbsPrecedingEm: .25
850
924
  } : null,
851
925
  breakPenalty,
852
926
  /**
@@ -862,6 +936,13 @@ const EPSILON = 1e-9;
862
936
  * Spending capacity at this priority changes nothing the reader can see.
863
937
  */
864
938
  const FREE_SHRINK_PRIORITY = 0;
939
+ /**
940
+ * Interleave the two kinds onto one number so the allocator can key a plain `Map` without building
941
+ * a string per boundary — this runs once per adjustment unit of every candidate line.
942
+ */
943
+ function slotKey(slot) {
944
+ return slot.kind === "gap" ? slot.boundary * 2 : slot.index * 2 + 1;
945
+ }
865
946
  function skipSourceGaps(atoms, start) {
866
947
  let cursor = start;
867
948
  while (atoms[cursor]?.sourceGap === true) cursor += 1;
@@ -870,14 +951,31 @@ function skipSourceGaps(atoms, start) {
870
951
  function previousVisible(atoms, end) {
871
952
  for (let index = end - 1; index >= 0; index -= 1) if (atoms[index]?.sourceGap === false) return index;
872
953
  }
873
- function opportunities(atoms, classes, start, end, profile) {
954
+ function opportunities(atoms, classes, start, end, profile, lineHead) {
874
955
  const result = [];
875
956
  const firstClass = classes[start];
876
- const startSpacing = firstClass === void 0 ? null : profile.lineStartSpacing(firstClass);
957
+ const startSpacing = firstClass === void 0 ? null : profile.lineStartSpacing(firstClass, lineHead);
877
958
  if (startSpacing !== null) result.push({
878
- boundary: start,
879
- spacing: startSpacing
959
+ slot: {
960
+ kind: "gap",
961
+ boundary: start
962
+ },
963
+ spacing: startSpacing,
964
+ absorbsPrecedingEm: 0
880
965
  });
966
+ for (let index = start; index < end; index += 1) {
967
+ const characterClass = classes[index];
968
+ if (characterClass === void 0 || atoms[index]?.sourceGap === true) continue;
969
+ const spacing = profile.spacingCharacter(characterClass, index === start || index === end - 1 ? "line-edge" : "mid-line");
970
+ if (spacing !== null) result.push({
971
+ slot: {
972
+ kind: "character",
973
+ index
974
+ },
975
+ spacing,
976
+ absorbsPrecedingEm: 0
977
+ });
978
+ }
881
979
  for (let right = start + 1; right < end; right += 1) {
882
980
  const leftAtom = atoms[right - 1];
883
981
  const rightAtom = atoms[right];
@@ -885,73 +983,203 @@ function opportunities(atoms, classes, start, end, profile) {
885
983
  const rightClass = classes[right];
886
984
  if (leftAtom === void 0 || rightAtom === void 0 || leftClass === void 0 || rightClass === void 0 || leftAtom.sourceGap || rightAtom.sourceGap || !leftAtom.pairSpacingAfter) continue;
887
985
  result.push({
888
- boundary: right,
889
- spacing: profile.pairSpacing(leftClass, rightClass)
986
+ slot: {
987
+ kind: "gap",
988
+ boundary: right
989
+ },
990
+ spacing: profile.pairSpacing(leftClass, rightClass),
991
+ absorbsPrecedingEm: 0
890
992
  });
891
993
  }
892
994
  const lastClass = classes[end - 1];
893
995
  const endSpacing = lastClass === void 0 ? null : profile.lineEndSpacing(lastClass);
894
996
  if (endSpacing !== null) result.push({
895
- boundary: end,
896
- spacing: endSpacing
997
+ slot: {
998
+ kind: "gap",
999
+ boundary: end
1000
+ },
1001
+ spacing: endSpacing.spacing,
1002
+ absorbsPrecedingEm: endSpacing.absorbsPrecedingEm
897
1003
  });
898
1004
  return result;
899
1005
  }
900
- function capacity(values, select) {
901
- return values.reduce((total, opportunity) => total + (select(opportunity.spacing)?.amountEm ?? 0), 0);
1006
+ /**
1007
+ * Break the line's spaces into the indivisible amounts the line adjustment may spend.
1008
+ *
1009
+ * A line end the profile says absorbs the space before it claims that much of the preceding
1010
+ * boundary's capacity, and the two become one unit at the line-end stage — JLReq 3.8.3's third
1011
+ * stage, which is free. Whatever the preceding boundary has left over stays an opportunity of its
1012
+ * own stage, so the half em of a `、` before a line-end `・` is still spent where 3.8.3 puts it.
1013
+ */
1014
+ function adjustmentUnits(values, adjustment) {
1015
+ const select = (spacing) => adjustment === "shrink" ? spacing.shrink : spacing.stretch;
1016
+ const units = [];
1017
+ const straddle = values.find(({ absorbsPrecedingEm }) => absorbsPrecedingEm > 0);
1018
+ let absorbedBoundary;
1019
+ let absorbedEm = 0;
1020
+ let absorbingBoundary;
1021
+ const straddleSlot = straddle?.slot.kind === "gap" ? straddle.slot : void 0;
1022
+ const straddleOwn = straddle === void 0 ? void 0 : select(straddle.spacing);
1023
+ if (straddle !== void 0 && straddleSlot !== void 0 && straddleOwn !== void 0) {
1024
+ const precedingBoundary = straddleSlot.boundary - 1;
1025
+ const preceding = values.find(({ slot }) => slot.kind === "gap" && slot.boundary === precedingBoundary);
1026
+ const availableEm = preceding === void 0 ? 0 : select(preceding.spacing)?.amountEm ?? 0;
1027
+ absorbingBoundary = straddleSlot.boundary;
1028
+ if (availableEm + EPSILON >= straddle.absorbsPrecedingEm) {
1029
+ absorbedBoundary = precedingBoundary;
1030
+ absorbedEm = straddle.absorbsPrecedingEm;
1031
+ units.push({
1032
+ parts: [{
1033
+ slot: {
1034
+ kind: "gap",
1035
+ boundary: precedingBoundary
1036
+ },
1037
+ amountEm: absorbedEm
1038
+ }, {
1039
+ slot: straddleSlot,
1040
+ amountEm: straddleOwn.amountEm
1041
+ }],
1042
+ priority: straddleOwn.priority,
1043
+ amountEm: absorbedEm + straddleOwn.amountEm,
1044
+ granularity: straddleOwn.granularity
1045
+ });
1046
+ }
1047
+ }
1048
+ for (const { slot, spacing } of values) {
1049
+ if (slot.kind === "gap" && slot.boundary === absorbingBoundary) continue;
1050
+ const own = select(spacing);
1051
+ if (own === void 0) continue;
1052
+ const claimedEm = slot.kind === "gap" && slot.boundary === absorbedBoundary ? absorbedEm : 0;
1053
+ const amountEm = own.amountEm - claimedEm;
1054
+ if (amountEm <= EPSILON) continue;
1055
+ units.push({
1056
+ parts: [{
1057
+ slot,
1058
+ amountEm
1059
+ }],
1060
+ priority: own.priority,
1061
+ amountEm,
1062
+ granularity: own.granularity
1063
+ });
1064
+ }
1065
+ return units;
902
1066
  }
903
- function resolveSpacings(values, adjustment, amountEm) {
904
- const remainingByBoundary = /* @__PURE__ */ new Map();
1067
+ /**
1068
+ * Spend `amountEm` over the line's units of adjustment, stage by stage.
1069
+ *
1070
+ * Every stage of JLReq 3.8.3 and 3.8.4 is stated as 文字サイズ比で均等に — the English text of 3.8.3 a puts
1071
+ * it as "The same width reduction is applied to all spaces on the target line at the same time."
1072
+ * Within a stage the amount is therefore split in proportion to what each space can give, rather
1073
+ * than taken out of the earliest space until it runs dry. Across stages the order stays a
1074
+ * waterfall: a later stage is reached only once every stage before it is spent out. The composer
1075
+ * sets one character size, so a share of the stage's capacity is a share by character size.
1076
+ *
1077
+ * An all-or-nothing unit larger than what is still needed is skipped rather than partly spent, and
1078
+ * never revisited: `remaining` only falls, so a unit that did not fit at its own stage cannot fit
1079
+ * at a later one. One forward pass is therefore exact and no search over subsets is needed.
1080
+ */
1081
+ function resolveSpacings(values, units, adjustment, amountEm) {
1082
+ const usedBySlot = /* @__PURE__ */ new Map();
905
1083
  let remaining = amountEm;
906
1084
  let priorityCost = 0;
907
- const capacities = values.flatMap((opportunity) => {
908
- const selected = adjustment === "shrink" ? opportunity.spacing.shrink : opportunity.spacing.stretch;
909
- return selected === void 0 ? [] : [{
910
- ...opportunity,
911
- capacity: selected
912
- }];
913
- }).sort((left, right) => left.capacity.priority - right.capacity.priority || left.boundary - right.boundary);
914
- for (const opportunity of capacities) {
1085
+ let freeEm = 0;
1086
+ const spend = (unit, fraction) => {
1087
+ for (const { slot, amountEm: partEm } of unit.parts) {
1088
+ const key = slotKey(slot);
1089
+ usedBySlot.set(key, (usedBySlot.get(key) ?? 0) + partEm * fraction);
1090
+ }
1091
+ const spentEm = unit.amountEm * fraction;
1092
+ priorityCost += spentEm * unit.priority;
1093
+ if (unit.priority === FREE_SHRINK_PRIORITY) freeEm += spentEm;
1094
+ remaining -= spentEm;
1095
+ };
1096
+ const stages = /* @__PURE__ */ new Map();
1097
+ for (const unit of units) {
1098
+ const stage = stages.get(unit.priority);
1099
+ if (stage === void 0) stages.set(unit.priority, [unit]);
1100
+ else stage.push(unit);
1101
+ }
1102
+ for (const priority of [...stages.keys()].sort((left, right) => left - right)) {
915
1103
  if (remaining <= EPSILON) break;
916
- const used = Math.min(remaining, opportunity.capacity.amountEm);
917
- remainingByBoundary.set(opportunity.boundary, used);
918
- priorityCost += used * opportunity.capacity.priority;
919
- remaining -= used;
1104
+ const stage = stages.get(priority) ?? [];
1105
+ let stageCapacityEm = 0;
1106
+ for (const unit of stage) if (unit.granularity === "continuous") stageCapacityEm += unit.amountEm;
1107
+ else if (unit.amountEm <= remaining + EPSILON) spend(unit, 1);
1108
+ if (stageCapacityEm <= EPSILON || remaining <= EPSILON) continue;
1109
+ const fraction = Math.min(remaining, stageCapacityEm) / stageCapacityEm;
1110
+ for (const unit of stage) if (unit.granularity === "continuous") spend(unit, fraction);
920
1111
  }
921
1112
  return {
922
- spacings: values.map(({ boundary, spacing }) => {
923
- const used = remainingByBoundary.get(boundary) ?? 0;
924
- return {
925
- boundary,
926
- kind: spacing.kind,
927
- naturalWidthEm: spacing.naturalWidthEm,
928
- widthEm: adjustment === "shrink" ? spacing.naturalWidthEm - used : spacing.naturalWidthEm + used
929
- };
1113
+ spacings: splitBySlot(values, (slot, spacing) => {
1114
+ const used = usedBySlot.get(slotKey(slot)) ?? 0;
1115
+ return adjustment === "shrink" ? spacing.naturalWidthEm - used : spacing.naturalWidthEm + used;
930
1116
  }),
931
- priorityCost
1117
+ priorityCost,
1118
+ freeEm,
1119
+ unabsorbedEm: Math.max(0, remaining)
1120
+ };
1121
+ }
1122
+ function splitBySlot(values, widthOf) {
1123
+ const pairSpacings = [];
1124
+ const characterSpacings = [];
1125
+ for (const { slot, spacing } of values) {
1126
+ const resolved = {
1127
+ kind: spacing.kind,
1128
+ naturalWidthEm: spacing.naturalWidthEm,
1129
+ widthEm: widthOf(slot, spacing)
1130
+ };
1131
+ if (slot.kind === "gap") pairSpacings.push({
1132
+ boundary: slot.boundary,
1133
+ ...resolved
1134
+ });
1135
+ else characterSpacings.push({
1136
+ index: slot.index,
1137
+ ...resolved
1138
+ });
1139
+ }
1140
+ return {
1141
+ pairSpacings,
1142
+ characterSpacings
1143
+ };
1144
+ }
1145
+ /**
1146
+ * The same spaces at the width the profile gives them, for a line no adjustment reaches.
1147
+ */
1148
+ function naturalSpacings(values) {
1149
+ return splitBySlot(values, (_slot, spacing) => spacing.naturalWidthEm);
1150
+ }
1151
+ /**
1152
+ * The same, minus the アキ the line end would have taken: a hanging character sits outside the text
1153
+ * area and the space that would have followed it goes with it.
1154
+ */
1155
+ function hangingSpacings(values, end) {
1156
+ const { pairSpacings, characterSpacings } = naturalSpacings(values);
1157
+ return {
1158
+ pairSpacings: pairSpacings.filter(({ boundary }) => boundary !== end),
1159
+ characterSpacings
932
1160
  };
933
1161
  }
934
1162
  function candidate(atoms, classes, start, end, lineLengthEm, profile) {
935
1163
  const contentStart = skipSourceGaps(atoms, start);
936
1164
  const suppressedIndexes = Array.from({ length: contentStart - start }, (_, index) => start + index);
937
- const pairValues = opportunities(atoms, classes, contentStart, end, profile);
1165
+ const pairValues = opportunities(atoms, classes, contentStart, end, profile, start === 0 ? "paragraph-start" : "turned-over");
938
1166
  const naturalSizeEm = atoms.slice(contentStart, end).reduce((total, atom) => total + atom.boxAdvanceEm, 0) + pairValues.reduce((total, value) => total + value.spacing.naturalWidthEm, 0);
939
1167
  const terminal = skipSourceGaps(atoms, end) === atoms.length;
940
- const shrinkCapacity = capacity(pairValues, ({ shrink }) => shrink);
941
- const freeShrinkCapacity = capacity(pairValues, ({ shrink }) => shrink?.priority === FREE_SHRINK_PRIORITY ? shrink : void 0);
942
- const stretchCapacity = capacity(pairValues, ({ stretch }) => stretch);
943
1168
  const overflow = naturalSizeEm - lineLengthEm;
944
1169
  const underflow = lineLengthEm - naturalSizeEm;
1170
+ const units = adjustmentUnits(pairValues, overflow > 0 ? "shrink" : "stretch");
1171
+ const capacity = units.reduce((total, { amountEm }) => total + amountEm, 0);
1172
+ const freeShrinkCapacity = units.reduce((total, { priority, amountEm }) => total + (priority === FREE_SHRINK_PRIORITY ? amountEm : 0), 0);
945
1173
  const lastVisible = previousVisible(atoms, end);
946
1174
  const lastClass = lastVisible === void 0 ? void 0 : classes[lastVisible];
947
1175
  const lastAdvance = lastVisible === void 0 ? 0 : atoms[lastVisible]?.boxAdvanceEm ?? 0;
948
- const trailingSpacing = pairValues.find(({ boundary }) => boundary === end)?.spacing.naturalWidthEm ?? 0;
1176
+ const trailingSpacing = pairValues.find(({ slot }) => slot.kind === "gap" && slot.boundary === end)?.spacing.naturalWidthEm ?? 0;
949
1177
  if (terminal && overflow <= EPSILON) return {
950
1178
  start,
951
1179
  contentStart,
952
1180
  end,
953
1181
  suppressedIndexes,
954
- pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings,
1182
+ ...naturalSpacings(pairValues),
955
1183
  inlineSizeEm: naturalSizeEm,
956
1184
  break: { kind: "paragraph-end" },
957
1185
  hangingIndex: null,
@@ -963,28 +1191,28 @@ function candidate(atoms, classes, start, end, lineLengthEm, profile) {
963
1191
  contentStart,
964
1192
  end,
965
1193
  suppressedIndexes,
966
- pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings,
1194
+ ...naturalSpacings(pairValues),
967
1195
  inlineSizeEm: naturalSizeEm,
968
1196
  break: { kind: "natural" },
969
1197
  hangingIndex: null,
970
1198
  deformationRatio: 0,
971
1199
  priorityCost: 0
972
1200
  };
973
- if (overflow > 0 && overflow <= shrinkCapacity + EPSILON) {
974
- const resolved = resolveSpacings(pairValues, "shrink", overflow);
975
- const chargeableCapacity = shrinkCapacity - freeShrinkCapacity;
976
- const chargeableShrink = Math.max(0, overflow - freeShrinkCapacity);
1201
+ const shrunk = overflow > 0 && overflow <= capacity + EPSILON ? resolveSpacings(pairValues, units, "shrink", overflow) : null;
1202
+ if (shrunk !== null && shrunk.unabsorbedEm <= EPSILON) {
1203
+ const chargeableCapacity = capacity - freeShrinkCapacity;
1204
+ const chargeableShrink = Math.max(0, overflow - shrunk.freeEm);
977
1205
  return {
978
1206
  start,
979
1207
  contentStart,
980
1208
  end,
981
1209
  suppressedIndexes,
982
- pairSpacings: resolved.spacings,
1210
+ ...shrunk.spacings,
983
1211
  inlineSizeEm: lineLengthEm,
984
1212
  break: { kind: "shrunk" },
985
1213
  hangingIndex: null,
986
1214
  deformationRatio: chargeableCapacity <= EPSILON ? 0 : chargeableShrink / chargeableCapacity,
987
- priorityCost: resolved.priorityCost
1215
+ priorityCost: shrunk.priorityCost
988
1216
  };
989
1217
  }
990
1218
  if (!terminal && overflow > 0 && lastVisible !== void 0 && lastClass !== void 0 && profile.canHang(lastClass) && naturalSizeEm - lastAdvance - trailingSpacing <= lineLengthEm + EPSILON) return {
@@ -992,34 +1220,32 @@ function candidate(atoms, classes, start, end, lineLengthEm, profile) {
992
1220
  contentStart,
993
1221
  end,
994
1222
  suppressedIndexes,
995
- pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings.filter(({ boundary }) => boundary !== end),
1223
+ ...hangingSpacings(pairValues, end),
996
1224
  inlineSizeEm: naturalSizeEm - lastAdvance - trailingSpacing,
997
1225
  break: { kind: "hanging" },
998
1226
  hangingIndex: lastVisible,
999
1227
  deformationRatio: overflow / Math.max(lastAdvance, EPSILON),
1000
1228
  priorityCost: 0
1001
1229
  };
1002
- if (!terminal && underflow > EPSILON && underflow <= stretchCapacity + EPSILON) {
1003
- const resolved = resolveSpacings(pairValues, "stretch", underflow);
1004
- return {
1005
- start,
1006
- contentStart,
1007
- end,
1008
- suppressedIndexes,
1009
- pairSpacings: resolved.spacings,
1010
- inlineSizeEm: lineLengthEm,
1011
- break: { kind: "stretched" },
1012
- hangingIndex: null,
1013
- deformationRatio: stretchCapacity === 0 ? 0 : underflow / stretchCapacity,
1014
- priorityCost: resolved.priorityCost
1015
- };
1016
- }
1230
+ const stretched = !terminal && underflow > EPSILON && underflow <= capacity + EPSILON ? resolveSpacings(pairValues, units, "stretch", underflow) : null;
1231
+ if (stretched !== null && stretched.unabsorbedEm <= EPSILON) return {
1232
+ start,
1233
+ contentStart,
1234
+ end,
1235
+ suppressedIndexes,
1236
+ ...stretched.spacings,
1237
+ inlineSizeEm: lineLengthEm,
1238
+ break: { kind: "stretched" },
1239
+ hangingIndex: null,
1240
+ deformationRatio: capacity === 0 ? 0 : underflow / capacity,
1241
+ priorityCost: stretched.priorityCost
1242
+ };
1017
1243
  return {
1018
1244
  start,
1019
1245
  contentStart,
1020
1246
  end,
1021
1247
  suppressedIndexes,
1022
- pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings,
1248
+ ...naturalSpacings(pairValues),
1023
1249
  inlineSizeEm: naturalSizeEm,
1024
1250
  break: { kind: "forced" },
1025
1251
  hangingIndex: null,
@@ -1759,7 +1985,9 @@ function positionedLine(atoms, sourceGapIndexes, plan) {
1759
1985
  }];
1760
1986
  });
1761
1987
  const spacings = new Map(plan.pairSpacings.map((spacing) => [spacing.boundary, spacing]));
1988
+ const characterSpacings = new Map(plan.characterSpacings.map((spacing) => [spacing.index, spacing]));
1762
1989
  let offsetEm = 0;
1990
+ const adjustmentOf = (widthEm, naturalWidthEm) => widthEm < naturalWidthEm ? "shrunk" : widthEm > naturalWidthEm ? "stretched" : "natural";
1763
1991
  const positionSpacing = (spacing) => {
1764
1992
  if (spacing.kind === "kern") items.push({
1765
1993
  kind: "kern",
@@ -1772,7 +2000,7 @@ function positionedLine(atoms, sourceGapIndexes, plan) {
1772
2000
  offsetEm,
1773
2001
  widthEm: spacing.widthEm,
1774
2002
  naturalWidthEm: spacing.naturalWidthEm,
1775
- adjustment: spacing.widthEm < spacing.naturalWidthEm ? "shrunk" : spacing.widthEm > spacing.naturalWidthEm ? "stretched" : "natural"
2003
+ adjustment: adjustmentOf(spacing.widthEm, spacing.naturalWidthEm)
1776
2004
  });
1777
2005
  offsetEm += spacing.widthEm;
1778
2006
  };
@@ -1781,18 +2009,21 @@ function positionedLine(atoms, sourceGapIndexes, plan) {
1781
2009
  if (spacing !== void 0) positionSpacing(spacing);
1782
2010
  const atom = atoms[index];
1783
2011
  if (atom === void 0) continue;
1784
- if (sourceGapIndexes.has(index)) {
2012
+ const characterSpacing = characterSpacings.get(index);
2013
+ if (sourceGapIndexes.has(index) || characterSpacing !== void 0) {
2014
+ const naturalWidthEm = characterSpacing?.naturalWidthEm ?? atom.boxAdvanceEm;
2015
+ const widthEm = characterSpacing?.widthEm ?? atom.boxAdvanceEm;
1785
2016
  items.push({
1786
2017
  kind: "glue",
1787
2018
  origin: "source",
1788
2019
  value: atom.grapheme.value,
1789
2020
  range: atom.grapheme.range,
1790
2021
  offsetEm,
1791
- widthEm: atom.boxAdvanceEm,
1792
- naturalWidthEm: atom.boxAdvanceEm,
1793
- adjustment: "natural"
2022
+ widthEm,
2023
+ naturalWidthEm,
2024
+ adjustment: adjustmentOf(widthEm, naturalWidthEm)
1794
2025
  });
1795
- offsetEm += atom.boxAdvanceEm;
2026
+ offsetEm += widthEm;
1796
2027
  continue;
1797
2028
  }
1798
2029
  const hanging = plan.hangingIndex === index;
@@ -1,4 +1,4 @@
1
- import { a as ManuscriptResult, i as ParsedManuscript, t as ComposedManuscript } from "./composed-manuscript-QgIdae8p.mjs";
1
+ import { a as ManuscriptResult, i as ParsedManuscript, t as ComposedManuscript } from "./composed-manuscript-C0f3mxKs.mjs";
2
2
  import * as v from "valibot";
3
3
  //#region src/result/rejection.d.ts
4
4
  declare const RejectionSchema: v.SchemaWithPipe<readonly [v.StrictObjectSchema<{
package/dist/plugin.d.mts CHANGED
@@ -1,3 +1,3 @@
1
- import { a as ManuscriptResult, i as ParsedManuscript, n as ValidationIssue, r as NamespacedId } from "./composed-manuscript-QgIdae8p.mjs";
2
- import { a as InlineMeasureRequest, c as logicalInlineMeasurer, f as ManuscriptComposer, m as Rejection, n as createNovelComposer, o as InlineMeasurement, p as ManuscriptParser, s as InlineMeasurer } from "./novel-composer-BCt9g172.mjs";
1
+ import { a as ManuscriptResult, i as ParsedManuscript, n as ValidationIssue, r as NamespacedId } from "./composed-manuscript-C0f3mxKs.mjs";
2
+ import { a as InlineMeasureRequest, c as logicalInlineMeasurer, f as ManuscriptComposer, m as Rejection, n as createNovelComposer, o as InlineMeasurement, p as ManuscriptParser, s as InlineMeasurer } from "./novel-composer-DhycV9B3.mjs";
3
3
  export { type InlineMeasureRequest, type InlineMeasurement, type InlineMeasurer, type ManuscriptComposer, type ManuscriptParser, ManuscriptResult, NamespacedId, ParsedManuscript, type Rejection, ValidationIssue, createNovelComposer, logicalInlineMeasurer };
package/dist/plugin.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import { c as ManuscriptResult, d as NamespacedId, s as ValidationIssue } from "./question-or-exclamation-spacing-B5o23ZLf.mjs";
2
- import { a as logicalInlineMeasurer, p as ParsedManuscript, t as createNovelComposer } from "./novel-composer-CROZLBqt.mjs";
2
+ import { a as logicalInlineMeasurer, p as ParsedManuscript, t as createNovelComposer } from "./novel-composer-CtovfhET.mjs";
3
3
  export { ManuscriptResult, NamespacedId, ParsedManuscript, ValidationIssue, createNovelComposer, logicalInlineMeasurer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sushichan044/kg-core",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Framework-independent parsing, composition, and proofreading for Japanese manuscripts",
5
5
  "keywords": [
6
6
  "japanese",