@sushichan044/kg-core 0.9.1 → 0.9.3

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
@@ -81,8 +81,11 @@ The composer classifies the core prose classes from JLReq, resolves pair spacing
81
81
  and selects line breaks over the whole source paragraph. The space between two
82
82
  classes follows appendix 表1 of JLReq: a half em before an opening bracket and after
83
83
  a closing bracket, comma, or full stop, a quarter em on either side of a middle dot,
84
- a quarter em between Japanese and a numeral, unit symbol, or Western character, and
85
- solid where two of those punctuation marks meet.
84
+ a quarter em between Japanese and a numeral, unit symbol, or a Western character rotated
85
+ 90 degrees or mixed into horizontal text, and solid where two of those punctuation marks
86
+ meet. JLReq 3.2.4 sets that quarter em's scope: a Western character set upright — one letter,
87
+ an all-caps or `Xxx` abbreviation, a lone digit, or a full-width letter — classes with kanji
88
+ instead and sits solid against the kana or kanji beside it, the same as a tate-chu-yoko run.
86
89
 
87
90
  A western word space is not a character with a box but an アキ of its own: a third em
88
91
  by rule rather than by measurement, nothing at all at a line head or a line end, and
@@ -95,9 +98,15 @@ stage, or by hanging a comma or full stop; a short line is answered by opening u
95
98
  where JLReq 3.8.4 and appendix 表6
96
99
  allow it — the word space first, up to a half, then the Japanese-to-Western quarter em,
97
100
  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
+ them, up to a quarter. If those stages cannot fill the line, the remaining width is
102
+ added equally to the admitted pairs in 表6 that are solid at rest, including pairs used by
103
+ the third stage. Intrinsic spaces keep their finite limits: a word space and a mixed
104
+ Japanese-to-Western, numeral, or unit-symbol gap stop at a half em, while punctuation and
105
+ explicit full-em spaces retain their own width. This keeps the unbounded stage from producing
106
+ double space or a hole disproportionate to a horizontal Western run's own length, since an
107
+ upright Western run classes with kanji and has no gap of its own to begin with.
108
+ Line-head and line-end space, inseparable pairs, and source-authored full-em gaps after
109
+ question or exclamation marks stay fixed. A line with no admitted gap is forced. The choice is made for the
101
110
  paragraph rather than line by line,
102
111
  minimizing in order the number of forced lines, then stretched, then hanging, then
103
112
  shrunk lines that spent visible space. Which space may be reduced, and in which
@@ -140,6 +149,11 @@ words remain unbroken and render sideways, and two ASCII digits form one
140
149
  tate-chu-yoko unit. The resulting `VerticalTextPresentation` is carried by every
141
150
  glyph item, so renderers do not need to infer orientation independently.
142
151
 
152
+ An upright Western character sets solid against the kana or kanji beside it rather than
153
+ taking the quarter em 3.2.6 puts around a sideways run, following JLReq 3.2.4: it classes
154
+ with kanji, the same way 3.2.5 classes a tate-chu-yoko run. `QR` and `URL` set upright by
155
+ this rule read flush against their surrounding Japanese text, the way a kanji compound does.
156
+
143
157
  The default logical measurer uses East Asian Width in a Japanese context. Upright
144
158
  ASCII advances by one em; proportional ASCII and the members of a tate-chu-yoko unit
145
159
  advance by half an em. Supply a synchronous measurer when the caller has more accurate
@@ -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-CtovfhET.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-DRgsEA2s.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
  /**
@@ -306,11 +306,19 @@ const HIRAGANA = /^\p{Script=Hiragana}$/u;
306
306
  const KATAKANA = /^\p{Script=Katakana}$/u;
307
307
  const WESTERN = /^[\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}]$/u;
308
308
  /**
309
- * Order matters: a character inside a tate-chu-yoko group is cl-30 whatever it would be on its own,
310
- * and anything left unmatched is cl-19, the class JLReq reserves for kanji and the like.
309
+ * Order matters: a character inside a tate-chu-yoko group is cl-30 (3.2.5) and an upright Latin
310
+ * letter or Arabic numeral is cl-19 (3.2.4), whatever either would be on its own; anything left
311
+ * unmatched is cl-19 too, the class JLReq reserves for kanji and the like.
312
+ *
313
+ * The quarter em 3.2.6 sets between Japanese and a Western character or numeral applies only to a
314
+ * character rotated 90 degrees in vertical writing mode or mixed into horizontal writing mode —
315
+ * 3.2.6 states that scope explicitly. A character set upright keeps its own rule, 3.2.4, which
316
+ * reads it as a full-width monospace glyph classed with kanji and set solid against its neighbours,
317
+ * the same as a tate-chu-yoko run's 3.2.5.
311
318
  */
