@sushichan044/kg-core 0.7.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,18 +84,52 @@ 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 stretching eligible space; a line that admits none
89
- of these is forced. The choice is made for the paragraph rather than line by line,
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
99
+ brackets, commas, full stops, middle dots, hyphens, and an ideographic space is never
100
+ opened up. A line that admits none of these is forced. The choice is made for the
101
+ paragraph rather than line by line,
90
102
  minimizing in order the number of forced lines, then stretched, then hanging, then
91
103
  shrunk lines that spent visible space. Which space may be reduced, and in which
92
104
  order, follows JLReq 3.8.3 and appendix 表3: the space at the line end goes first and
93
105
  costs the paragraph nothing, because it is invisible once that character lands there;
94
- 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
95
108
  closing bracket or comma, then the Japanese-to-Western quarter down to an eighth. The
96
109
  half em after a mid-line full stop marks the end of a sentence and is never reduced,
97
110
  and neither is the half em that keeps a line-head opening bracket off the edge.
98
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
+
99
133
  The composer also owns Japanese line-start and line-end restrictions, inseparable
100
134
  punctuation, question/exclamation gap suppression, and ruby placement.
101
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-Cl6LJXOA.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,27 +356,65 @@ 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
- * 無印 in 表1: the pair is set solid.
376
+ * The stages JLReq 3.8.4 adds inter-character space in, as glue priorities on the same reading as
377
+ * {@link REDUCTION_STAGE}: the lower the number, the earlier the paragraph optimizer takes it. A
378
+ * line is either reduced or expanded, never both, so the two scales never meet.
379
+ *
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.
386
+ */
387
+ const EXPANSION_STAGE = {
388
+ /**
389
+ * Stage 1: the western word space, from its 三分アキ up to a 二分アキ.
390
+ */
391
+ wordSpace: 1,
392
+ /**
393
+ * Stage 2: the quarter em between Japanese and a numeral, unit symbol or western character.
394
+ */
395
+ mixedText: 2,
396
+ /**
397
+ * Stage 3: everywhere else 表6 admits, from solid up to a quarter em.
398
+ */
399
+ solid: 3
400
+ };
401
+ /**
402
+ * 無印 in 表1 with no colour in 表6: the pair is set solid and line adjustment leaves it alone.
370
403
  */
371
404
  const SOLID = {
405
+ kind: "glue",
406
+ naturalWidthEm: 0
407
+ };
408
+ /**
409
+ * `1/4` in 表6: set solid, but line adjustment may open it up to a quarter em.
410
+ */
411
+ const SOLID_EXPANDABLE = {
372
412
  kind: "glue",
373
413
  naturalWidthEm: 0,
374
414
  stretch: {
375
- priority: 3,
376
- amountEm: .25
415
+ priority: EXPANSION_STAGE.solid,
416
+ amountEm: .25,
417
+ granularity: "continuous"
377
418
  }
378
419
  };
379
420
  /**
@@ -384,7 +425,8 @@ const HALF = {
384
425
  naturalWidthEm: .5,
385
426
  shrink: {
386
427
  priority: REDUCTION_STAGE.punctuation,
387
- amountEm: .5
428
+ amountEm: .5,
429
+ granularity: "continuous"
388
430
  }
389
431
  };
390
432
  /**
@@ -402,7 +444,8 @@ const QUARTER = {
402
444
  naturalWidthEm: .25,
403
445
  shrink: {
404
446
  priority: REDUCTION_STAGE.middleDot,
405
- amountEm: .25
447
+ amountEm: .25,
448
+ granularity: "continuous"
406
449
  }
407
450
  };
408
451
  /**
@@ -413,14 +456,34 @@ const FIXED_QUARTER = {
413
456
  naturalWidthEm: .25
414
457
  };
415
458
  /**
416
- * `1/4-1/8` in 表3: the Japanese-to-western quarter em, reducible to an eighth.
459
+ * `1/4-1/8` in 表3 with `1/4-1/2` in 表6: the Japanese-to-western quarter em, reducible to an eighth
460
+ * and expandable to a half.
417
461
  */
