@sushichan044/kg-core 0.9.0 → 0.9.2

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
@@ -95,9 +95,14 @@ stage, or by hanging a comma or full stop; a short line is answered by opening u
95
95
  where JLReq 3.8.4 and appendix 表6
96
96
  allow it — the word space first, up to a half, then the Japanese-to-Western quarter em,
97
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
98
+ them, up to a quarter. If those stages cannot fill the line, the remaining width is
99
+ added equally to the admitted pairs in 表6 that are solid at rest, including pairs used by
100
+ the third stage. Intrinsic spaces keep their finite limits: a word space and a mixed
101
+ Japanese-to-Western, numeral, or unit-symbol gap stop at a half em, while punctuation and
102
+ explicit full-em spaces retain their own width. This keeps the unbounded stage from producing
103
+ conspicuous holes around short upright Western words or already-spaced marks.
104
+ Line-head and line-end space, inseparable pairs, and source-authored full-em gaps after
105
+ question or exclamation marks stay fixed. A line with no admitted gap is forced. The choice is made for the
101
106
  paragraph rather than line by line,
102
107
  minimizing in order the number of forced lines, then stretched, then hanging, then
103
108
  shrunk lines that spent visible space. Which space may be reduced, and in which
@@ -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: "italic";
917
+ kind: "ruby";
918
918
  readonly range: Readonly<{
919
919
  source: Readonly<{
920
920
  start: number;
@@ -929,8 +929,18 @@ 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
+ }>;
932
942
  }> | Readonly<{
933
- kind: "ruby";
943
+ kind: "bold";
934
944
  readonly range: Readonly<{
935
945
  source: Readonly<{
936
946
  start: number;
@@ -945,18 +955,8 @@ declare const ParsedManuscriptSchema: v.SchemaWithPipe<readonly [v.StrictObjectS
945
955
  end: number;
946
956
  }> & v.Brand<"GraphemeRange">;
947
957
  }>;
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: "bold";
959
+ kind: "italic";
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: "italic";
1013
+ kind: "ruby";
1014
1014
  readonly range: Readonly<{
1015
1015
  source: Readonly<{
1016
1016
  start: number;
@@ -1025,8 +1025,18 @@ 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
+ }>;
1028
1038
  }> | Readonly<{
1029
- kind: "ruby";
1039
+ kind: "bold";
1030
1040
  readonly range: Readonly<{
1031
1041
  source: Readonly<{
1032
1042
  start: number;
@@ -1041,18 +1051,8 @@ declare const ParsedManuscriptSchema: v.SchemaWithPipe<readonly [v.StrictObjectS
1041
1051
  end: number;
1042
1052
  }> & v.Brand<"GraphemeRange">;
1043
1053
  }>;
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: "bold";
1055
+ kind: "italic";
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: "italic";
1506
+ kind: "ruby";
1507
1507
  readonly range: Readonly<{
1508
1508
  source: Readonly<{
1509
1509
  start: number;
@@ -1518,8 +1518,18 @@ 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
+ }>;
1521
1531
  }> | Readonly<{
1522
- kind: "ruby";
1532
+ kind: "bold";
1523
1533
  readonly range: Readonly<{
1524
1534
  source: Readonly<{
1525
1535
  start: number;
@@ -1534,18 +1544,8 @@ declare const ParsedManuscript: {
1534
1544
  end: number;
1535
1545
  }> & v.Brand<"GraphemeRange">;
1536
1546
  }>;
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: "bold";
1548
+ kind: "italic";
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: "italic";
1602
+ kind: "ruby";
1603
1603
  readonly range: Readonly<{
1604
1604
  source: Readonly<{
1605
1605
  start: number;
@@ -1614,8 +1614,18 @@ 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
+ }>;
1617
1627
  }> | Readonly<{
1618
- kind: "ruby";
1628
+ kind: "bold";
1619
1629
  readonly range: Readonly<{
1620
1630
  source: Readonly<{
1621
1631
  start: number;
@@ -1630,18 +1640,8 @@ declare const ParsedManuscript: {
1630
1640
  end: number;
1631
1641
  }> & v.Brand<"GraphemeRange">;
1632
1642
  }>;
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: "bold";
1644
+ kind: "italic";
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-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";
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";
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-CNBT4TDN.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-C0f3mxKs.mjs";
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";
2
2
  import * as v from "valibot";
3
3
  //#region src/proofreading/invalid-rule-options.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { a as ManuscriptResult, i as ParsedManuscript, t as ComposedManuscript } from "./composed-manuscript-C0f3mxKs.mjs";
1
+ import { a as ManuscriptResult, i as ParsedManuscript, t as ComposedManuscript } from "./composed-manuscript-QgIdae8p.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<{
@@ -377,10 +377,6 @@ const REDUCTION_STAGE = {
377
377
  * {@link REDUCTION_STAGE}: the lower the number, the earlier the paragraph optimizer takes it. A
378
378
  * line is either reduced or expanded, never both, so the two scales never meet.
379
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
380
  * Nothing here takes priority 0. Expanding is always visible, and priority 0 means an adjustment
385
381
  * the optimizer is charged nothing for.
386
382
  */
@@ -396,7 +392,11 @@ const EXPANSION_STAGE = {
396
392
  /**
397
393
  * Stage 3: everywhere else 表6 admits, from solid up to a quarter em.
398
394
  */
399
- solid: 3
395
+ solid: 3,
396
+ /**
397
+ * Stage 4: the unbounded remainder, spread equally across every colored or gray cell of 表6.
398
+ */
399
+ final: 4
400
400
  };
401
401
  /**
402
402
  * 無印 in 表1 with no colour in 表6: the pair is set solid and line adjustment leaves it alone.
@@ -635,6 +635,125 @@ const EXPANDABLE_AFTER = [
635
635
  "cl-30"
636
636
  ];
637
637
  /**
638
+ * The gray, blue and pink cells of 表6: all pairs that join the fourth and final expansion stage.
639
+ * The finite capacities above cover the blue and pink cells at stages three and two; once those
640
+ * stages are exhausted, JLReq 3.8.4 d adds the remainder equally across every cell named here.
641
+ *
642
+ * A cl-08/cl-08 cell is blue only when the two characters are different kinds of mark (注4). This
643
+ * profile cannot distinguish the two halves of one 2倍ダッシュ from different marks, so it keeps the
644
+ * pair joined, as it already does for the finite third stage.
645
+ */
646
+ const FINAL_EXPANSION_BASIC_AFTER = /* @__PURE__ */ new Set([
647
+ "cl-01",
648
+ "cl-08",
649
+ "cl-12",
650
+ "cl-13",
651
+ "cl-14",
652
+ "cl-15",
653
+ "cl-16",
654
+ "cl-19",
655
+ "cl-24",
656
+ "cl-25",
657
+ "cl-26",
658
+ "cl-27",
659
+ "cl-30"
660
+ ]);
661
+ const FINAL_EXPANSION_WIDE_AFTER = /* @__PURE__ */ new Set([
662
+ ...FINAL_EXPANSION_BASIC_AFTER,
663
+ "cl-09",
664
+ "cl-10",
665
+ "cl-11"
666
+ ]);
667
+ const FINAL_EXPANSION_AFTER = new Map([
668
+ ...[
669
+ "cl-02",
670
+ "cl-03",
671
+ "cl-04",
672
+ "cl-05",
673
+ "cl-06",
674
+ "cl-07",
675
+ "cl-14"
676
+ ].map((left) => [left, FINAL_EXPANSION_BASIC_AFTER]),
677
+ ...[
678
+ "cl-09",
679
+ "cl-10",
680
+ "cl-11",
681
+ "cl-13",
682
+ "cl-15",
683
+ "cl-16",
684
+ "cl-19",
685
+ "cl-30"
686
+ ].map((left) => [left, FINAL_EXPANSION_WIDE_AFTER]),
687
+ ["cl-08", new Set([...FINAL_EXPANSION_WIDE_AFTER].filter((characterClass) => characterClass !== "cl-08"))],
688
+ ["cl-12", /* @__PURE__ */ new Set([
689
+ "cl-08",
690
+ "cl-09",
691
+ "cl-10",
692
+ "cl-11",
693
+ "cl-12",
694
+ "cl-13",
695
+ "cl-15",
696
+ "cl-16",
697
+ "cl-19",
698
+ "cl-25",
699
+ "cl-26",
700
+ "cl-27",
701
+ "cl-30"
702
+ ])],
703
+ ["cl-24", /* @__PURE__ */ new Set([
704
+ "cl-01",
705
+ "cl-08",
706
+ "cl-09",
707
+ "cl-10",
708
+ "cl-11",
709
+ "cl-12",
710
+ "cl-14",
711
+ "cl-15",
712
+ "cl-16",
713
+ "cl-19",
714
+ "cl-25",
715
+ "cl-26",
716
+ "cl-30"
717
+ ])],
718
+ ["cl-25", /* @__PURE__ */ new Set([
719
+ "cl-01",
720
+ "cl-08",
721
+ "cl-09",
722
+ "cl-10",
723
+ "cl-11",
724
+ "cl-12",
725
+ "cl-13",
726
+ "cl-14",
727
+ "cl-15",
728
+ "cl-16",
729
+ "cl-19",
730
+ "cl-24",
731
+ "cl-26",
732
+ "cl-30"
733
+ ])],
734
+ ["cl-26", new Set(japaneseCharacterClasses.filter((characterClass) => characterClass !== "cl-02"))],
735
+ ["cl-27", /* @__PURE__ */ new Set([
736
+ "cl-01",
737
+ "cl-08",
738
+ "cl-09",
739
+ "cl-10",
740
+ "cl-11",
741
+ "cl-12",
742
+ "cl-13",
743
+ "cl-14",
744
+ "cl-15",
745
+ "cl-16",
746
+ "cl-19",
747
+ "cl-26",
748
+ "cl-30"
749
+ ])]
750
+ ]);
751
+ function canExpandAtFinalStage(left, right) {
752
+ if (left === "cl-14" || left === "cl-26" || left === "cl-27" || right === "cl-14" || right === "cl-26" || right === "cl-27") return false;
753
+ if (FINAL_EXPANSION_AFTER.get(left)?.has(right) !== true) return false;
754
+ return pairSpacing(left, right).naturalWidthEm === 0;
755
+ }
756
+ /**
638
757
  * The classes JLReq 3.1.2 sets on a half em rather than a full one.
639
758
  */
640
759
  const HALF_EM_CLASSES = /* @__PURE__ */ new Set([
@@ -922,6 +1041,8 @@ const defaultJapaneseTypesettingProfile = {
922
1041
  },
923
1042
  absorbsPrecedingEm: .25
924
1043
  } : null,
1044
+ finalStretchPriority: EXPANSION_STAGE.final,
1045
+ canExpandAtFinalStage,
925
1046
  breakPenalty,
926
1047
  /**
927
1048
  * Hanging punctuation (ぶら下げ組, JLReq 3.8.2) lets a full stop or comma sit outside the text area
@@ -951,6 +1072,15 @@ function skipSourceGaps(atoms, start) {
951
1072
  function previousVisible(atoms, end) {
952
1073
  for (let index = end - 1; index >= 0; index -= 1) if (atoms[index]?.sourceGap === false) return index;
953
1074
  }
1075
+ /**
1076
+ * Whether a shorter line ending somewhere inside `(contentStart, end)` was ever an option — checked
1077
+ * as a plain short-circuiting scan rather than building the boundary list first, since this runs
1078
+ * once per over-long candidate the optimizer considers.
1079
+ */
1080
+ function hasEarlierBoundary(boundaryAllowed, contentStart, end) {
1081
+ for (let boundary = contentStart + 1; boundary < end; boundary += 1) if (boundaryAllowed(boundary - 1, boundary)) return true;
1082
+ return false;
1083
+ }
954
1084
  function opportunities(atoms, classes, start, end, profile, lineHead) {
955
1085
  const result = [];
956
1086
  const firstClass = classes[start];
@@ -961,6 +1091,7 @@ function opportunities(atoms, classes, start, end, profile, lineHead) {
961
1091
  boundary: start
962
1092
  },
963
1093
  spacing: startSpacing,
1094
+ finalStretch: false,
964
1095
  absorbsPrecedingEm: 0
965
1096
  });
966
1097
  for (let index = start; index < end; index += 1) {
@@ -973,6 +1104,7 @@ function opportunities(atoms, classes, start, end, profile, lineHead) {
973
1104
  index
974
1105
  },
975
1106
  spacing,
1107
+ finalStretch: false,
976
1108
  absorbsPrecedingEm: 0
977
1109
  });
978
1110
  }
@@ -988,6 +1120,7 @@ function opportunities(atoms, classes, start, end, profile, lineHead) {
988
1120
  boundary: right
989
1121
  },
990
1122
  spacing: profile.pairSpacing(leftClass, rightClass),
1123
+ finalStretch: profile.canExpandAtFinalStage(leftClass, rightClass),
991
1124
  absorbsPrecedingEm: 0
992
1125
  });
993
1126
  }
@@ -999,6 +1132,7 @@ function opportunities(atoms, classes, start, end, profile, lineHead) {
999
1132
  boundary: end
1000
1133
  },
1001
1134
  spacing: endSpacing.spacing,
1135
+ finalStretch: false,
1002
1136
  absorbsPrecedingEm: endSpacing.absorbsPrecedingEm
1003
1137
  });
1004
1138
  return result;
@@ -1072,17 +1206,20 @@ function adjustmentUnits(values, adjustment) {
1072
1206
  * Within a stage the amount is therefore split in proportion to what each space can give, rather
1073
1207
  * than taken out of the earliest space until it runs dry. Across stages the order stays a
1074
1208
  * 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.
1209
+ * sets one character size, so a share of the stage's capacity is a share by character size. JLReq
1210
+ * 3.8.4's final stage has no upper bound: after the finite stages are full, the unresolved
1211
+ * remainder is added equally to every pair the profile admits, including the earlier-stage pairs.
1076
1212
  *
1077
1213
  * An all-or-nothing unit larger than what is still needed is skipped rather than partly spent, and
1078
1214
  * never revisited: `remaining` only falls, so a unit that did not fit at its own stage cannot fit
1079
1215
  * at a later one. One forward pass is therefore exact and no search over subsets is needed.
1080
1216
  */
1081
- function resolveSpacings(values, units, adjustment, amountEm) {
1217
+ function resolveSpacings(values, units, adjustment, amountEm, finalStretchPriority) {
1082
1218
  const usedBySlot = /* @__PURE__ */ new Map();
1083
1219
  let remaining = amountEm;
1084
1220
  let priorityCost = 0;
1085
1221
  let freeEm = 0;
1222
+ let finalStretchPerSlotEm = 0;
1086
1223
  const spend = (unit, fraction) => {
1087
1224
  for (const { slot, amountEm: partEm } of unit.parts) {
1088
1225
  const key = slotKey(slot);
@@ -1109,6 +1246,20 @@ function resolveSpacings(values, units, adjustment, amountEm) {
1109
1246
  const fraction = Math.min(remaining, stageCapacityEm) / stageCapacityEm;
1110
1247
  for (const unit of stage) if (unit.granularity === "continuous") spend(unit, fraction);
1111
1248
  }
1249
+ if (adjustment === "stretch" && remaining > EPSILON) {
1250
+ let finalSlotCount = 0;
1251
+ for (const { finalStretch } of values) if (finalStretch) finalSlotCount += 1;
1252
+ if (finalSlotCount > 0) {
1253
+ finalStretchPerSlotEm = remaining / finalSlotCount;
1254
+ for (const { slot, finalStretch } of values) {
1255
+ if (!finalStretch) continue;
1256
+ const key = slotKey(slot);
1257
+ usedBySlot.set(key, (usedBySlot.get(key) ?? 0) + finalStretchPerSlotEm);
1258
+ }
1259
+ priorityCost += remaining * finalStretchPriority;
1260
+ remaining = 0;
1261
+ }
1262
+ }
1112
1263
  return {
1113
1264
  spacings: splitBySlot(values, (slot, spacing) => {
1114
1265
  const used = usedBySlot.get(slotKey(slot)) ?? 0;
@@ -1116,6 +1267,7 @@ function resolveSpacings(values, units, adjustment, amountEm) {
1116
1267
  }),
1117
1268
  priorityCost,
1118
1269
  freeEm,
1270
+ finalStretchPerSlotEm,
1119
1271
  unabsorbedEm: Math.max(0, remaining)
1120
1272
  };
1121
1273
  }
@@ -1159,11 +1311,11 @@ function hangingSpacings(values, end) {
1159
1311
  characterSpacings
1160
1312
  };
1161
1313
  }
1162
- function candidate(atoms, classes, start, end, lineLengthEm, profile) {
1314
+ function candidate(atoms, classes, boxPrefixEm, start, end, lineLengthEm, profile) {
1163
1315
  const contentStart = skipSourceGaps(atoms, start);
1164
1316
  const suppressedIndexes = Array.from({ length: contentStart - start }, (_, index) => start + index);
1165
1317
  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);
1318
+ const naturalSizeEm = (boxPrefixEm[end] ?? 0) - (boxPrefixEm[contentStart] ?? 0) + pairValues.reduce((total, value) => total + value.spacing.naturalWidthEm, 0);
1167
1319
  const terminal = skipSourceGaps(atoms, end) === atoms.length;
1168
1320
  const overflow = naturalSizeEm - lineLengthEm;
1169
1321
  const underflow = lineLengthEm - naturalSizeEm;
@@ -1174,6 +1326,7 @@ function candidate(atoms, classes, start, end, lineLengthEm, profile) {
1174
1326
  const lastClass = lastVisible === void 0 ? void 0 : classes[lastVisible];
1175
1327
  const lastAdvance = lastVisible === void 0 ? 0 : atoms[lastVisible]?.boxAdvanceEm ?? 0;
1176
1328
  const trailingSpacing = pairValues.find(({ slot }) => slot.kind === "gap" && slot.boundary === end)?.spacing.naturalWidthEm ?? 0;
1329
+ const hasFinalStretch = pairValues.some(({ finalStretch }) => finalStretch);
1177
1330
  if (terminal && overflow <= EPSILON) return {
1178
1331
  start,
1179
1332
  contentStart,
@@ -1198,7 +1351,7 @@ function candidate(atoms, classes, start, end, lineLengthEm, profile) {
1198
1351
  deformationRatio: 0,
1199
1352
  priorityCost: 0
1200
1353
  };
1201
- const shrunk = overflow > 0 && overflow <= capacity + EPSILON ? resolveSpacings(pairValues, units, "shrink", overflow) : null;
1354
+ const shrunk = overflow > 0 && overflow <= capacity + EPSILON ? resolveSpacings(pairValues, units, "shrink", overflow, profile.finalStretchPriority) : null;
1202
1355
  if (shrunk !== null && shrunk.unabsorbedEm <= EPSILON) {
1203
1356
  const chargeableCapacity = capacity - freeShrinkCapacity;
1204
1357
  const chargeableShrink = Math.max(0, overflow - shrunk.freeEm);
@@ -1227,7 +1380,7 @@ function candidate(atoms, classes, start, end, lineLengthEm, profile) {
1227
1380
  deformationRatio: overflow / Math.max(lastAdvance, EPSILON),
1228
1381
  priorityCost: 0
1229
1382
  };
1230
- const stretched = !terminal && underflow > EPSILON && underflow <= capacity + EPSILON ? resolveSpacings(pairValues, units, "stretch", underflow) : null;
1383
+ const stretched = !terminal && underflow > EPSILON && (underflow <= capacity + EPSILON || hasFinalStretch) ? resolveSpacings(pairValues, units, "stretch", underflow, profile.finalStretchPriority) : null;
1231
1384
  if (stretched !== null && stretched.unabsorbedEm <= EPSILON) return {
1232
1385
  start,
1233
1386
  contentStart,
@@ -1237,7 +1390,7 @@ function candidate(atoms, classes, start, end, lineLengthEm, profile) {
1237
1390
  inlineSizeEm: lineLengthEm,
1238
1391
  break: { kind: "stretched" },
1239
1392
  hangingIndex: null,
1240
- deformationRatio: capacity === 0 ? 0 : underflow / capacity,
1393
+ deformationRatio: stretched.finalStretchPerSlotEm > 0 ? 1 + stretched.finalStretchPerSlotEm : capacity === 0 ? 0 : underflow / capacity,
1241
1394
  priorityCost: stretched.priorityCost
1242
1395
  };
1243
1396
  return {
@@ -1287,6 +1440,22 @@ function addScore(left, right) {
1287
1440
  left[6] + right[6]
1288
1441
  ];
1289
1442
  }
1443
+ /**
1444
+ * The line-end offsets a state's path took, earliest first. Rebuilt on demand by walking `previous`
1445
+ * rather than carried on every `State`: every candidate line attempted would otherwise pay for a
1446
+ * copy of its whole path, when `isBetter` only ever reads it on the exact-score tie this comparison
1447
+ * breaks — for `boundaryAllowed` predicates permissive enough to fully saturate the optimizer's
1448
+ * search window, ties are rare enough that this deferred cost is negligible.
1449
+ */
1450
+ function breaksOf(state, states) {
1451
+ const result = [];
1452
+ let cursor = state;
1453
+ while (cursor?.line !== null && cursor?.line !== void 0) {
1454
+ result.push(cursor.line.end);
1455
+ cursor = cursor.previous === null ? void 0 : states.get(cursor.previous.index)?.get(cursor.previous.fitness);
1456
+ }
1457
+ return result.reverse();
1458
+ }
1290
1459
  function compareBreaks(left, right) {
1291
1460
  const length = Math.min(left.length, right.length);
1292
1461
  for (let index = 0; index < length; index += 1) {
@@ -1295,17 +1464,19 @@ function compareBreaks(left, right) {
1295
1464
  }
1296
1465
  return left.length - right.length;
1297
1466
  }
1298
- function isBetter(candidateState, current) {
1467
+ function isBetter(candidateState, current, states) {
1299
1468
  if (current === void 0) return true;
1300
1469
  for (let index = 0; index < candidateState.score.length; index += 1) {
1301
1470
  const difference = (candidateState.score[index] ?? 0) - (current.score[index] ?? 0);
1302
1471
  if (Math.abs(difference) > EPSILON) return difference < 0;
1303
1472
  }
1304
- return compareBreaks(candidateState.breaks, current.breaks) < 0;
1473
+ return compareBreaks(breaksOf(candidateState, states), breaksOf(current, states)) < 0;
1305
1474
  }
1306
1475
  function layoutParagraph(atoms, lineLengthEm, profile, boundaryAllowed) {
1307
1476
  if (atoms.length === 0) return [];
1308
1477
  const classes = atoms.map(({ characterClass }) => characterClass);
1478
+ const boxPrefixEm = Array.from({ length: atoms.length + 1 }).fill(0);
1479
+ for (let index = 0; index < atoms.length; index += 1) boxPrefixEm[index + 1] = (boxPrefixEm[index] ?? 0) + (atoms[index]?.boxAdvanceEm ?? 0);
1309
1480
  const states = /* @__PURE__ */ new Map([[0, /* @__PURE__ */ new Map([[0, {
1310
1481
  score: [
1311
1482
  0,
@@ -1318,8 +1489,7 @@ function layoutParagraph(atoms, lineLengthEm, profile, boundaryAllowed) {
1318
1489
  ],
1319
1490
  fitness: 0,
1320
1491
  previous: null,
1321
- line: null,
1322
- breaks: []
1492
+ line: null
1323
1493
  }]])]]);
1324
1494
  for (let start = 0; start < atoms.length; start += 1) {
1325
1495
  const activeStates = states.get(start);
@@ -1327,7 +1497,7 @@ function layoutParagraph(atoms, lineLengthEm, profile, boundaryAllowed) {
1327
1497
  const contentStart = skipSourceGaps(atoms, start);
1328
1498
  if (contentStart === atoms.length) {
1329
1499
  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);
1500
+ for (const state of activeStates.values()) if (isBetter(state, terminalStates.get(state.fitness), states)) terminalStates.set(state.fitness, state);
1331
1501
  states.set(atoms.length, terminalStates);
1332
1502
  continue;
1333
1503
  }
@@ -1336,8 +1506,8 @@ function layoutParagraph(atoms, lineLengthEm, profile, boundaryAllowed) {
1336
1506
  const left = previousVisible(atoms, end);
1337
1507
  if (atoms[end - 1]?.sourceGap === true) continue;
1338
1508
  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))) {
1509
+ const line = candidate(atoms, classes, boxPrefixEm, start, end, lineLengthEm, profile);
1510
+ if (line.break.kind === "forced" && line.inlineSizeEm > lineLengthEm + EPSILON && hasEarlierBoundary(boundaryAllowed, contentStart, end)) {
1341
1511
  if (line.inlineSizeEm > lineLengthEm * 2 && end > contentStart + 1) break;
1342
1512
  continue;
1343
1513
  }
@@ -1349,18 +1519,17 @@ function layoutParagraph(atoms, lineLengthEm, profile, boundaryAllowed) {
1349
1519
  index: start,
1350
1520
  fitness: state.fitness
1351
1521
  },
1352
- line,
1353
- breaks: [...state.breaks, end]
1522
+ line
1354
1523
  };
1355
1524
  const statesAtEnd = states.get(end) ?? /* @__PURE__ */ new Map();
1356
- if (isBetter(nextState, statesAtEnd.get(nextState.fitness))) {
1525
+ if (isBetter(nextState, statesAtEnd.get(nextState.fitness), states)) {
1357
1526
  statesAtEnd.set(nextState.fitness, nextState);
1358
1527
  states.set(end, statesAtEnd);
1359
1528
  }
1360
1529
  if (line.inlineSizeEm > lineLengthEm * 2 && line.break.kind === "forced" && end > contentStart + 1) break;
1361
1530
  }
1362
1531
  }
1363
- const terminal = [...states.get(atoms.length)?.values() ?? []].reduce((best, state) => isBetter(state, best) ? state : best, void 0);
1532
+ const terminal = [...states.get(atoms.length)?.values() ?? []].reduce((best, state) => isBetter(state, best, states) ? state : best, void 0);
1364
1533
  if (terminal === void 0) return [];
1365
1534
  const lines = [];
1366
1535
  let cursor = terminal;
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-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";
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";
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-CNBT4TDN.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.0",
3
+ "version": "0.9.2",
4
4
  "description": "Framework-independent parsing, composition, and proofreading for Japanese manuscripts",
5
5
  "keywords": [
6
6
  "japanese",