312
319
  function classify({ value, presentation }) {
313
320
  if (presentation === "tate-chu-yoko") return "cl-30";
321
+ if (presentation === "upright") return "cl-19";
314
322
  if (OPENING.has(value)) return "cl-01";
315
323
  if (CLOSING.has(value)) return "cl-02";
316
324
  if (HYPHENS.has(value)) return "cl-03";
@@ -377,10 +385,6 @@ const REDUCTION_STAGE = {
377
385
  * {@link REDUCTION_STAGE}: the lower the number, the earlier the paragraph optimizer takes it. A
378
386
  * line is either reduced or expanded, never both, so the two scales never meet.
379
387
  *
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
388
  * Nothing here takes priority 0. Expanding is always visible, and priority 0 means an adjustment
385
389
  * the optimizer is charged nothing for.
386
390
  */
@@ -396,7 +400,11 @@ const EXPANSION_STAGE = {
396
400
  /**
397
401
  * Stage 3: everywhere else 表6 admits, from solid up to a quarter em.
398
402
  */
399
- solid: 3
403
+ solid: 3,
404
+ /**
405
+ * Stage 4: the unbounded remainder, spread equally across every colored or gray cell of 表6.
406
+ */
407
+ final: 4
400
408
  };
401
409
  /**
402
410
  * 無印 in 表1 with no colour in 表6: the pair is set solid and line adjustment leaves it alone.
@@ -457,7 +465,9 @@ const FIXED_QUARTER = {
457
465
  };
458
466
  /**
459
467
  * `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.
468
+ * and expandable to a half. 3.2.6 sets this only against a Western character rotated 90 degrees or
469
+ * mixed into horizontal text (cl-27) and a numeral or unit symbol (cl-24, cl-25); an upright
470
+ * Western character classifies as cl-19 instead and never reaches this constant.
461
471
  */
462
472
  const MIXED_TEXT_QUARTER = {
463
473
  kind: "glue",
@@ -568,9 +578,10 @@ const EDGE_WORD_SPACE = {
568
578
  naturalWidthEm: 0
569
579
  };
570
580
  /**
571
- * The classes that take a quarter em against a numeral, unit symbol or western character (3.2.6).
572
- * Dividing punctuation is on the list in that direction only, because the space before a `?` or `!`
573
- * is solid (3.1.6).
581
+ * The classes that take a quarter em against a numeral, unit symbol or western character rotated 90
582
+ * degrees or mixed into horizontal text (3.2.6) — an upright western character classifies as cl-19
583
+ * and is covered by the general kanji rows and columns instead. Dividing punctuation is on the list
584
+ * in that direction only, because the space before a `?` or `!` is solid (3.1.6).
574
585
  */
575
586
  const JAPANESE_BEFORE_WESTERN = [
576
587
  "cl-04",
@@ -635,6 +646,125 @@ const EXPANDABLE_AFTER = [
635
646
  "cl-30"
636
647
  ];
637
648
  /**
649
+ * The gray, blue and pink cells of 表6: all pairs that join the fourth and final expansion stage.
650
+ * The finite capacities above cover the blue and pink cells at stages three and two; once those
651
+ * stages are exhausted, JLReq 3.8.4 d adds the remainder equally across every cell named here.
652
+ *
653
+ * A cl-08/cl-08 cell is blue only when the two characters are different kinds of mark (注4). This
654
+ * profile cannot distinguish the two halves of one 2倍ダッシュ from different marks, so it keeps the
655
+ * pair joined, as it already does for the finite third stage.
656
+ */
657
+ const FINAL_EXPANSION_BASIC_AFTER = /* @__PURE__ */ new Set([
658
+ "cl-01",
659
+ "cl-08",
660
+ "cl-12",
661
+ "cl-13",
662
+ "cl-14",
663
+ "cl-15",
664
+ "cl-16",
665
+ "cl-19",
666
+ "cl-24",
667
+ "cl-25",
668
+ "cl-26",
669
+ "cl-27",
670
+ "cl-30"
671
+ ]);
672
+ const FINAL_EXPANSION_WIDE_AFTER = /* @__PURE__ */ new Set([
673
+ ...FINAL_EXPANSION_BASIC_AFTER,
674
+ "cl-09",
675
+ "cl-10",
676
+ "cl-11"
677
+ ]);
678
+ const FINAL_EXPANSION_AFTER = new Map([
679
+ ...[
680
+ "cl-02",
681
+ "cl-03",
682
+ "cl-04",
683
+ "cl-05",
684
+ "cl-06",
685
+ "cl-07",
686
+ "cl-14"
687
+ ].map((left) => [left, FINAL_EXPANSION_BASIC_AFTER]),
688
+ ...[
689
+ "cl-09",
690
+ "cl-10",
691
+ "cl-11",
692
+ "cl-13",
693
+ "cl-15",
694
+ "cl-16",
695
+ "cl-19",
696
+ "cl-30"
697
+ ].map((left) => [left, FINAL_EXPANSION_WIDE_AFTER]),
698
+ ["cl-08", new Set([...FINAL_EXPANSION_WIDE_AFTER].filter((characterClass) => characterClass !== "cl-08"))],
699
+ ["cl-12", /* @__PURE__ */ new Set([
700
+ "cl-08",
701
+ "cl-09",
702
+ "cl-10",
703
+ "cl-11",
704
+ "cl-12",
705
+ "cl-13",
706
+ "cl-15",
707
+ "cl-16",
708
+ "cl-19",
709
+ "cl-25",
710
+ "cl-26",
711
+ "cl-27",
712
+ "cl-30"
713
+ ])],
714
+ ["cl-24", /* @__PURE__ */ new Set([
715
+ "cl-01",
716
+ "cl-08",
717
+ "cl-09",
718
+ "cl-10",
719
+ "cl-11",
720
+ "cl-12",
721
+ "cl-14",
722
+ "cl-15",
723
+ "cl-16",
724
+ "cl-19",
725
+ "cl-25",
726
+ "cl-26",
727
+ "cl-30"
728
+ ])],
729
+ ["cl-25", /* @__PURE__ */ new Set([
730
+ "cl-01",
731
+ "cl-08",
732
+ "cl-09",
733
+ "cl-10",
734
+ "cl-11",
735
+ "cl-12",
736
+ "cl-13",
737
+ "cl-14",
738
+ "cl-15",
739
+ "cl-16",
740
+ "cl-19",
741
+ "cl-24",
742
+ "cl-26",
743
+ "cl-30"
744
+ ])],
745
+ ["cl-26", new Set(japaneseCharacterClasses.filter((characterClass) => characterClass !== "cl-02"))],
746
+ ["cl-27", /* @__PURE__ */ new Set([
747
+ "cl-01",
748
+ "cl-08",
749
+ "cl-09",
750
+ "cl-10",
751
+ "cl-11",
752
+ "cl-12",
753
+ "cl-13",
754
+ "cl-14",
755
+ "cl-15",
756
+ "cl-16",
757
+ "cl-19",
758
+ "cl-26",
759
+ "cl-30"
760
+ ])]
761
+ ]);
762
+ function canExpandAtFinalStage(left, right) {
763
+ if (left === "cl-14" || left === "cl-26" || left === "cl-27" || right === "cl-14" || right === "cl-26" || right === "cl-27") return false;
764
+ if (FINAL_EXPANSION_AFTER.get(left)?.has(right) !== true) return false;
765
+ return pairSpacing(left, right).naturalWidthEm === 0;
766
+ }
767
+ /**
638
768
  * The classes JLReq 3.1.2 sets on a half em rather than a full one.
639
769
  */
640
770
  const HALF_EM_CLASSES = /* @__PURE__ */ new Set([
@@ -922,6 +1052,8 @@ const defaultJapaneseTypesettingProfile = {
922
1052
  },
923
1053
  absorbsPrecedingEm: .25
924
1054
  } : null,
1055
+ finalStretchPriority: EXPANSION_STAGE.final,
1056
+ canExpandAtFinalStage,
925
1057
  breakPenalty,
926
1058
  /**
927
1059
  * Hanging punctuation (ぶら下げ組, JLReq 3.8.2) lets a full stop or comma sit outside the text area
@@ -951,6 +1083,15 @@ function skipSourceGaps(atoms, start) {
951
1083
  function previousVisible(atoms, end) {
952
1084
  for (let index = end - 1; index >= 0; index -= 1) if (atoms[index]?.sourceGap === false) return index;
953
1085
  }
1086
+ /**
1087
+ * Whether a shorter line ending somewhere inside `(contentStart, end)` was ever an option — checked
1088
+ * as a plain short-circuiting scan rather than building the boundary list first, since this runs
1089
+ * once per over-long candidate the optimizer considers.
1090
+ */
1091
+ function hasEarlierBoundary(boundaryAllowed, contentStart, end) {
1092
+ for (let boundary = contentStart + 1; boundary < end; boundary += 1) if (boundaryAllowed(boundary - 1, boundary)) return true;
1093
+ return false;
1094
+ }
954
1095
  function opportunities(atoms, classes, start, end, profile, lineHead) {
955
1096
  const result = [];
956
1097
  const firstClass = classes[start];
@@ -961,6 +1102,7 @@ function opportunities(atoms, classes, start, end, profile, lineHead) {
961
1102
  boundary: start
962
1103
  },
963
1104
  spacing: startSpacing,
1105
+ finalStretch: false,
964
1106
  absorbsPrecedingEm: 0
965
1107
  });
966
1108
  for (let index = start; index < end; index += 1) {
@@ -973,6 +1115,7 @@ function opportunities(atoms, classes, start, end, profile, lineHead) {
973
1115
  index
974
1116
  },
975
1117
  spacing,
1118
+ finalStretch: false,
976
1119
  absorbsPrecedingEm: 0
977
1120
  });
978
1121
  }
@@ -988,6 +1131,7 @@ function opportunities(atoms, classes, start, end, profile, lineHead) {
988
1131
  boundary: right
989
1132
  },
990
1133
  spacing: profile.pairSpacing(leftClass, rightClass),
1134
+ finalStretch: profile.canExpandAtFinalStage(leftClass, rightClass),
991
1135
  absorbsPrecedingEm: 0
992
1136
  });