418
462
  const MIXED_TEXT_QUARTER = {
419
463
  kind: "glue",
420
464
  naturalWidthEm: .25,
421
465
  shrink: {
422
466
  priority: REDUCTION_STAGE.mixedText,
423
- amountEm: .125
467
+ amountEm: .125,
468
+ granularity: "continuous"
469
+ },
470
+ stretch: {
471
+ priority: EXPANSION_STAGE.mixedText,
472
+ amountEm: .25,
473
+ granularity: "continuous"
474
+ }
475
+ };
476
+ /**
477
+ * The same quarter em where 表6 leaves it out of the expansion: between a unit symbol and the
478
+ * quantity on either side of it.
479
+ */
480
+ const UNIT_SYMBOL_QUARTER = {
481
+ kind: "glue",
482
+ naturalWidthEm: .25,
483
+ shrink: {
484
+ priority: REDUCTION_STAGE.mixedText,
485
+ amountEm: .125,
486
+ granularity: "continuous"
424
487
  }
425
488
  };
426
489
  /**
@@ -431,7 +494,8 @@ const TWO_QUARTERS = {
431
494
  naturalWidthEm: .5,
432
495
  shrink: {
433
496
  priority: REDUCTION_STAGE.middleDot,
434
- amountEm: .5
497
+ amountEm: .5,
498
+ granularity: "continuous"
435
499
  }
436
500
  };
437
501
  /**
@@ -442,7 +506,8 @@ const FIXED_HALF_AND_QUARTER = {
442
506
  naturalWidthEm: .75,
443
507
  shrink: {
444
508
  priority: REDUCTION_STAGE.middleDot,
445
- amountEm: .25
509
+ amountEm: .25,
510
+ granularity: "continuous"
446
511
  }
447
512
  };
448
513
  /**
@@ -457,7 +522,8 @@ const HALF_AND_QUARTER = {
457
522
  naturalWidthEm: .75,
458
523
  shrink: {
459
524
  priority: REDUCTION_STAGE.punctuation,
460
- amountEm: .75
525
+ amountEm: .75,
526
+ granularity: "continuous"
461
527
  }
462
528
  };
463
529
  /**
@@ -468,6 +534,40 @@ const DASH_JOINT = {
468
534
  naturalWidthEm: 0
469
535
  };
470
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
+ /**
471
571
  * The classes that take a quarter em against a numeral, unit symbol or western character (3.2.6).
472
572
  * Dividing punctuation is on the list in that direction only, because the space before a `?` or `!`
473
573
  * is solid (3.1.6).
@@ -502,6 +602,39 @@ const PUNCTUATION_TAKING_SPACE_AFTER = [
502
602
  "cl-07"
503
603
  ];
504
604
  /**
605
+ * 表6 の色付きの小間: the classes a solid pair may be opened up between at stage 3. Neither list holds a
606
+ * bracket, a middle dot, a full stop, a comma, a hyphen, an ideographic space or a word space —
607
+ * JLReq opens none of those — and only the left-hand list holds a numeral, unit symbol or western
608
+ * character, since the space on their Japanese side belongs to stage 2 instead.
609
+ */
610
+ const EXPANDABLE_BEFORE = [
611
+ "cl-08",
612
+ "cl-09",
613
+ "cl-10",
614
+ "cl-11",
615
+ "cl-12",
616
+ "cl-13",
617
+ "cl-15",
618
+ "cl-16",
619
+ "cl-19",
620
+ "cl-24",
621
+ "cl-25",
622
+ "cl-27",
623
+ "cl-30"
624
+ ];
625
+ const EXPANDABLE_AFTER = [
626
+ "cl-08",
627
+ "cl-09",
628
+ "cl-10",
629
+ "cl-11",
630
+ "cl-12",
631
+ "cl-13",
632
+ "cl-15",
633
+ "cl-16",
634
+ "cl-19",
635
+ "cl-30"
636
+ ];
637
+ /**
505
638
  * The classes JLReq 3.1.2 sets on a half em rather than a full one.
506
639
  */