993
1137
  }
@@ -999,6 +1143,7 @@ function opportunities(atoms, classes, start, end, profile, lineHead) {
999
1143
  boundary: end
1000
1144
  },
1001
1145
  spacing: endSpacing.spacing,
1146
+ finalStretch: false,
1002
1147
  absorbsPrecedingEm: endSpacing.absorbsPrecedingEm
1003
1148
  });
1004
1149
  return result;
@@ -1072,17 +1217,20 @@ function adjustmentUnits(values, adjustment) {
1072
1217
  * Within a stage the amount is therefore split in proportion to what each space can give, rather
1073
1218
  * than taken out of the earliest space until it runs dry. Across stages the order stays a
1074
1219
  * 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.
1220
+ * sets one character size, so a share of the stage's capacity is a share by character size. JLReq
1221
+ * 3.8.4's final stage has no upper bound: after the finite stages are full, the unresolved
1222
+ * remainder is added equally to every pair the profile admits, including the earlier-stage pairs.
1076
1223
  *
1077
1224
  * An all-or-nothing unit larger than what is still needed is skipped rather than partly spent, and
1078
1225
  * never revisited: `remaining` only falls, so a unit that did not fit at its own stage cannot fit
1079
1226
  * at a later one. One forward pass is therefore exact and no search over subsets is needed.
1080
1227
  */
1081
- function resolveSpacings(values, units, adjustment, amountEm) {
1228
+ function resolveSpacings(values, units, adjustment, amountEm, finalStretchPriority) {
1082
1229
  const usedBySlot = /* @__PURE__ */ new Map();
1083
1230
  let remaining = amountEm;
1084
1231
  let priorityCost = 0;
1085
1232
  let freeEm = 0;
1233
+ let finalStretchPerSlotEm = 0;
1086
1234
  const spend = (unit, fraction) => {
1087
1235
  for (const { slot, amountEm: partEm } of unit.parts) {
1088
1236
  const key = slotKey(slot);
@@ -1109,6 +1257,20 @@ function resolveSpacings(values, units, adjustment, amountEm) {
1109
1257
  const fraction = Math.min(remaining, stageCapacityEm) / stageCapacityEm;
1110
1258
  for (const unit of stage) if (unit.granularity === "continuous") spend(unit, fraction);
1111
1259
  }
1260
+ if (adjustment === "stretch" && remaining > EPSILON) {
1261
+ let finalSlotCount = 0;
1262
+ for (const { finalStretch } of values) if (finalStretch) finalSlotCount += 1;
1263
+ if (finalSlotCount > 0) {
1264
+ finalStretchPerSlotEm = remaining / finalSlotCount;
1265
+ for (const { slot, finalStretch } of values) {
1266
+ if (!finalStretch) continue;
1267
+ const key = slotKey(slot);
1268
+ usedBySlot.set(key, (usedBySlot.get(key) ?? 0) + finalStretchPerSlotEm);
1269
+ }
1270
+ priorityCost += remaining * finalStretchPriority;
1271
+ remaining = 0;
1272
+ }
1273
+ }
1112
1274
  return {
1113
1275
  spacings: splitBySlot(values, (slot, spacing) => {
1114
1276
  const used = usedBySlot.get(slotKey(slot)) ?? 0;
@@ -1116,6 +1278,7 @@ function resolveSpacings(values, units, adjustment, amountEm) {
1116
1278
  }),
1117
1279
  priorityCost,
1118
1280
  freeEm,
1281
+ finalStretchPerSlotEm,
1119
1282
  unabsorbedEm: Math.max(0, remaining)
1120
1283
  };
1121
1284
  }
@@ -1159,11 +1322,11 @@ function hangingSpacings(values, end) {
1159
1322
  characterSpacings
1160
1323
  };
1161
1324
  }
1162
- function candidate(atoms, classes, start, end, lineLengthEm, profile) {
1325
+ function candidate(atoms, classes, boxPrefixEm, start, end, lineLengthEm, profile) {
1163
1326
  const contentStart = skipSourceGaps(atoms, start);
1164
1327
  const suppressedIndexes = Array.from({ length: contentStart - start }, (_, index) => start + index);
1165
1328
  const pairValues = opportunities(atoms, classes, contentStart, end, profile, start === 0 ? "paragraph-start" : "turned-over");
1166
- const naturalSizeEm = atoms.slice(contentStart, end).reduce((total, atom) => total + atom.boxAdvanceEm, 0) + pairValues.reduce((total, value) => total + value.spacing.naturalWidthEm, 0);
1329
+ const naturalSizeEm = (boxPrefixEm[end] ?? 0) - (boxPrefixEm[contentStart] ?? 0) + pairValues.reduce((total, value) => total + value.spacing.naturalWidthEm, 0);
1167
1330
  const terminal = skipSourceGaps(atoms, end) === atoms.length;
1168
1331
  const overflow = naturalSizeEm - lineLengthEm;
1169
1332
  const underflow = lineLengthEm - naturalSizeEm;
@@ -1174,6 +1337,7 @@ function candidate(atoms, classes, start, end, lineLengthEm, profile) {
1174
1337
  const lastClass = lastVisible === void 0 ? void 0 : classes[lastVisible];
1175
1338
  const lastAdvance = lastVisible === void 0 ? 0 : atoms[lastVisible]?.boxAdvanceEm ?? 0;
1176
1339
  const trailingSpacing = pairValues.find(({ slot }) => slot.kind === "gap" && slot.boundary === end)?.spacing.naturalWidthEm ?? 0;
1340
+ const hasFinalStretch = pairValues.some(({ finalStretch }) => finalStretch);
1177
1341
  if (terminal && overflow <= EPSILON) return {
1178
1342
  start,
1179
1343
  contentStart,
@@ -1198,7 +1362,7 @@ function candidate(atoms, classes, start, end, lineLengthEm, profile) {
1198
1362
  deformationRatio: 0,
1199
1363
  priorityCost: 0
1200
1364
  };
1201
- const shrunk = overflow > 0 && overflow <= capacity + EPSILON ? resolveSpacings(pairValues, units, "shrink", overflow) : null;
1365
+ const shrunk = overflow > 0 && overflow <= capacity + EPSILON ? resolveSpacings(pairValues, units, "shrink", overflow, profile.finalStretchPriority) : null;
1202
1366
  if (shrunk !== null && shrunk.unabsorbedEm <= EPSILON) {
1203
1367
  const chargeableCapacity = capacity - freeShrinkCapacity;
1204
1368
  const chargeableShrink = Math.max(0, overflow - shrunk.freeEm);
@@ -1227,7 +1391,7 @@ function candidate(atoms, classes, start, end, lineLengthEm, profile) {
1227
1391
  deformationRatio: overflow / Math.max(lastAdvance, EPSILON),
1228
1392
  priorityCost: 0
1229
1393
  };
1230
- const stretched = !terminal && underflow > EPSILON && underflow <= capacity + EPSILON ? resolveSpacings(pairValues, units, "stretch", underflow) : null;
1394
+ const stretched = !terminal && underflow > EPSILON && (underflow <= capacity + EPSILON || hasFinalStretch) ? resolveSpacings(pairValues, units, "stretch", underflow, profile.finalStretchPriority) : null;
1231
1395
  if (stretched !== null && stretched.unabsorbedEm <= EPSILON) return {
1232
1396
  start,
1233
1397
  contentStart,
@@ -1237,7 +1401,7 @@ function candidate(atoms, classes, start, end, lineLengthEm, profile) {
1237
1401
  inlineSizeEm: lineLengthEm,
1238
1402
  break: { kind: "stretched" },
1239
1403
  hangingIndex: null,
1240
- deformationRatio: capacity === 0 ? 0 : underflow / capacity,
1404
+ deformationRatio: stretched.finalStretchPerSlotEm > 0 ? 1 + stretched.finalStretchPerSlotEm : capacity === 0 ? 0 : underflow / capacity,
1241
1405
  priorityCost: stretched.priorityCost
1242
1406
  };
1243
1407
  return {
@@ -1287,6 +1451,22 @@ function addScore(left, right) {
1287
1451
  left[6] + right[6]
1288
1452
  ];
1289
1453
  }
1454
+ /**
1455
+ * The line-end offsets a state's path took, earliest first. Rebuilt on demand by walking `previous`
1456
+ * rather than carried on every `State`: every candidate line attempted would otherwise pay for a
1457
+ * copy of its whole path, when `isBetter` only ever reads it on the exact-score tie this comparison
1458
+ * breaks — for `boundaryAllowed` predicates permissive enough to fully saturate the optimizer's
1459
+ * search window, ties are rare enough that this deferred cost is negligible.
1460
+ */
1461
+ function breaksOf(state, states) {
1462
+ const result = [];
1463
+ let cursor = state;
1464
+ while (cursor?.line !== null && cursor?.line !== void 0) {
1465
+ result.push(cursor.line.end);
1466
+ cursor = cursor.previous === null ? void 0 : states.get(cursor.previous.index)?.get(cursor.previous.fitness);
1467
+ }
1468
+ return result.reverse();
1469
+ }
1290
1470
  function compareBreaks(left, right) {
1291
1471
  const length = Math.min(left.length, right.length);
1292
1472
  for (let index = 0; index < length; index += 1) {
@@ -1295,17 +1475,19 @@ function compareBreaks(left, right) {
1295
1475
  }
1296
1476
  return left.length - right.length;
1297
1477
  }
1298
- function isBetter(candidateState, current) {
1478
+ function isBetter(candidateState, current, states) {
1299
1479
  if (current === void 0) return true;
1300
1480
  for (let index = 0; index < candidateState.score.length; index += 1) {
1301
1481
  const difference = (candidateState.score[index] ?? 0) - (current.score[index] ?? 0);
1302
1482
  if (Math.abs(difference) > EPSILON) return difference < 0;
1303
1483
  }
1304
- return compareBreaks(candidateState.breaks, current.breaks) < 0;
1484
+ return compareBreaks(breaksOf(candidateState, states), breaksOf(current, states)) < 0;
1305
1485
  }
1306
1486
  function layoutParagraph(atoms, lineLengthEm, profile, boundaryAllowed) {
1307
1487
  if (atoms.length === 0) return [];
1308
1488
  const classes = atoms.map(({ characterClass }) => characterClass);
1489
+ const boxPrefixEm = Array.from({ length: atoms.length + 1 }).fill(0);
1490
+ for (let index = 0; index < atoms.length; index += 1) boxPrefixEm[index + 1] = (boxPrefixEm[index] ?? 0) + (atoms[index]?.boxAdvanceEm ?? 0);
1309
1491
  const states = /* @__PURE__ */ new Map([[0, /* @__PURE__ */ new Map([[0, {
1310
1492
  score: [
1311
1493
  0,
@@ -1318,8 +1500,7 @@ function layoutParagraph(atoms, lineLengthEm, profile, boundaryAllowed) {
1318
1500
  ],
1319
1501
  fitness: 0,
1320
1502
  previous: null,
1321
- line: null,
1322
- breaks: []
1503
+ line: null
1323
1504
  }]])]]);
1324
1505
  for (let start = 0; start < atoms.length; start += 1) {
1325
1506
  const activeStates = states.get(start);
@@ -1327,7 +1508,7 @@ function layoutParagraph(atoms, lineLengthEm, profile, boundaryAllowed) {
1327
1508
  const contentStart = skipSourceGaps(atoms, start);
1328
1509
  if (contentStart === atoms.length) {
1329
1510
  const terminalStates = states.get(atoms.length) ?? /* @__PURE__ */ new Map();
1330
- for (const state of activeStates.values()) if (isBetter(state, terminalStates.get(state.fitness))) terminalStates.set(state.fitness, state);
1511
+ for (const state of activeStates.values()) if (isBetter(state, terminalStates.get(state.fitness), states)) terminalStates.set(state.fitness, state);
1331
1512
  states.set(atoms.length, terminalStates);
1332
1513
  continue;
1333
1514
  }
@@ -1336,8 +1517,8 @@ function layoutParagraph(atoms, lineLengthEm, profile, boundaryAllowed) {
1336
1517
  const left = previousVisible(atoms, end);
1337
1518
  if (atoms[end - 1]?.sourceGap === true) continue;
1338
1519
  if (end < atoms.length && right < atoms.length && (left === void 0 || !boundaryAllowed(left, right))) continue;
1339
- const line = candidate(atoms, classes, start, end, lineLengthEm, profile);
1340
- if (line.break.kind === "forced" && line.inlineSizeEm > lineLengthEm + EPSILON && Array.from({ length: end - contentStart - 1 }, (_, index) => contentStart + index + 1).some((boundary) => boundaryAllowed(boundary - 1, boundary))) {
1520
+ const line = candidate(atoms, classes, boxPrefixEm, start, end, lineLengthEm, profile);
1521
+ if (line.break.kind === "forced" && line.inlineSizeEm > lineLengthEm + EPSILON && hasEarlierBoundary(boundaryAllowed, contentStart, end)) {
1341
1522
  if (line.inlineSizeEm > lineLengthEm * 2 && end > contentStart + 1) break;
1342
1523
  continue;
1343
1524
  }
@@ -1349,18 +1530,17 @@ function layoutParagraph(atoms, lineLengthEm, profile, boundaryAllowed) {
1349
1530
  index: start,
1350
1531
  fitness: state.fitness
1351
1532
  },
1352
- line,
1353
- breaks: [...state.breaks, end]
1533
+ line
1354
1534
  };
1355
1535
  const statesAtEnd = states.get(end) ?? /* @__PURE__ */ new Map();
1356
- if (isBetter(nextState, statesAtEnd.get(nextState.fitness))) {
1536
+ if (isBetter(nextState, statesAtEnd.get(nextState.fitness), states)) {
1357
1537
  statesAtEnd.set(nextState.fitness, nextState);
1358
1538
  states.set(end, statesAtEnd);
1359
1539
  }
1360
1540
  if (line.inlineSizeEm > lineLengthEm * 2 && line.break.kind === "forced" && end > contentStart + 1) break;
1361
1541
  }
1362
1542
  }
1363
- const terminal = [...states.get(atoms.length)?.values() ?? []].reduce((best, state) => isBetter(state, best) ? state : best, void 0);
1543
+ const terminal = [...states.get(atoms.length)?.values() ?? []].reduce((best, state) => isBetter(state, best, states) ? state : best, void 0);
1364
1544
  if (terminal === void 0) return [];
1365
1545
  const lines = [];
1366
1546
  let cursor = terminal;
@@ -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-CtovfhET.mjs";
2
+ import { a as logicalInlineMeasurer, p as ParsedManuscript, t as createNovelComposer } from "./novel-composer-DRgsEA2s.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.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Framework-independent parsing, composition, and proofreading for Japanese manuscripts",
5
5
  "keywords": [
6
6
  "japanese",