507
640
  const HALF_EM_CLASSES = /* @__PURE__ */ new Set([
@@ -517,6 +650,44 @@ const PAIR_SPACING_TABLE = new Map([
517
650
  right: "any",
518
651
  spacing: SOLID
519
652
  },
653
+ {
654
+ left: EXPANDABLE_BEFORE,
655
+ right: EXPANDABLE_AFTER,
656
+ spacing: SOLID_EXPANDABLE
657
+ },
658
+ {
659
+ left: [
660
+ "cl-08",
661
+ "cl-12",
662
+ "cl-13"
663
+ ],
664
+ right: NUMERAL_UNIT_OR_WESTERN,
665
+ spacing: SOLID_EXPANDABLE
666
+ },
667
+ {
668
+ left: ["cl-08"],
669
+ right: ["cl-24", "cl-25"],
670
+ spacing: SOLID
671
+ },
672
+ {
673
+ left: ["cl-12"],
674
+ right: ["cl-24"],
675
+ spacing: SOLID
676
+ },
677
+ {
678
+ left: ["cl-24"],
679
+ right: ["cl-13"],
680
+ spacing: SOLID
681
+ },
682
+ {
683
+ left: [
684
+ "cl-09",
685
+ "cl-10",
686
+ "cl-11"
687
+ ],
688
+ right: ["cl-08"],
689
+ spacing: SOLID
690
+ },
520
691
  {
521
692
  left: "any",
522
693
  right: ["cl-01"],
@@ -545,12 +716,12 @@ const PAIR_SPACING_TABLE = new Map([
545
716
  {
546
717
  left: ["cl-25"],
547
718
  right: ["cl-24"],
548
- spacing: MIXED_TEXT_QUARTER
719
+ spacing: UNIT_SYMBOL_QUARTER
549
720
  },
550
721
  {
551
722
  left: ["cl-27"],
552
723
  right: ["cl-25"],
553
- spacing: MIXED_TEXT_QUARTER
724
+ spacing: UNIT_SYMBOL_QUARTER
554
725
  },
555
726
  {
556
727
  left: ["cl-24"],
@@ -692,6 +863,10 @@ const defaultJapaneseTypesettingProfile = {
692
863
  * trailing half of its em, a middle dot in the centre, and the rest in the leading half.
693
864
  */
694
865
  boxMetrics: (characterClass, measuredAdvanceEm) => {
866
+ if (characterClass === "cl-26") return {
867
+ advanceEm: 0,
868
+ renderOffsetEm: 0
869
+ };
695
870
  const advanceEm = HALF_EM_CLASSES.has(characterClass) ? Math.min(.5, measuredAdvanceEm) : measuredAdvanceEm;
696
871
  const inkOverhangEm = measuredAdvanceEm - advanceEm;
697
872
  return {
@@ -700,16 +875,18 @@ const defaultJapaneseTypesettingProfile = {
700
875
  };
701
876
  },
702
877
  pairSpacing,
878
+ spacingCharacter: (characterClass, position) => characterClass !== "cl-26" ? null : position === "mid-line" ? WORD_SPACE : EDGE_WORD_SPACE,
703
879
  /**
704
- * An opening bracket at the line head keeps a half em of white, so a line of dialogue starts one
705
- * half em in rather than tight against the edge. JLReq 3.1.5 lists three schemes for that white
706
- * and 表1 注17 sets solid as the principle; the half em is the variant the publishers of Japanese
707
- * 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.
708
885
  *
709
886
  * The white is not shrinkable. 表3 excludes the line head from line adjustment altogether, and
710
887
  * shrinking it is exactly what pushed a line-head bracket flush against the edge in #94.
711
888
  */
712
- lineStartSpacing: (first) => first === "cl-01" ? {
889
+ lineStartSpacing: (first, lineHead) => first === "cl-01" && lineHead === "paragraph-start" ? {
713
890
  kind: "glue",
714
891
  naturalWidthEm: .5
715
892
  } : null,
@@ -717,21 +894,33 @@ const defaultJapaneseTypesettingProfile = {
717
894
  * The half em trailing a closing bracket, full stop or comma at the line end, and the quarter em
718
895
  * trailing a middle dot there (JLReq 3.1.9). Priority 0: spending it changes nothing the reader
719
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).
720
901
  */
721
902
  lineEndSpacing: (last) => last === "cl-02" || last === "cl-06" || last === "cl-07" ? {
722
- kind: "glue",
723
- naturalWidthEm: .5,
724
- shrink: {
725
- priority: REDUCTION_STAGE.lineEnd,
726
- amountEm: .5
727
- }
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
728
913
  } : last === "cl-05" ? {
729
- kind: "glue",
730
- naturalWidthEm: .25,
731
- shrink: {
732
- priority: REDUCTION_STAGE.lineEnd,
733
- amountEm: .25
734
- }
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
735
924
  } : null,
736
925
  breakPenalty,
737
926
  /**
@@ -747,6 +936,13 @@ const EPSILON = 1e-9;
747
936
  * Spending capacity at this priority changes nothing the reader can see.
748
937
  */
749
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
+ }
750
946
  function skipSourceGaps(atoms, start) {
751
947
  let cursor = start;
752
948
  while (atoms[cursor]?.sourceGap === true) cursor += 1;
@@ -755,14 +951,31 @@ function skipSourceGaps(atoms, start) {
755
951
  function previousVisible(atoms, end) {
756
952
  for (let index = end - 1; index >= 0; index -= 1) if (atoms[index]?.sourceGap === false) return index;
757
953
  }
758
- function opportunities(atoms, classes, start, end, profile) {
954
+ function opportunities(atoms, classes, start, end, profile, lineHead) {
759
955
  const result = [];
760
956
  const firstClass = classes[start];
761
- const startSpacing = firstClass === void 0 ? null : profile.lineStartSpacing(firstClass);
957
+ const startSpacing = firstClass === void 0 ? null : profile.lineStartSpacing(firstClass, lineHead);
762
958
  if (startSpacing !== null) result.push({
763
- boundary: start,
764
- spacing: startSpacing
959
+ slot: {
960
+ kind: "gap",
961
+ boundary: start
962
+ },
963
+ spacing: startSpacing,
964
+ absorbsPrecedingEm: 0
765
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
+ }
766
979
  for (let right = start + 1; right < end; right += 1) {
767
980
  const leftAtom = atoms[right - 1];
768
981
  const rightAtom = atoms[right];
@@ -770,73 +983,203 @@ function opportunities(atoms, classes, start, end, profile) {
770
983
  const rightClass = classes[right];
771
984
  if (leftAtom === void 0 || rightAtom === void 0 || leftClass === void 0 || rightClass === void 0 || leftAtom.sourceGap || rightAtom.sourceGap || !leftAtom.pairSpacingAfter) continue;
772
985
  result.push({
773
- boundary: right,
774
- spacing: profile.pairSpacing(leftClass, rightClass)
986
+ slot: {
987
+ kind: "gap",
988
+ boundary: right
989
+ },
990
+ spacing: profile.pairSpacing(leftClass, rightClass),
991
+ absorbsPrecedingEm: 0
775
992
  });
776
993
  }
777
994
  const lastClass = classes[end - 1];
778
995
  const endSpacing = lastClass === void 0 ? null : profile.lineEndSpacing(lastClass);
779
996
  if (endSpacing !== null) result.push({
780
- boundary: end,
781
- spacing: endSpacing
997
+ slot: {
998
+ kind: "gap",
999
+ boundary: end
1000
+ },
1001
+ spacing: endSpacing.spacing,
1002
+ absorbsPrecedingEm: endSpacing.absorbsPrecedingEm
782
1003
  });
783
1004
  return result;
784
1005
  }
785
- function capacity(values, select) {
786
- 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;
787
1066
  }
788
- function resolveSpacings(values, adjustment, amountEm) {
789
- 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();
790
1083
  let remaining = amountEm;
791
1084
  let priorityCost = 0;
792
- const capacities = values.flatMap((opportunity) => {
793
- const selected = adjustment === "shrink" ? opportunity.spacing.shrink : opportunity.spacing.stretch;
794
- return selected === void 0 ? [] : [{
795
- ...opportunity,
796
- capacity: selected
797
- }];
798
- }).sort((left, right) => left.capacity.priority - right.capacity.priority || left.boundary - right.boundary);
799
- 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)) {
800
1103
  if (remaining <= EPSILON) break;
801
- const used = Math.min(remaining, opportunity.capacity.amountEm);
802
- remainingByBoundary.set(opportunity.boundary, used);
803
- priorityCost += used * opportunity.capacity.priority;
804
- 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);
805
1111
  }
806
1112
  return {
807
- spacings: values.map(({ boundary, spacing }) => {
808
- const used = remainingByBoundary.get(boundary) ?? 0;
809
- return {
810
- boundary,
811
- kind: spacing.kind,
812
- naturalWidthEm: spacing.naturalWidthEm,
813
- widthEm: adjustment === "shrink" ? spacing.naturalWidthEm - used : spacing.naturalWidthEm + used
814
- };
1113
+ spacings: splitBySlot(values, (slot, spacing) => {
1114
+ const used = usedBySlot.get(slotKey(slot)) ?? 0;
1115
+ return adjustment === "shrink" ? spacing.naturalWidthEm - used : spacing.naturalWidthEm + used;
815
1116
  }),
816
- 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
817
1160
  };
818
1161
  }
819
1162
  function candidate(atoms, classes, start, end, lineLengthEm, profile) {
820
1163
  const contentStart = skipSourceGaps(atoms, start);
821
1164
  const suppressedIndexes = Array.from({ length: contentStart - start }, (_, index) => start + index);
822
- const pairValues = opportunities(atoms, classes, contentStart, end, profile);
1165
+ const pairValues = opportunities(atoms, classes, contentStart, end, profile, start === 0 ? "paragraph-start" : "turned-over");
823
1166
  const naturalSizeEm = atoms.slice(contentStart, end).reduce((total, atom) => total + atom.boxAdvanceEm, 0) + pairValues.reduce((total, value) => total + value.spacing.naturalWidthEm, 0);
824
1167
  const terminal = skipSourceGaps(atoms, end) === atoms.length;
825
- const shrinkCapacity = capacity(pairValues, ({ shrink }) => shrink);
826
- const freeShrinkCapacity = capacity(pairValues, ({ shrink }) => shrink?.priority === FREE_SHRINK_PRIORITY ? shrink : void 0);
827
- const stretchCapacity = capacity(pairValues, ({ stretch }) => stretch);
828
1168
  const overflow = naturalSizeEm - lineLengthEm;
829
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);
830
1173
  const lastVisible = previousVisible(atoms, end);
831
1174
  const lastClass = lastVisible === void 0 ? void 0 : classes[lastVisible];
832
1175
  const lastAdvance = lastVisible === void 0 ? 0 : atoms[lastVisible]?.boxAdvanceEm ?? 0;
833
- 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;
834
1177
  if (terminal && overflow <= EPSILON) return {
835
1178
  start,
836
1179
  contentStart,
837
1180
  end,
838
1181
  suppressedIndexes,
839
- pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings,
1182
+ ...naturalSpacings(pairValues),
840
1183
  inlineSizeEm: naturalSizeEm,
841
1184
  break: { kind: "paragraph-end" },
842
1185
  hangingIndex: null,
@@ -848,28 +1191,28 @@ function candidate(atoms, classes, start, end, lineLengthEm, profile) {
848
1191
  contentStart,
849
1192
  end,
850
1193
  suppressedIndexes,
851
- pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings,
1194
+ ...naturalSpacings(pairValues),
852
1195
  inlineSizeEm: naturalSizeEm,
853
1196
  break: { kind: "natural" },
854
1197
  hangingIndex: null,
855
1198
  deformationRatio: 0,
856
1199
  priorityCost: 0
857
1200
  };
858
- if (overflow > 0 && overflow <= shrinkCapacity + EPSILON) {
859
- const resolved = resolveSpacings(pairValues, "shrink", overflow);
860
- const chargeableCapacity = shrinkCapacity - freeShrinkCapacity;
861
- 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);
862
1205
  return {
863
1206
  start,
864
1207
  contentStart,
865
1208
  end,
866
1209
  suppressedIndexes,
867
- pairSpacings: resolved.spacings,
1210
+ ...shrunk.spacings,
868
1211
  inlineSizeEm: lineLengthEm,
869
1212
  break: { kind: "shrunk" },
870
1213
  hangingIndex: null,
871
1214
  deformationRatio: chargeableCapacity <= EPSILON ? 0 : chargeableShrink / chargeableCapacity,
872
- priorityCost: resolved.priorityCost
1215
+ priorityCost: shrunk.priorityCost
873
1216
  };
874
1217
  }
875
1218
  if (!terminal && overflow > 0 && lastVisible !== void 0 && lastClass !== void 0 && profile.canHang(lastClass) && naturalSizeEm - lastAdvance - trailingSpacing <= lineLengthEm + EPSILON) return {
@@ -877,34 +1220,32 @@ function candidate(atoms, classes, start, end, lineLengthEm, profile) {
877
1220
  contentStart,
878
1221
  end,
879
1222
  suppressedIndexes,
880
- pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings.filter(({ boundary }) => boundary !== end),
1223
+ ...hangingSpacings(pairValues, end),
881
1224
  inlineSizeEm: naturalSizeEm - lastAdvance - trailingSpacing,
882
1225
  break: { kind: "hanging" },
883
1226
  hangingIndex: lastVisible,
884
1227
  deformationRatio: overflow / Math.max(lastAdvance, EPSILON),
885
1228
  priorityCost: 0
886
1229
  };
887
- if (!terminal && underflow > EPSILON && underflow <= stretchCapacity + EPSILON) {
888
- const resolved = resolveSpacings(pairValues, "stretch", underflow);
889
- return {
890
- start,
891
- contentStart,
892
- end,
893
- suppressedIndexes,
894
- pairSpacings: resolved.spacings,
895
- inlineSizeEm: lineLengthEm,
896
- break: { kind: "stretched" },
897
- hangingIndex: null,
898
- deformationRatio: stretchCapacity === 0 ? 0 : underflow / stretchCapacity,
899
- priorityCost: resolved.priorityCost
900
- };
901
- }
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
+ };
902
1243
  return {
903
1244
  start,
904
1245
  contentStart,
905
1246
  end,
906
1247
  suppressedIndexes,
907
- pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings,
1248
+ ...naturalSpacings(pairValues),
908
1249
  inlineSizeEm: naturalSizeEm,
909
1250
  break: { kind: "forced" },
910
1251
  hangingIndex: null,
@@ -1644,7 +1985,9 @@ function positionedLine(atoms, sourceGapIndexes, plan) {
1644
1985
  }];
1645
1986
  });
1646
1987
  const spacings = new Map(plan.pairSpacings.map((spacing) => [spacing.boundary, spacing]));
1988
+ const characterSpacings = new Map(plan.characterSpacings.map((spacing) => [spacing.index, spacing]));
1647
1989
  let offsetEm = 0;
1990
+ const adjustmentOf = (widthEm, naturalWidthEm) => widthEm < naturalWidthEm ? "shrunk" : widthEm > naturalWidthEm ? "stretched" : "natural";
1648
1991
  const positionSpacing = (spacing) => {
1649
1992
  if (spacing.kind === "kern") items.push({
1650
1993
  kind: "kern",
@@ -1657,7 +2000,7 @@ function positionedLine(atoms, sourceGapIndexes, plan) {
1657
2000
  offsetEm,
1658
2001
  widthEm: spacing.widthEm,
1659
2002
  naturalWidthEm: spacing.naturalWidthEm,
1660
- adjustment: spacing.widthEm < spacing.naturalWidthEm ? "shrunk" : spacing.widthEm > spacing.naturalWidthEm ? "stretched" : "natural"
2003
+ adjustment: adjustmentOf(spacing.widthEm, spacing.naturalWidthEm)
1661
2004
  });
1662
2005
  offsetEm += spacing.widthEm;
1663
2006
  };
@@ -1666,18 +2009,21 @@ function positionedLine(atoms, sourceGapIndexes, plan) {
1666
2009
  if (spacing !== void 0) positionSpacing(spacing);
1667
2010
  const atom = atoms[index];
1668
2011
  if (atom === void 0) continue;
1669
- 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;
1670
2016
  items.push({
1671
2017
  kind: "glue",
1672
2018
  origin: "source",
1673
2019
  value: atom.grapheme.value,
1674
2020
  range: atom.grapheme.range,
1675
2021
  offsetEm,
1676
- widthEm: atom.boxAdvanceEm,
1677
- naturalWidthEm: atom.boxAdvanceEm,
1678
- adjustment: "natural"
2022
+ widthEm,
2023
+ naturalWidthEm,
2024
+ adjustment: adjustmentOf(widthEm, naturalWidthEm)
1679
2025
  });
1680
- offsetEm += atom.boxAdvanceEm;
2026
+ offsetEm += widthEm;
1681
2027
  continue;
1682
2028
  }
1683
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-Cl6LJXOA.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.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Framework-independent parsing, composition, and proofreading for Japanese manuscripts",
5
5
  "keywords": [
6
6
  "japanese",