@sushichan044/kg-core 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +54 -10
  2. package/dist/index.d.mts +24947 -6873
  3. package/dist/index.mjs +1126 -198
  4. package/package.json +3 -2
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as v from "valibot";
2
+ import { eastAsianWidth } from "get-east-asian-width";
2
3
  //#region src/assert-never.ts
3
4
  /**
4
5
  * Marks a branch the compiler proved unreachable. Reaching it at runtime means a variant was added
@@ -308,11 +309,38 @@ const ItalicAnnotation = { schema: readonlyObject({
308
309
  kind: v.literal("italic"),
309
310
  range: ManuscriptRange.schema
310
311
  }) };
311
- const RubyAnnotation = { schema: readonlyObject({
312
+ //#endregion
313
+ //#region src/parser/annotation/ruby-annotation.ts
314
+ const GroupRubyKindSchema = v.literal("group");
315
+ const MonoRubyKindSchema = v.literal("mono");
316
+ const JukugoRubyKindSchema = v.literal("jukugo");
317
+ const RubyKindSchema = v.union([
318
+ GroupRubyKindSchema,
319
+ MonoRubyKindSchema,
320
+ JukugoRubyKindSchema
321
+ ]);
322
+ const RubyReadingSchema = v.variant("kind", [
323
+ readonlyObject({
324
+ kind: GroupRubyKindSchema,
325
+ text: v.pipe(v.string(), v.nonEmpty())
326
+ }),
327
+ readonlyObject({
328
+ kind: MonoRubyKindSchema,
329
+ segments: v.pipe(v.array(v.pipe(v.string(), v.nonEmpty())), v.nonEmpty(), v.readonly())
330
+ }),
331
+ readonlyObject({
332
+ kind: JukugoRubyKindSchema,
333
+ segments: v.pipe(v.array(v.pipe(v.string(), v.nonEmpty())), v.nonEmpty(), v.readonly())
334
+ })
335
+ ]);
336
+ const RubyAnnotationSchema = readonlyObject({
312
337
  kind: v.literal("ruby"),
313
338
  range: ManuscriptRange.schema,
314
- reading: v.string()
315
- }) };
339
+ reading: RubyReadingSchema
340
+ });
341
+ const RubyKind = { schema: RubyKindSchema };
342
+ const RubyReading = { schema: RubyReadingSchema };
343
+ const RubyAnnotation = { schema: RubyAnnotationSchema };
316
344
  const ManuscriptAnnotation = {
317
345
  schema: v.variant("kind", [
318
346
  RubyAnnotation.schema,
@@ -364,12 +392,21 @@ const ParsedManuscriptSchema = readonlyObject({
364
392
  function rangeFits(range, manuscript) {
365
393
  return range.source.end <= manuscript.source.length && range.display.end <= manuscript.displayText.length && range.graphemes.end <= manuscript.graphemes.length;
366
394
  }
395
+ function hasValidRubyAssociations(manuscript) {
396
+ const ruby = manuscript.annotations.filter((annotation) => annotation.kind === "ruby");
397
+ return ruby.every((annotation, index) => {
398
+ const baseLength = annotation.range.graphemes.end - annotation.range.graphemes.start;
399
+ if (baseLength <= 0) return false;
400
+ if (annotation.reading.kind !== "group" && annotation.reading.segments.length !== baseLength) return false;
401
+ return ruby.slice(index + 1).every((other) => !ManuscriptRange.overlaps(annotation.range, other.range));
402
+ });
403
+ }
367
404
  function isConsistent(manuscript, source) {
368
405
  const joined = manuscript.graphemes.map(({ value }) => value).join("");
369
406
  const sequential = manuscript.graphemes.every(({ range }, index) => range.graphemes.start === index && range.graphemes.end === index + 1 && rangeFits(range, manuscript));
370
- return manuscript.source === source && manuscript.displayText === joined && sequential && manuscript.annotations.every(({ range }) => rangeFits(range, manuscript));
407
+ return manuscript.source === source && manuscript.displayText === joined && sequential && manuscript.annotations.every(({ range }) => rangeFits(range, manuscript)) && hasValidRubyAssociations(manuscript);
371
408
  }
372
- const CONTRACT_MESSAGE = "parser output must contain consistent source, display, grapheme, and annotation ranges";
409
+ const CONTRACT_MESSAGE = "parser output must contain consistent source, display, grapheme, and annotation ranges and valid ruby associations";
373
410
  const ParsedManuscript = {
374
411
  schema: ParsedManuscriptSchema,
375
412
  /**
@@ -681,7 +718,10 @@ const kakuyomuParser = {
681
718
  const range = draft.append(source.slice(ruby.parentStart, ruby.parentEnd), ruby.parentStart);
682
719
  draft.annotate({
683
720
  kind: "ruby",
684
- reading: ruby.reading,
721
+ reading: {
722
+ kind: "group",
723
+ text: ruby.reading
724
+ },
685
725
  range: {
686
726
  ...range,
687
727
  source: {
@@ -731,7 +771,10 @@ const kakuyomuParser = {
731
771
  const range = draft.append(source.slice(ruby.parentStart, ruby.parentEnd), ruby.parentStart);
732
772
  draft.annotate({
733
773
  kind: "ruby",
734
- reading: ruby.reading,
774
+ reading: {
775
+ kind: "group",
776
+ text: ruby.reading
777
+ },
735
778
  range: {
736
779
  ...range,
737
780
  source: {
@@ -797,7 +840,10 @@ function parsePairTag(source, start, prefix) {
797
840
  content,
798
841
  annotation: {
799
842
  kind: "ruby",
800
- reading: value.text
843
+ reading: {
844
+ kind: "group",
845
+ text: value.text
846
+ }
801
847
  }
802
848
  };
803
849
  if ([...graphemeSegmenter.segment(value.text)].length !== 1) return void 0;
@@ -957,35 +1003,48 @@ function composeManuscript(parsed, options) {
957
1003
  }, result.warnings);
958
1004
  }
959
1005
  //#endregion
960
- //#region src/composer/grid-settings.ts
961
- const RANGES = {
962
- charsPerLine: {
963
- min: 10,
964
- max: 60
965
- },
966
- linesPerStage: {
967
- min: 10,
968
- max: 60
969
- },
970
- stagesPerPage: {
971
- min: 1,
972
- max: 3
973
- }
1006
+ //#region src/composer/composed-annotation-fragment.ts
1007
+ const emLength = () => v.pipe(v.number(), v.finite(), v.minValue(0));
1008
+ const continuation = v.union([
1009
+ v.literal("whole"),
1010
+ v.literal("start"),
1011
+ v.literal("middle"),
1012
+ v.literal("end")
1013
+ ]);
1014
+ const PositionedReadingGraphemeSchema = readonlyObject({
1015
+ value: v.string(),
1016
+ offsetEm: v.pipe(v.number(), v.finite()),
1017
+ advanceEm: emLength()
1018
+ });
1019
+ const common$1 = {
1020
+ annotationRange: ManuscriptRange.schema,
1021
+ fragmentRange: ManuscriptRange.schema,
1022
+ continuation
974
1023
  };
975
- const boundedCount = (bounds) => v.pipe(v.number(), v.finite(), v.integer(), v.minValue(bounds.min), v.maxValue(bounds.max));
976
- const GridSettings = {
977
- schema: readonlyObject({
978
- charsPerLine: boundedCount(RANGES.charsPerLine),
979
- linesPerStage: boundedCount(RANGES.linesPerStage),
980
- stagesPerPage: boundedCount(RANGES.stagesPerPage)
1024
+ const ComposedAnnotationFragment = { schema: v.variant("kind", [
1025
+ readonlyObject({
1026
+ kind: v.literal("bold"),
1027
+ ...common$1
981
1028
  }),
982
- ranges: RANGES,
983
- defaults: {
984
- charsPerLine: 27,
985
- linesPerStage: 23,
986
- stagesPerPage: 2
987
- }
988
- };
1029
+ readonlyObject({
1030
+ kind: v.literal("italic"),
1031
+ ...common$1
1032
+ }),
1033
+ readonlyObject({
1034
+ kind: v.literal("emphasis"),
1035
+ mark: v.string(),
1036
+ ...common$1
1037
+ }),
1038
+ readonlyObject({
1039
+ kind: v.literal("ruby"),
1040
+ rubyKind: RubyKind.schema,
1041
+ reading: v.string(),
1042
+ baseOffsetEm: emLength(),
1043
+ baseAdvanceEm: emLength(),
1044
+ readingGraphemes: readonlyArray(PositionedReadingGraphemeSchema),
1045
+ ...common$1
1046
+ })
1047
+ ]) };
989
1048
  const LineOffset = {
990
1049
  schema: readonlyObject({
991
1050
  leading: nonNegativeInteger(),
@@ -1023,44 +1082,231 @@ const ManuscriptOffsets = {
1023
1082
  /**
1024
1083
  * Stage offsets must leave at least one usable line in the stage.
1025
1084
  */
1026
- maxStageTotal: (grid) => Math.max(0, grid.linesPerStage - 1),
1085
+ maxStageTotal: (flow) => Math.max(0, flow.linesPerStage - 1),
1027
1086
  /**
1028
1087
  * Page offsets must leave at least one usable line on the page, after stage offsets apply.
1029
1088
  */
1030
- maxPageTotal: (grid, stage) => Math.max(0, (grid.linesPerStage - LineOffset.total(stage)) * grid.stagesPerPage - 1)
1089
+ maxPageTotal: (flow, stage) => Math.max(0, (flow.linesPerStage - LineOffset.total(stage)) * flow.stagesPerPage - 1)
1090
+ };
1091
+ //#endregion
1092
+ //#region src/composer/novel-flow-settings.ts
1093
+ const RANGES = {
1094
+ lineLengthEm: {
1095
+ min: 10,
1096
+ max: 60
1097
+ },
1098
+ linesPerStage: {
1099
+ min: 10,
1100
+ max: 60
1101
+ },
1102
+ stagesPerPage: {
1103
+ min: 1,
1104
+ max: 3
1105
+ }
1106
+ };
1107
+ const boundedCount = (bounds) => v.pipe(v.number(), v.finite(), v.integer(), v.minValue(bounds.min), v.maxValue(bounds.max));
1108
+ const NovelFlowSettings = {
1109
+ schema: readonlyObject({
1110
+ lineLengthEm: boundedCount(RANGES.lineLengthEm),
1111
+ linesPerStage: boundedCount(RANGES.linesPerStage),
1112
+ stagesPerPage: boundedCount(RANGES.stagesPerPage)
1113
+ }),
1114
+ ranges: RANGES,
1115
+ defaults: {
1116
+ lineLengthEm: 27,
1117
+ linesPerStage: 23,
1118
+ stagesPerPage: 2
1119
+ }
1031
1120
  };
1032
1121
  //#endregion
1033
1122
  //#region src/composer/composition-settings.ts
1034
- const ManuscriptCompositionSettingsSchema = v.pipe(readonlyObject({
1035
- grid: GridSettings.schema,
1123
+ const NovelCompositionSettingsSchema = v.pipe(readonlyObject({
1124
+ flow: NovelFlowSettings.schema,
1036
1125
  offsets: ManuscriptOffsets.schema,
1037
1126
  appearance: ManuscriptAppearanceSettings.schema
1038
- }), v.check(({ grid, offsets }) => LineOffset.total(offsets.stage) <= ManuscriptOffsets.maxStageTotal(grid) && LineOffset.total(offsets.page) <= ManuscriptOffsets.maxPageTotal(grid, offsets.stage), "page or stage offsets leave no usable manuscript lines"));
1039
- const ManuscriptCompositionSettings = {
1040
- schema: ManuscriptCompositionSettingsSchema,
1127
+ }), v.check(({ flow, offsets }) => LineOffset.total(offsets.stage) <= ManuscriptOffsets.maxStageTotal(flow) && LineOffset.total(offsets.page) <= ManuscriptOffsets.maxPageTotal(flow, offsets.stage), "page or stage offsets leave no usable novel lines"));
1128
+ const NovelCompositionSettings = {
1129
+ schema: NovelCompositionSettingsSchema,
1041
1130
  defaults: {
1042
- grid: GridSettings.defaults,
1131
+ flow: NovelFlowSettings.defaults,
1043
1132
  offsets: ManuscriptOffsets.defaults,
1044
1133
  appearance: ManuscriptAppearanceSettings.defaults
1045
1134
  },
1046
- is: (value) => v.is(ManuscriptCompositionSettingsSchema, value)
1135
+ is: (value) => v.is(NovelCompositionSettingsSchema, value)
1047
1136
  };
1048
1137
  const CompositionStatistics = { schema: readonlyObject({
1049
1138
  chars: nonNegativeInteger(),
1050
1139
  sourceLines: nonNegativeInteger(),
1051
1140
  pages: nonNegativeInteger()
1052
1141
  }) };
1053
- const GridCell = {
1054
- schema: readonlyObject({
1055
- value: v.nullable(v.string()),
1056
- range: v.nullable(ManuscriptRange.schema),
1057
- annotations: readonlyArray(ManuscriptAnnotation.schema)
1142
+ const InlineSpan = { schema: readonlyObject({
1143
+ offsetEm: v.pipe(v.number(), v.finite()),
1144
+ advanceEm: v.pipe(v.number(), v.finite(), v.minValue(0))
1145
+ }) };
1146
+ //#endregion
1147
+ //#region src/composer/vertical-text-presentation.ts
1148
+ const common = { groupRange: ManuscriptRange.schema };
1149
+ const VerticalTextPresentation = { schema: v.variant("kind", [
1150
+ readonlyObject({
1151
+ kind: v.literal("mixed"),
1152
+ ...common
1058
1153
  }),
1059
- empty: () => ({
1060
- value: null,
1061
- range: null,
1062
- annotations: []
1154
+ readonlyObject({
1155
+ kind: v.literal("upright"),
1156
+ ...common
1157
+ }),
1158
+ readonlyObject({
1159
+ kind: v.literal("sideways"),
1160
+ ...common
1161
+ }),
1162
+ readonlyObject({
1163
+ kind: v.literal("tate-chu-yoko"),
1164
+ ...common
1063
1165
  })
1166
+ ]) };
1167
+ //#endregion
1168
+ //#region src/composer/composed-inline-item.ts
1169
+ const signedEm = () => v.pipe(v.number(), v.finite());
1170
+ const GlyphItemSchema = readonlyObject({
1171
+ kind: v.literal("glyph"),
1172
+ value: v.string(),
1173
+ range: ManuscriptRange.schema,
1174
+ layoutSpan: InlineSpan.schema,
1175
+ renderSpan: InlineSpan.schema,
1176
+ disposition: v.union([v.literal("placed"), v.literal("hanging")]),
1177
+ presentation: VerticalTextPresentation.schema
1178
+ });
1179
+ const GeneratedGlueItemSchema = readonlyObject({
1180
+ kind: v.literal("glue"),
1181
+ origin: v.literal("generated"),
1182
+ offsetEm: signedEm(),
1183
+ widthEm: signedEm(),
1184
+ naturalWidthEm: signedEm(),
1185
+ adjustment: v.union([
1186
+ v.literal("natural"),
1187
+ v.literal("shrunk"),
1188
+ v.literal("stretched")
1189
+ ])
1190
+ });
1191
+ const SourceGlueItemSchema = readonlyObject({
1192
+ kind: v.literal("glue"),
1193
+ origin: v.literal("source"),
1194
+ value: v.string(),
1195
+ range: ManuscriptRange.schema,
1196
+ offsetEm: signedEm(),
1197
+ widthEm: signedEm(),
1198
+ naturalWidthEm: signedEm(),
1199
+ adjustment: v.union([
1200
+ v.literal("natural"),
1201
+ v.literal("shrunk"),
1202
+ v.literal("stretched")
1203
+ ])
1204
+ });
1205
+ const KernItemSchema = readonlyObject({
1206
+ kind: v.literal("kern"),
1207
+ offsetEm: signedEm(),
1208
+ widthEm: signedEm()
1209
+ });
1210
+ const SuppressedItemSchema = readonlyObject({
1211
+ kind: v.literal("suppressed"),
1212
+ value: v.string(),
1213
+ range: ManuscriptRange.schema,
1214
+ reason: v.literal("question-or-exclamation-gap")
1215
+ });
1216
+ const ComposedInlineItem = { schema: v.union([
1217
+ GlyphItemSchema,
1218
+ GeneratedGlueItemSchema,
1219
+ SourceGlueItemSchema,
1220
+ KernItemSchema,
1221
+ SuppressedItemSchema
1222
+ ]) };
1223
+ const InlineMeasurement = { schema: readonlyObject({ advanceEm: v.pipe(v.number(), v.finite(), v.minValue(0)) }) };
1224
+ function graphemeWidth(grapheme) {
1225
+ let width = 1;
1226
+ for (const character of grapheme) {
1227
+ const codePoint = character.codePointAt(0);
1228
+ if (codePoint !== void 0) width = Math.max(width, eastAsianWidth(codePoint, { ambiguousAsWide: true }));
1229
+ }
1230
+ return width / 2;
1231
+ }
1232
+ /**
1233
+ * Deterministic Japanese-context measurement in logical em units.
1234
+ */
1235
+ const logicalInlineMeasurer = (request) => {
1236
+ const { text } = request;
1237
+ const width = [...graphemeSegmenter.segment(text)].reduce((total, { segment }) => total + graphemeWidth(segment), 0);
1238
+ if (request.role === "ruby") return { advanceEm: width / 2 };
1239
+ return { advanceEm: request.presentation === "upright" ? [...graphemeSegmenter.segment(text)].length : width };
1240
+ };
1241
+ const LineBreakResult = { schema: v.variant("kind", [
1242
+ readonlyObject({ kind: v.literal("natural") }),
1243
+ readonlyObject({ kind: v.literal("shrunk") }),
1244
+ readonlyObject({ kind: v.literal("stretched") }),
1245
+ readonlyObject({ kind: v.literal("hanging") }),
1246
+ readonlyObject({ kind: v.literal("forced") }),
1247
+ readonlyObject({ kind: v.literal("paragraph-end") })
1248
+ ]) };
1249
+ //#endregion
1250
+ //#region src/composer/manuscript-geometry.ts
1251
+ const STAGE_GAP_CELLS = 2;
1252
+ const LINE_GAP_CELLS = .5;
1253
+ const nonNegativeLength = () => v.pipe(v.number(), v.finite(), v.minValue(0));
1254
+ const ManuscriptGeometrySchema = readonlyObject({
1255
+ paperWidthMm: nonNegativeLength(),
1256
+ paperHeightMm: nonNegativeLength(),
1257
+ cellSizeMm: nonNegativeLength(),
1258
+ lineGapMm: nonNegativeLength(),
1259
+ fontSizePt: FontSizePt.schema,
1260
+ contentWidthMm: nonNegativeLength(),
1261
+ contentHeightMm: nonNegativeLength(),
1262
+ marginInlineMm: v.pipe(v.number(), v.finite()),
1263
+ marginBlockMm: v.pipe(v.number(), v.finite()),
1264
+ fitsPaper: v.boolean()
1265
+ });
1266
+ /**
1267
+ * Every stage stacked vertically, plus the gap between adjacent stages.
1268
+ */
1269
+ function contentHeightInEm(flow) {
1270
+ return flow.stagesPerPage * flow.lineLengthEm + STAGE_GAP_CELLS * (flow.stagesPerPage - 1);
1271
+ }
1272
+ /**
1273
+ * Every vertical line side by side, plus the gap between adjacent lines.
1274
+ */
1275
+ function contentWidthInEm(flow) {
1276
+ return flow.linesPerStage + LINE_GAP_CELLS * (flow.linesPerStage - 1);
1277
+ }
1278
+ const ManuscriptGeometry = {
1279
+ schema: ManuscriptGeometrySchema,
1280
+ of: (flow, appearance) => {
1281
+ const paper = PaperSize.of(appearance.paperSize);
1282
+ const cellSizeMm = ptToMm(appearance.fontSizePt);
1283
+ const contentWidthMm = contentWidthInEm(flow) * cellSizeMm;
1284
+ const contentHeightMm = contentHeightInEm(flow) * cellSizeMm;
1285
+ const marginInlineMm = (paper.widthMm - contentWidthMm) / 2;
1286
+ const marginBlockMm = (paper.heightMm - contentHeightMm) / 2;
1287
+ return {
1288
+ paperWidthMm: paper.widthMm,
1289
+ paperHeightMm: paper.heightMm,
1290
+ cellSizeMm,
1291
+ lineGapMm: LINE_GAP_CELLS * cellSizeMm,
1292
+ fontSizePt: appearance.fontSizePt,
1293
+ contentWidthMm,
1294
+ contentHeightMm,
1295
+ marginInlineMm,
1296
+ marginBlockMm,
1297
+ fitsPaper: marginInlineMm >= 0 && marginBlockMm >= 0
1298
+ };
1299
+ },
1300
+ /**
1301
+ * The largest step-aligned point size whose text area still fits the paper.
1302
+ */
1303
+ maxFontSizePt: (flow, paperSizeId) => {
1304
+ const paper = PaperSize.of(paperSizeId);
1305
+ const maxCellSizeMm = Math.min(paper.widthMm / contentWidthInEm(flow), paper.heightMm / contentHeightInEm(flow));
1306
+ const steps = Math.floor(mmToPt(maxCellSizeMm) / FontSizePt.range.step + 1e-9);
1307
+ const aligned = Math.round(steps * FontSizePt.range.step * 1e3) / 1e3;
1308
+ return Math.min(FontSizePt.range.max, Math.max(FontSizePt.range.min, aligned));
1309
+ }
1064
1310
  };
1065
1311
  //#endregion
1066
1312
  //#region src/internal/japanese-brackets.ts
@@ -1139,113 +1385,484 @@ function analyzeMatch(line, marks) {
1139
1385
  function questionOrExclamationSpacings(line) {
1140
1386
  return [...line.matchAll(MARKS)].map((marks) => analyzeMatch(line, marks)).filter((spacing) => spacing !== void 0);
1141
1387
  }
1142
- const GridLine = {
1388
+ //#endregion
1389
+ //#region src/composer/internal/japanese-typesetting-profile.ts
1390
+ const OPENING = /* @__PURE__ */ new Set(`${OPENING_BRACKETS}【〘〝⦅«`);
1391
+ const CLOSING = /* @__PURE__ */ new Set(`${CLOSING_BRACKETS}】〙〟⦆»`);
1392
+ const HYPHENS = /* @__PURE__ */ new Set("‐‑⁃–");
1393
+ const DIVIDING_PUNCTUATION = /* @__PURE__ */ new Set("!?‼⁇⁈⁉");
1394
+ const MIDDLE_DOTS = /* @__PURE__ */ new Set("・:;");
1395
+ const SMALL_KANA = /* @__PURE__ */ new Set("ァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖ");
1396
+ const ITERATION_MARKS = /* @__PURE__ */ new Set("ヽヾゝゞ々〻");
1397
+ const INSEPARABLE = /* @__PURE__ */ new Set("—―…‥─〳〵");
1398
+ const PREFIXED_ABBREVIATIONS = /* @__PURE__ */ new Set("¥$£##");
1399
+ const POSTFIXED_ABBREVIATIONS = /* @__PURE__ */ new Set("°′″%‰");
1400
+ const UNIT_SYMBOLS = /* @__PURE__ */ new Set("℃℉㎀㎁㎂㎃㎄㎅㎆㎇㎈㎉㎊㎋㎌㎍㎎㎏㎐㎑㎒㎓㎔㎕㎖㎗㎘㎙㎚㎛㎜㎝㎞㎟㎠㎡㎢");
1401
+ const GROUPED_NUMERAL = /^[0-90-9]$/u;
1402
+ const HIRAGANA = /^\p{Script=Hiragana}$/u;
1403
+ const KATAKANA = /^\p{Script=Katakana}$/u;
1404
+ const WESTERN = /^[\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}]$/u;
1405
+ function classify({ value, presentation }) {
1406
+ if (presentation === "tate-chu-yoko") return "cl-30";
1407
+ if (OPENING.has(value)) return "cl-01";
1408
+ if (CLOSING.has(value)) return "cl-02";
1409
+ if (HYPHENS.has(value)) return "cl-03";
1410
+ if (DIVIDING_PUNCTUATION.has(value)) return "cl-04";
1411
+ if (MIDDLE_DOTS.has(value)) return "cl-05";
1412
+ if (value === "。" || value === ".") return "cl-06";
1413
+ if (value === "、" || value === ",") return "cl-07";
1414
+ if (INSEPARABLE.has(value)) return "cl-08";
1415
+ if (ITERATION_MARKS.has(value)) return "cl-09";
1416
+ if (value === "ー") return "cl-10";
1417
+ if (SMALL_KANA.has(value)) return "cl-11";
1418
+ if (PREFIXED_ABBREVIATIONS.has(value)) return "cl-12";
1419
+ if (POSTFIXED_ABBREVIATIONS.has(value)) return "cl-13";
1420
+ if (value === " ") return "cl-14";
1421
+ if (HIRAGANA.test(value)) return "cl-15";
1422
+ if (KATAKANA.test(value)) return "cl-16";
1423
+ if (GROUPED_NUMERAL.test(value)) return "cl-24";
1424
+ if (UNIT_SYMBOLS.has(value)) return "cl-25";
1425
+ if (value === " ") return "cl-26";
1426
+ if (WESTERN.test(value)) return "cl-27";
1427
+ return "cl-19";
1428
+ }
1429
+ function pairSpacing(left, right) {
1430
+ if (left === "cl-08" && right === "cl-08") return {
1431
+ kind: "kern",
1432
+ naturalWidthEm: 0
1433
+ };
1434
+ if (left === "cl-01" && right === "cl-01" || left === "cl-02" && right === "cl-02") return {
1435
+ kind: "kern",
1436
+ naturalWidthEm: -.5
1437
+ };
1438
+ if (left === "cl-07" || left === "cl-06") return {
1439
+ kind: "glue",
1440
+ naturalWidthEm: .5,
1441
+ shrink: {
1442
+ priority: 1,
1443
+ amountEm: .5
1444
+ }
1445
+ };
1446
+ if (left === "cl-02" || right === "cl-01") return {
1447
+ kind: "glue",
1448
+ naturalWidthEm: .5,
1449
+ shrink: {
1450
+ priority: 2,
1451
+ amountEm: .5
1452
+ }
1453
+ };
1454
+ return {
1455
+ kind: "glue",
1456
+ naturalWidthEm: 0,
1457
+ stretch: {
1458
+ priority: 3,
1459
+ amountEm: .25
1460
+ }
1461
+ };
1462
+ }
1463
+ function breakPenalty(left, right) {
1464
+ if (left === "cl-01" || left === "cl-12") return null;
1465
+ if (right === "cl-02" || right === "cl-03" || right === "cl-04" || right === "cl-05" || right === "cl-06" || right === "cl-07" || right === "cl-09" || right === "cl-10" || right === "cl-11" || right === "cl-13") return null;
1466
+ if (left === "cl-08" && right === "cl-08") return null;
1467
+ return 0;
1468
+ }
1469
+ const defaultJapaneseTypesettingProfile = {
1470
+ classify,
1471
+ boxMetrics: (characterClass, measuredAdvanceEm) => ({
1472
+ advanceEm: characterClass === "cl-01" || characterClass === "cl-02" || characterClass === "cl-06" || characterClass === "cl-07" ? Math.min(.5, measuredAdvanceEm) : measuredAdvanceEm,
1473
+ renderOffsetEm: characterClass === "cl-01" ? -.5 : 0
1474
+ }),
1475
+ pairSpacing,
1476
+ lineStartSpacing: (first) => first === "cl-01" ? {
1477
+ kind: "glue",
1478
+ naturalWidthEm: .5,
1479
+ shrink: {
1480
+ priority: 2,
1481
+ amountEm: .5
1482
+ }
1483
+ } : null,
1484
+ lineEndSpacing: (last) => last === "cl-02" || last === "cl-06" || last === "cl-07" ? {
1485
+ kind: "glue",
1486
+ naturalWidthEm: .5,
1487
+ shrink: {
1488
+ priority: 2,
1489
+ amountEm: .5
1490
+ }
1491
+ } : null,
1492
+ breakPenalty,
1493
+ canHang: (characterClass) => characterClass === "cl-07" || characterClass === "cl-06"
1494
+ };
1495
+ //#endregion
1496
+ //#region src/composer/internal/paragraph-layout.ts
1497
+ const EPSILON = 1e-9;
1498
+ function skipSourceGaps(atoms, start) {
1499
+ let cursor = start;
1500
+ while (atoms[cursor]?.sourceGap === true) cursor += 1;
1501
+ return cursor;
1502
+ }
1503
+ function previousVisible(atoms, end) {
1504
+ for (let index = end - 1; index >= 0; index -= 1) if (atoms[index]?.sourceGap === false) return index;
1505
+ }
1506
+ function opportunities(atoms, classes, start, end, profile) {
1507
+ const result = [];
1508
+ const firstClass = classes[start];
1509
+ const startSpacing = firstClass === void 0 ? null : profile.lineStartSpacing(firstClass);
1510
+ if (startSpacing !== null) result.push({
1511
+ boundary: start,
1512
+ spacing: startSpacing
1513
+ });
1514
+ for (let right = start + 1; right < end; right += 1) {
1515
+ const leftAtom = atoms[right - 1];
1516
+ const rightAtom = atoms[right];
1517
+ const leftClass = classes[right - 1];
1518
+ const rightClass = classes[right];
1519
+ if (leftAtom === void 0 || rightAtom === void 0 || leftClass === void 0 || rightClass === void 0 || leftAtom.sourceGap || rightAtom.sourceGap || !leftAtom.pairSpacingAfter) continue;
1520
+ result.push({
1521
+ boundary: right,
1522
+ spacing: profile.pairSpacing(leftClass, rightClass)
1523
+ });
1524
+ }
1525
+ const lastClass = classes[end - 1];
1526
+ const endSpacing = lastClass === void 0 ? null : profile.lineEndSpacing(lastClass);
1527
+ if (endSpacing !== null) result.push({
1528
+ boundary: end,
1529
+ spacing: endSpacing
1530
+ });
1531
+ return result;
1532
+ }
1533
+ function capacity(values, select) {
1534
+ return values.reduce((total, opportunity) => total + (select(opportunity.spacing)?.amountEm ?? 0), 0);
1535
+ }
1536
+ function resolveSpacings(values, adjustment, amountEm) {
1537
+ const remainingByBoundary = /* @__PURE__ */ new Map();
1538
+ let remaining = amountEm;
1539
+ let priorityCost = 0;
1540
+ const capacities = values.flatMap((opportunity) => {
1541
+ const selected = adjustment === "shrink" ? opportunity.spacing.shrink : opportunity.spacing.stretch;
1542
+ return selected === void 0 ? [] : [{
1543
+ ...opportunity,
1544
+ capacity: selected
1545
+ }];
1546
+ }).sort((left, right) => left.capacity.priority - right.capacity.priority || left.boundary - right.boundary);
1547
+ for (const opportunity of capacities) {
1548
+ if (remaining <= EPSILON) break;
1549
+ const used = Math.min(remaining, opportunity.capacity.amountEm);
1550
+ remainingByBoundary.set(opportunity.boundary, used);
1551
+ priorityCost += used * opportunity.capacity.priority;
1552
+ remaining -= used;
1553
+ }
1554
+ return {
1555
+ spacings: values.map(({ boundary, spacing }) => {
1556
+ const used = remainingByBoundary.get(boundary) ?? 0;
1557
+ return {
1558
+ boundary,
1559
+ kind: spacing.kind,
1560
+ naturalWidthEm: spacing.naturalWidthEm,
1561
+ widthEm: adjustment === "shrink" ? spacing.naturalWidthEm - used : spacing.naturalWidthEm + used
1562
+ };
1563
+ }),
1564
+ priorityCost
1565
+ };
1566
+ }
1567
+ function candidate(atoms, classes, start, end, lineLengthEm, profile) {
1568
+ const contentStart = skipSourceGaps(atoms, start);
1569
+ const suppressedIndexes = Array.from({ length: contentStart - start }, (_, index) => start + index);
1570
+ const pairValues = opportunities(atoms, classes, contentStart, end, profile);
1571
+ const naturalSizeEm = atoms.slice(contentStart, end).reduce((total, atom) => total + atom.boxAdvanceEm, 0) + pairValues.reduce((total, value) => total + value.spacing.naturalWidthEm, 0);
1572
+ const terminal = skipSourceGaps(atoms, end) === atoms.length;
1573
+ const shrinkCapacity = capacity(pairValues, ({ shrink }) => shrink);
1574
+ const stretchCapacity = capacity(pairValues, ({ stretch }) => stretch);
1575
+ const overflow = naturalSizeEm - lineLengthEm;
1576
+ const underflow = lineLengthEm - naturalSizeEm;
1577
+ const lastVisible = previousVisible(atoms, end);
1578
+ const lastClass = lastVisible === void 0 ? void 0 : classes[lastVisible];
1579
+ const lastAdvance = lastVisible === void 0 ? 0 : atoms[lastVisible]?.boxAdvanceEm ?? 0;
1580
+ const trailingSpacing = pairValues.find(({ boundary }) => boundary === end)?.spacing.naturalWidthEm ?? 0;
1581
+ if (terminal && overflow <= EPSILON) return {
1582
+ start,
1583
+ contentStart,
1584
+ end,
1585
+ suppressedIndexes,
1586
+ pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings,
1587
+ inlineSizeEm: naturalSizeEm,
1588
+ break: { kind: "paragraph-end" },
1589
+ hangingIndex: null,
1590
+ deformationRatio: 0,
1591
+ priorityCost: 0
1592
+ };
1593
+ if (Math.abs(overflow) <= EPSILON) return {
1594
+ start,
1595
+ contentStart,
1596
+ end,
1597
+ suppressedIndexes,
1598
+ pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings,
1599
+ inlineSizeEm: naturalSizeEm,
1600
+ break: { kind: "natural" },
1601
+ hangingIndex: null,
1602
+ deformationRatio: 0,
1603
+ priorityCost: 0
1604
+ };
1605
+ if (overflow > 0 && overflow <= shrinkCapacity + EPSILON) {
1606
+ const resolved = resolveSpacings(pairValues, "shrink", overflow);
1607
+ return {
1608
+ start,
1609
+ contentStart,
1610
+ end,
1611
+ suppressedIndexes,
1612
+ pairSpacings: resolved.spacings,
1613
+ inlineSizeEm: lineLengthEm,
1614
+ break: { kind: "shrunk" },
1615
+ hangingIndex: null,
1616
+ deformationRatio: shrinkCapacity === 0 ? 0 : overflow / shrinkCapacity,
1617
+ priorityCost: resolved.priorityCost
1618
+ };
1619
+ }
1620
+ if (!terminal && overflow > 0 && lastVisible !== void 0 && lastClass !== void 0 && profile.canHang(lastClass) && naturalSizeEm - lastAdvance - trailingSpacing <= lineLengthEm + EPSILON) return {
1621
+ start,
1622
+ contentStart,
1623
+ end,
1624
+ suppressedIndexes,
1625
+ pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings.filter(({ boundary }) => boundary !== end),
1626
+ inlineSizeEm: naturalSizeEm - lastAdvance - trailingSpacing,
1627
+ break: { kind: "hanging" },
1628
+ hangingIndex: lastVisible,
1629
+ deformationRatio: overflow / Math.max(lastAdvance, EPSILON),
1630
+ priorityCost: 0
1631
+ };
1632
+ if (!terminal && underflow > EPSILON && underflow <= stretchCapacity + EPSILON) {
1633
+ const resolved = resolveSpacings(pairValues, "stretch", underflow);
1634
+ return {
1635
+ start,
1636
+ contentStart,
1637
+ end,
1638
+ suppressedIndexes,
1639
+ pairSpacings: resolved.spacings,
1640
+ inlineSizeEm: lineLengthEm,
1641
+ break: { kind: "stretched" },
1642
+ hangingIndex: null,
1643
+ deformationRatio: stretchCapacity === 0 ? 0 : underflow / stretchCapacity,
1644
+ priorityCost: resolved.priorityCost
1645
+ };
1646
+ }
1647
+ return {
1648
+ start,
1649
+ contentStart,
1650
+ end,
1651
+ suppressedIndexes,
1652
+ pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings,
1653
+ inlineSizeEm: naturalSizeEm,
1654
+ break: { kind: "forced" },
1655
+ hangingIndex: null,
1656
+ deformationRatio: Math.abs(naturalSizeEm - lineLengthEm) / Math.max(lineLengthEm, EPSILON),
1657
+ priorityCost: 0
1658
+ };
1659
+ }
1660
+ function scoreFor(line, previousFitness) {
1661
+ const mode = line.break.kind;
1662
+ const fitness = Math.min(3, Math.floor(line.deformationRatio * 4));
1663
+ const transition = Math.abs(previousFitness - fitness) > 1 ? 1 : 0;
1664
+ const modeCounts = {
1665
+ forced: mode === "forced" ? 1 : 0,
1666
+ stretched: mode === "stretched" ? 1 : 0,
1667
+ hanging: mode === "hanging" ? 1 : 0,
1668
+ shrunk: mode === "shrunk" ? 1 : 0
1669
+ };
1670
+ return {
1671
+ score: [
1672
+ modeCounts.forced,
1673
+ modeCounts.stretched,
1674
+ modeCounts.hanging,
1675
+ modeCounts.shrunk,
1676
+ line.priorityCost,
1677
+ line.deformationRatio ** 3,
1678
+ transition
1679
+ ],
1680
+ fitness
1681
+ };
1682
+ }
1683
+ function addScore(left, right) {
1684
+ return [
1685
+ left[0] + right[0],
1686
+ left[1] + right[1],
1687
+ left[2] + right[2],
1688
+ left[3] + right[3],
1689
+ left[4] + right[4],
1690
+ left[5] + right[5],
1691
+ left[6] + right[6]
1692
+ ];
1693
+ }
1694
+ function compareBreaks(left, right) {
1695
+ const length = Math.min(left.length, right.length);
1696
+ for (let index = 0; index < length; index += 1) {
1697
+ const difference = (right[index] ?? 0) - (left[index] ?? 0);
1698
+ if (difference !== 0) return difference;
1699
+ }
1700
+ return left.length - right.length;
1701
+ }
1702
+ function isBetter(candidateState, current) {
1703
+ if (current === void 0) return true;
1704
+ for (let index = 0; index < candidateState.score.length; index += 1) {
1705
+ const difference = (candidateState.score[index] ?? 0) - (current.score[index] ?? 0);
1706
+ if (Math.abs(difference) > EPSILON) return difference < 0;
1707
+ }
1708
+ return compareBreaks(candidateState.breaks, current.breaks) < 0;
1709
+ }
1710
+ function layoutParagraph(atoms, lineLengthEm, profile, boundaryAllowed) {
1711
+ if (atoms.length === 0) return [];
1712
+ const classes = atoms.map(({ characterClass }) => characterClass);
1713
+ const states = /* @__PURE__ */ new Map([[0, /* @__PURE__ */ new Map([[0, {
1714
+ score: [
1715
+ 0,
1716
+ 0,
1717
+ 0,
1718
+ 0,
1719
+ 0,
1720
+ 0,
1721
+ 0
1722
+ ],
1723
+ fitness: 0,
1724
+ previous: null,
1725
+ line: null,
1726
+ breaks: []
1727
+ }]])]]);
1728
+ for (let start = 0; start < atoms.length; start += 1) {
1729
+ const activeStates = states.get(start);
1730
+ if (activeStates === void 0) continue;
1731
+ const contentStart = skipSourceGaps(atoms, start);
1732
+ if (contentStart === atoms.length) {
1733
+ const terminalStates = states.get(atoms.length) ?? /* @__PURE__ */ new Map();
1734
+ for (const state of activeStates.values()) if (isBetter(state, terminalStates.get(state.fitness))) terminalStates.set(state.fitness, state);
1735
+ states.set(atoms.length, terminalStates);
1736
+ continue;
1737
+ }
1738
+ for (const state of activeStates.values()) for (let end = contentStart + 1; end <= atoms.length; end += 1) {
1739
+ const right = skipSourceGaps(atoms, end);
1740
+ const left = previousVisible(atoms, end);
1741
+ if (atoms[end - 1]?.sourceGap === true) continue;
1742
+ if (end < atoms.length && right < atoms.length && (left === void 0 || !boundaryAllowed(left, right))) continue;
1743
+ const line = candidate(atoms, classes, start, end, lineLengthEm, profile);
1744
+ 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))) {
1745
+ if (line.inlineSizeEm > lineLengthEm * 2 && end > contentStart + 1) break;
1746
+ continue;
1747
+ }
1748
+ const lineScore = scoreFor(line, state.fitness);
1749
+ const nextState = {
1750
+ score: addScore(state.score, lineScore.score),
1751
+ fitness: lineScore.fitness,
1752
+ previous: {
1753
+ index: start,
1754
+ fitness: state.fitness
1755
+ },
1756
+ line,
1757
+ breaks: [...state.breaks, end]
1758
+ };
1759
+ const statesAtEnd = states.get(end) ?? /* @__PURE__ */ new Map();
1760
+ if (isBetter(nextState, statesAtEnd.get(nextState.fitness))) {
1761
+ statesAtEnd.set(nextState.fitness, nextState);
1762
+ states.set(end, statesAtEnd);
1763
+ }
1764
+ if (line.inlineSizeEm > lineLengthEm * 2 && line.break.kind === "forced" && end > contentStart + 1) break;
1765
+ }
1766
+ }
1767
+ const terminal = [...states.get(atoms.length)?.values() ?? []].reduce((best, state) => isBetter(state, best) ? state : best, void 0);
1768
+ if (terminal === void 0) return [];
1769
+ const lines = [];
1770
+ let cursor = terminal;
1771
+ while (cursor?.line !== null && cursor?.line !== void 0) {
1772
+ lines.push(cursor.line);
1773
+ cursor = cursor.previous === null ? void 0 : states.get(cursor.previous.index)?.get(cursor.previous.fitness);
1774
+ }
1775
+ lines.reverse();
1776
+ const last = lines.at(-1);
1777
+ if (last !== void 0 && last.end < atoms.length) lines[lines.length - 1] = {
1778
+ ...last,
1779
+ suppressedIndexes: [...last.suppressedIndexes, ...Array.from({ length: atoms.length - last.end }, (_, index) => last.end + index)]
1780
+ };
1781
+ return lines;
1782
+ }
1783
+ const NovelLine = {
1143
1784
  schema: readonlyObject({
1144
1785
  range: v.nullable(ManuscriptRange.schema),
1145
- cells: readonlyArray(GridCell.schema)
1786
+ inlineSizeEm: v.pipe(v.number(), v.finite(), v.minValue(0)),
1787
+ items: readonlyArray(ComposedInlineItem.schema),
1788
+ break: LineBreakResult.schema,
1789
+ annotations: readonlyArray(ComposedAnnotationFragment.schema)
1146
1790
  }),
1147
- of: (cells) => ({
1148
- cells,
1149
- range: ManuscriptRange.merge(cells.map(({ range }) => range))
1150
- }),
1151
- /**
1152
- * Pads with empty cells so every line in a stage has the same length.
1153
- */
1154
- padded: (cells, charsPerLine) => GridLine.of([...cells, ...Array.from({ length: Math.max(0, charsPerLine - cells.length) }, GridCell.empty)])
1791
+ empty: () => ({
1792
+ range: null,
1793
+ inlineSizeEm: 0,
1794
+ items: [],
1795
+ break: { kind: "paragraph-end" },
1796
+ annotations: []
1797
+ })
1155
1798
  };
1156
- const GridStage = {
1799
+ const NovelStage = {
1157
1800
  schema: readonlyObject({
1158
1801
  range: v.nullable(ManuscriptRange.schema),
1159
- lines: readonlyArray(GridLine.schema)
1802
+ lines: readonlyArray(NovelLine.schema)
1160
1803
  }),
1161
1804
  of: (lines) => ({
1162
1805
  lines,
1163
1806
  range: ManuscriptRange.merge(lines.map(({ range }) => range))
1164
1807
  })
1165
1808
  };
1166
- const GridPage = {
1809
+ const NovelPage = {
1167
1810
  schema: readonlyObject({
1168
1811
  range: v.nullable(ManuscriptRange.schema),
1169
- stages: readonlyArray(GridStage.schema)
1812
+ stages: readonlyArray(NovelStage.schema)
1170
1813
  }),
1171
1814
  of: (stages) => ({
1172
1815
  stages,
1173
1816
  range: ManuscriptRange.merge(stages.map(({ range }) => range))
1174
1817
  })
1175
1818
  };
1176
- //#endregion
1177
- //#region src/composer/manuscript-geometry.ts
1178
- const STAGE_GAP_CELLS = 2;
1179
- const LINE_GAP_CELLS = .5;
1180
- const nonNegativeLength = () => v.pipe(v.number(), v.finite(), v.minValue(0));
1181
- const ManuscriptGeometrySchema = readonlyObject({
1182
- paperWidthMm: nonNegativeLength(),
1183
- paperHeightMm: nonNegativeLength(),
1184
- cellSizeMm: nonNegativeLength(),
1185
- lineGapMm: nonNegativeLength(),
1186
- fontSizePt: FontSizePt.schema,
1187
- gridWidthMm: nonNegativeLength(),
1188
- gridHeightMm: nonNegativeLength(),
1189
- marginInlineMm: v.pipe(v.number(), v.finite()),
1190
- marginBlockMm: v.pipe(v.number(), v.finite()),
1191
- fitsPaper: v.boolean()
1192
- });
1193
- /**
1194
- * Every stage stacked vertically, plus the gap between adjacent stages.
1195
- */
1196
- function gridHeightInCells(grid) {
1197
- return grid.stagesPerPage * grid.charsPerLine + STAGE_GAP_CELLS * (grid.stagesPerPage - 1);
1198
- }
1199
- /**
1200
- * Every vertical line side by side, plus the gap between adjacent lines.
1201
- */
1202
- function gridWidthInCells(grid) {
1203
- return grid.linesPerStage + LINE_GAP_CELLS * (grid.linesPerStage - 1);
1204
- }
1205
- const ManuscriptGeometry = {
1206
- schema: ManuscriptGeometrySchema,
1207
- of: (grid, appearance) => {
1208
- const paper = PaperSize.of(appearance.paperSize);
1209
- const cellSizeMm = ptToMm(appearance.fontSizePt);
1210
- const gridWidthMm = gridWidthInCells(grid) * cellSizeMm;
1211
- const gridHeightMm = gridHeightInCells(grid) * cellSizeMm;
1212
- const marginInlineMm = (paper.widthMm - gridWidthMm) / 2;
1213
- const marginBlockMm = (paper.heightMm - gridHeightMm) / 2;
1214
- return {
1215
- paperWidthMm: paper.widthMm,
1216
- paperHeightMm: paper.heightMm,
1217
- cellSizeMm,
1218
- lineGapMm: LINE_GAP_CELLS * cellSizeMm,
1219
- fontSizePt: appearance.fontSizePt,
1220
- gridWidthMm,
1221
- gridHeightMm,
1222
- marginInlineMm,
1223
- marginBlockMm,
1224
- fitsPaper: marginInlineMm >= 0 && marginBlockMm >= 0
1225
- };
1226
- },
1227
- /**
1228
- * The largest step-aligned point size whose grid still fits the paper, clamped to the range.
1229
- */
1230
- maxFontSizePt: (grid, paperSizeId) => {
1231
- const paper = PaperSize.of(paperSizeId);
1232
- const maxCellSizeMm = Math.min(paper.widthMm / gridWidthInCells(grid), paper.heightMm / gridHeightInCells(grid));
1233
- const steps = Math.floor(mmToPt(maxCellSizeMm) / FontSizePt.range.step + 1e-9);
1234
- const aligned = Math.round(steps * FontSizePt.range.step * 1e3) / 1e3;
1235
- return Math.min(FontSizePt.range.max, Math.max(FontSizePt.range.min, aligned));
1236
- }
1237
- };
1238
- const ManuscriptGridLayout = { schema: readonlyObject({
1239
- pages: readonlyArray(GridPage.schema),
1819
+ const NovelLayout = { schema: readonlyObject({
1820
+ pages: readonlyArray(NovelPage.schema),
1240
1821
  geometry: ManuscriptGeometry.schema,
1241
1822
  stats: CompositionStatistics.schema
1242
1823
  }) };
1243
1824
  //#endregion
1244
- //#region src/composer/grid-composer.ts
1245
- const COMPOSER_ID = NamespacedId.of("kg/grid");
1246
- /**
1247
- * Splits display graphemes on line breaks. A trailing break does not create an empty last line.
1248
- */
1825
+ //#region src/composer/novel-composer.ts
1826
+ const COMPOSER_ID = NamespacedId.of("kg/novel");
1827
+ const TYPESETTING_PROFILE = defaultJapaneseTypesettingProfile;
1828
+ const ASCII_ALPHANUMERIC = /^[A-Za-z0-9]$/u;
1829
+ const ASCII_TWO_DIGITS = /^[0-9]{2}$/u;
1830
+ const UPRIGHT_LATIN_ABBREVIATION = /^(?:[A-Z]+|[A-Z][a-z]{1,2})$/u;
1831
+ const FULLWIDTH_ALPHANUMERIC = /^[A-Za-z0-9]$/u;
1832
+ function presentation(kind, graphemes) {
1833
+ const groupRange = ManuscriptRange.merge(graphemes.map(({ range }) => range));
1834
+ return groupRange === null ? void 0 : {
1835
+ kind,
1836
+ groupRange
1837
+ };
1838
+ }
1839
+ function presentedSourceLine(sourceLine) {
1840
+ const presented = [];
1841
+ let cursor = 0;
1842
+ while (cursor < sourceLine.length) {
1843
+ const first = sourceLine[cursor];
1844
+ if (first === void 0) break;
1845
+ if (!ASCII_ALPHANUMERIC.test(first.value)) {
1846
+ const itemPresentation = presentation(FULLWIDTH_ALPHANUMERIC.test(first.value) ? "upright" : "mixed", [first]);
1847
+ if (itemPresentation !== void 0) presented.push({
1848
+ grapheme: first,
1849
+ presentation: itemPresentation
1850
+ });
1851
+ cursor += 1;
1852
+ continue;
1853
+ }
1854
+ const start = cursor;
1855
+ while (ASCII_ALPHANUMERIC.test(sourceLine[cursor]?.value ?? "")) cursor += 1;
1856
+ const run = sourceLine.slice(start, cursor);
1857
+ const text = run.map(({ value }) => value).join("");
1858
+ const runPresentation = presentation(ASCII_TWO_DIGITS.test(text) ? "tate-chu-yoko" : text.length === 1 || UPRIGHT_LATIN_ABBREVIATION.test(text) ? "upright" : "sideways", run);
1859
+ if (runPresentation !== void 0) presented.push(...run.map((grapheme) => ({
1860
+ grapheme,
1861
+ presentation: runPresentation
1862
+ })));
1863
+ }
1864
+ return presented;
1865
+ }
1249
1866
  function displayedLines(graphemes) {
1250
1867
  const lines = [[]];
1251
1868
  let endedWithLineBreak = false;
@@ -1255,20 +1872,13 @@ function displayedLines(graphemes) {
1255
1872
  endedWithLineBreak = true;
1256
1873
  continue;
1257
1874
  }
1258
- lines.at(-1).push(grapheme);
1875
+ lines.at(-1)?.push(grapheme);
1259
1876
  endedWithLineBreak = false;
1260
1877
  }
1261
1878
  if (lines.length > 1 && endedWithLineBreak) lines.pop();
1262
1879
  return lines;
1263
1880
  }
1264
- function toCells(sourceLine, annotations) {
1265
- return sourceLine.map((grapheme) => ({
1266
- value: grapheme.value,
1267
- range: grapheme.range,
1268
- annotations: ManuscriptAnnotation.overlapping(annotations, grapheme.range)
1269
- }));
1270
- }
1271
- function hangingGapIndexes(sourceLine) {
1881
+ function validGapIndexes(sourceLine) {
1272
1882
  const line = sourceLine.map(({ value }) => value).join("");
1273
1883
  const gapStarts = new Set(questionOrExclamationSpacings(line).filter((spacing) => spacing.kind === "valid").map(({ gap }) => gap.start));
1274
1884
  const indexes = /* @__PURE__ */ new Set();
@@ -1279,31 +1889,345 @@ function hangingGapIndexes(sourceLine) {
1279
1889
  }
1280
1890
  return indexes;
1281
1891
  }
1282
- /**
1283
- * Wraps one source line across as many grid lines as its length requires. A valid gap after ! or ?
1284
- * that would start a wrapped line is consumed as hanging punctuation instead of occupying a cell.
1285
- */
1286
- function wrapLine(cells, charsPerLine, hangingGaps) {
1287
- if (cells.length === 0) return [GridLine.padded([], charsPerLine)];
1288
- const lines = [];
1289
- let cursor = 0;
1290
- while (cursor < cells.length) {
1291
- if (lines.length > 0 && hangingGaps.has(cursor)) cursor += 1;
1292
- if (cursor >= cells.length) break;
1293
- lines.push(GridLine.padded(cells.slice(cursor, cursor + charsPerLine), charsPerLine));
1294
- cursor += charsPerLine;
1892
+ function indicesInside(atoms, annotation) {
1893
+ return atoms.flatMap(({ grapheme }, index) => ManuscriptRange.overlaps(grapheme.range, annotation.range) ? [index] : []);
1894
+ }
1895
+ function measure(measurer, text, role, settings, presentationKind) {
1896
+ const context = {
1897
+ text,
1898
+ fontPreset: settings.appearance.fontPreset,
1899
+ writingMode: "vertical-rl"
1900
+ };
1901
+ const measurement = role === "base" ? measurer({
1902
+ ...context,
1903
+ role,
1904
+ presentation: presentationKind ?? "mixed"
1905
+ }) : measurer({
1906
+ ...context,
1907
+ role
1908
+ });
1909
+ const parsed = v.safeParse(InlineMeasurement.schema, measurement);
1910
+ if (!parsed.success) return void 0;
1911
+ const { advanceEm } = parsed.output;
1912
+ return advanceEm;
1913
+ }
1914
+ function measureSourceLine(sourceLine, annotations, settings, measurer) {
1915
+ const mutable = presentedSourceLine(sourceLine).map(({ grapheme, presentation }) => {
1916
+ const renderAdvanceEm = measure(measurer, grapheme.value, "base", settings, presentation.kind);
1917
+ if (renderAdvanceEm === void 0) return void 0;
1918
+ const characterClass = TYPESETTING_PROFILE.classify({
1919
+ value: grapheme.value,
1920
+ presentation: presentation.kind
1921
+ });
1922
+ const metrics = TYPESETTING_PROFILE.boxMetrics(characterClass, renderAdvanceEm);
1923
+ return {
1924
+ grapheme,
1925
+ boxAdvanceEm: metrics.advanceEm,
1926
+ renderAdvanceEm,
1927
+ renderOffsetEm: metrics.renderOffsetEm,
1928
+ presentation
1929
+ };
1930
+ });
1931
+ if (mutable.some((atom) => atom === void 0)) return void 0;
1932
+ const atoms = mutable.flatMap((atom) => atom === void 0 ? [] : [atom]);
1933
+ for (const annotation of annotations) {
1934
+ if (annotation.kind !== "ruby") continue;
1935
+ const indexes = indicesInside(atoms, annotation);
1936
+ if (indexes.length === 0) continue;
1937
+ if ((annotation.reading.kind === "group" ? [annotation.reading.text] : annotation.reading.segments).some((text) => [...graphemeSegmenter.segment(text)].some(({ segment }) => measure(measurer, segment, "ruby", settings) === void 0))) return void 0;
1938
+ if (annotation.reading.kind === "group") {
1939
+ const readingAdvance = measure(measurer, annotation.reading.text, "ruby", settings);
1940
+ if (readingAdvance === void 0) return void 0;
1941
+ const baseAdvance = indexes.reduce((total, index) => total + (atoms[index]?.boxAdvanceEm ?? 0), 0);
1942
+ const extra = Math.max(0, readingAdvance - baseAdvance) / indexes.length;
1943
+ for (const index of indexes) {
1944
+ const atom = atoms[index];
1945
+ if (atom !== void 0) atom.boxAdvanceEm += extra;
1946
+ }
1947
+ continue;
1948
+ }
1949
+ for (const [segmentIndex, index] of indexes.entries()) {
1950
+ const segment = annotation.reading.segments[segmentIndex];
1951
+ const atom = atoms[index];
1952
+ if (segment === void 0 || atom === void 0) continue;
1953
+ const readingAdvance = measure(measurer, segment, "ruby", settings);
1954
+ if (readingAdvance === void 0) return void 0;
1955
+ atom.boxAdvanceEm = Math.max(atom.boxAdvanceEm, readingAdvance);
1956
+ }
1295
1957
  }
1296
- return lines;
1958
+ return {
1959
+ atoms,
1960
+ suppressedIndexes: validGapIndexes(sourceLine)
1961
+ };
1297
1962
  }
1298
- function blankLines(count, charsPerLine) {
1299
- return Array.from({ length: count }, () => GridLine.padded([], charsPerLine));
1963
+ function breakInsideFittableGroupRuby(atoms, annotations, boundary, lineLengthEm) {
1964
+ const right = atoms[boundary];
1965
+ if (right === void 0) return false;
1966
+ const graphemeIndex = right.grapheme.range.graphemes.start;
1967
+ return annotations.some((annotation) => {
1968
+ if (annotation.kind !== "ruby" || annotation.reading.kind !== "group") return false;
1969
+ if (graphemeIndex <= annotation.range.graphemes.start || graphemeIndex >= annotation.range.graphemes.end) return false;
1970
+ return indicesInside(atoms, annotation).reduce((total, index) => total + (atoms[index]?.boxAdvanceEm ?? 0), 0) <= lineLengthEm;
1971
+ });
1972
+ }
1973
+ function samePresentationGroup(left, right) {
1974
+ return left.presentation.groupRange.graphemes.start === right.presentation.groupRange.graphemes.start && left.presentation.groupRange.graphemes.end === right.presentation.groupRange.graphemes.end;
1975
+ }
1976
+ function legalBoundary(atoms, annotations, leftIndex, rightIndex, lineLengthEm, profile) {
1977
+ const left = atoms[leftIndex];
1978
+ const right = atoms[rightIndex];
1979
+ if (left === void 0 || right === void 0) return true;
1980
+ return !samePresentationGroup(left, right) && profile.breakPenalty(profile.classify({
1981
+ value: left.grapheme.value,
1982
+ presentation: left.presentation.kind
1983
+ }), profile.classify({
1984
+ value: right.grapheme.value,
1985
+ presentation: right.presentation.kind
1986
+ })) !== null && !breakInsideFittableGroupRuby(atoms, annotations, rightIndex, lineLengthEm);
1987
+ }
1988
+ function continuationFor(annotation, range) {
1989
+ const starts = range.graphemes.start === annotation.range.graphemes.start;
1990
+ const ends = range.graphemes.end === annotation.range.graphemes.end;
1991
+ if (starts && ends) return "whole";
1992
+ if (starts) return "start";
1993
+ if (ends) return "end";
1994
+ return "middle";
1995
+ }
1996
+ function groupReadingsForFragments(annotation, fragmentRanges, baseAdvances) {
1997
+ const baseLength = annotation.range.graphemes.end - annotation.range.graphemes.start;
1998
+ if (annotation.reading.kind !== "group") return [];
1999
+ const reading = [...graphemeSegmenter.segment(annotation.reading.text)].map(({ segment }) => segment);
2000
+ const advances = Array.from({ length: baseLength }, (_, index) => baseAdvances.get(annotation.range.graphemes.start + index));
2001
+ const measured = advances.every((advance) => advance !== void 0);
2002
+ const advanceFor = (fragment) => {
2003
+ const fragmentStart = fragment.graphemes.start - annotation.range.graphemes.start;
2004
+ const fragmentLength = fragment.graphemes.end - fragment.graphemes.start;
2005
+ return measured ? advances.slice(fragmentStart, fragmentStart + fragmentLength).reduce((total, advance) => total + advance, 0) : fragmentLength;
2006
+ };
2007
+ const weights = fragmentRanges.map(advanceFor);
2008
+ const effectiveWeights = weights.reduce((total, weight) => total + weight, 0) > 0 ? weights : fragmentRanges.map(({ graphemes }) => graphemes.end - graphemes.start);
2009
+ const totalWeight = effectiveWeights.reduce((total, weight) => total + weight, 0);
2010
+ const reserveEveryFragment = reading.length >= fragmentRanges.length;
2011
+ const fragments = [];
2012
+ let readingStart = 0;
2013
+ let accumulatedWeight = 0;
2014
+ for (const [index, weight] of effectiveWeights.entries()) {
2015
+ accumulatedWeight += weight;
2016
+ const remainingFragments = effectiveWeights.length - index - 1;
2017
+ const proportionalEnd = index === effectiveWeights.length - 1 ? reading.length : Math.round(accumulatedWeight * reading.length / totalWeight);
2018
+ const minimumEnd = reserveEveryFragment ? readingStart + 1 : readingStart;
2019
+ const maximumEnd = reserveEveryFragment ? reading.length - remainingFragments : reading.length;
2020
+ const readingEnd = Math.min(maximumEnd, Math.max(minimumEnd, proportionalEnd));
2021
+ fragments.push(reading.slice(readingStart, readingEnd).join(""));
2022
+ readingStart = readingEnd;
2023
+ }
2024
+ return fragments;
2025
+ }
2026
+ function readingForFragment(annotation, range, fragmentPlan) {
2027
+ const start = range.graphemes.start - annotation.range.graphemes.start;
2028
+ const length = range.graphemes.end - range.graphemes.start;
2029
+ if (annotation.reading.kind !== "group") return {
2030
+ kind: annotation.reading.kind,
2031
+ text: annotation.reading.segments.slice(start, start + length).join("")
2032
+ };
2033
+ const fragmentIndex = fragmentPlan.ranges.findIndex(({ graphemes }) => graphemes.start === range.graphemes.start && graphemes.end === range.graphemes.end);
2034
+ return {
2035
+ kind: "group",
2036
+ text: fragmentPlan.groupReadings[fragmentIndex] ?? ""
2037
+ };
2038
+ }
2039
+ function positionReading(text, baseOffsetEm, baseAdvanceEm, settings, measurer) {
2040
+ const measured = [...graphemeSegmenter.segment(text)].map(({ segment }) => ({
2041
+ value: segment,
2042
+ advanceEm: measure(measurer, segment, "ruby", settings) ?? 0
2043
+ }));
2044
+ const total = measured.reduce((sum, grapheme) => sum + grapheme.advanceEm, 0);
2045
+ const edgeGap = measured.length > 0 && total < baseAdvanceEm ? (baseAdvanceEm - total) / (2 * measured.length) : 0;
2046
+ let offsetEm = baseOffsetEm + (total > baseAdvanceEm ? (baseAdvanceEm - total) / 2 : edgeGap);
2047
+ return measured.map((grapheme, index) => {
2048
+ const positioned = {
2049
+ value: grapheme.value,
2050
+ advanceEm: grapheme.advanceEm,
2051
+ offsetEm
2052
+ };
2053
+ offsetEm += grapheme.advanceEm;
2054
+ if (index < measured.length - 1) offsetEm += edgeGap * 2;
2055
+ return positioned;
2056
+ });
2057
+ }
2058
+ function annotationFragmentRange(graphemes, suppressed, sourceGlues, annotation) {
2059
+ return ManuscriptRange.merge([
2060
+ ...graphemes.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range),
2061
+ ...suppressed.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range),
2062
+ ...sourceGlues.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range)
2063
+ ]);
2064
+ }
2065
+ function annotationFragments(graphemes, suppressed, sourceGlues, annotations, settings, measurer, fragmentPlansByAnnotation) {
2066
+ const fragments = [];
2067
+ for (const annotation of annotations) {
2068
+ const covered = graphemes.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range));
2069
+ const fragmentRange = annotationFragmentRange(graphemes, suppressed, sourceGlues, annotation);
2070
+ const first = covered[0];
2071
+ const last = covered.at(-1);
2072
+ if (fragmentRange === null || first === void 0 || last === void 0) continue;
2073
+ const common = {
2074
+ annotationRange: annotation.range,
2075
+ fragmentRange,
2076
+ continuation: continuationFor(annotation, fragmentRange)
2077
+ };
2078
+ switch (annotation.kind) {
2079
+ case "bold":
2080
+ case "italic":
2081
+ fragments.push({
2082
+ kind: annotation.kind,
2083
+ ...common
2084
+ });
2085
+ break;
2086
+ case "emphasis":
2087
+ fragments.push({
2088
+ kind: "emphasis",
2089
+ mark: annotation.mark,
2090
+ ...common
2091
+ });
2092
+ break;
2093
+ case "ruby": {
2094
+ const fragmentPlan = fragmentPlansByAnnotation.get(annotation);
2095
+ if (fragmentPlan === void 0) break;
2096
+ const baseOffsetEm = first.layoutSpan.offsetEm;
2097
+ const baseAdvanceEm = last.layoutSpan.offsetEm + last.layoutSpan.advanceEm - baseOffsetEm;
2098
+ const reading = readingForFragment(annotation, fragmentRange, fragmentPlan);
2099
+ fragments.push({
2100
+ kind: "ruby",
2101
+ rubyKind: reading.kind,
2102
+ reading: reading.text,
2103
+ baseOffsetEm,
2104
+ baseAdvanceEm,
2105
+ readingGraphemes: positionReading(reading.text, baseOffsetEm, baseAdvanceEm, settings, measurer),
2106
+ ...common
2107
+ });
2108
+ break;
2109
+ }
2110
+ }
2111
+ }
2112
+ return fragments;
2113
+ }
2114
+ function positionedLine(atoms, sourceGapIndexes, plan) {
2115
+ const items = plan.suppressedIndexes.flatMap((index) => {
2116
+ const atom = atoms[index];
2117
+ return atom === void 0 ? [] : [{
2118
+ kind: "suppressed",
2119
+ value: atom.grapheme.value,
2120
+ range: atom.grapheme.range,
2121
+ reason: "question-or-exclamation-gap"
2122
+ }];
2123
+ });
2124
+ const spacings = new Map(plan.pairSpacings.map((spacing) => [spacing.boundary, spacing]));
2125
+ let offsetEm = 0;
2126
+ const positionSpacing = (spacing) => {
2127
+ if (spacing.kind === "kern") items.push({
2128
+ kind: "kern",
2129
+ offsetEm,
2130
+ widthEm: spacing.widthEm
2131
+ });
2132
+ else items.push({
2133
+ kind: "glue",
2134
+ origin: "generated",
2135
+ offsetEm,
2136
+ widthEm: spacing.widthEm,
2137
+ naturalWidthEm: spacing.naturalWidthEm,
2138
+ adjustment: spacing.widthEm < spacing.naturalWidthEm ? "shrunk" : spacing.widthEm > spacing.naturalWidthEm ? "stretched" : "natural"
2139
+ });
2140
+ offsetEm += spacing.widthEm;
2141
+ };
2142
+ for (let index = plan.contentStart; index < plan.end; index += 1) {
2143
+ const spacing = spacings.get(index);
2144
+ if (spacing !== void 0) positionSpacing(spacing);
2145
+ const atom = atoms[index];
2146
+ if (atom === void 0) continue;
2147
+ if (sourceGapIndexes.has(index)) {
2148
+ items.push({
2149
+ kind: "glue",
2150
+ origin: "source",
2151
+ value: atom.grapheme.value,
2152
+ range: atom.grapheme.range,
2153
+ offsetEm,
2154
+ widthEm: atom.boxAdvanceEm,
2155
+ naturalWidthEm: atom.boxAdvanceEm,
2156
+ adjustment: "natural"
2157
+ });
2158
+ offsetEm += atom.boxAdvanceEm;
2159
+ continue;
2160
+ }
2161
+ const hanging = plan.hangingIndex === index;
2162
+ const layoutAdvanceEm = hanging ? 0 : atom.boxAdvanceEm;
2163
+ items.push({
2164
+ kind: "glyph",
2165
+ value: atom.grapheme.value,
2166
+ range: atom.grapheme.range,
2167
+ layoutSpan: {
2168
+ offsetEm,
2169
+ advanceEm: layoutAdvanceEm
2170
+ },
2171
+ renderSpan: {
2172
+ offsetEm: offsetEm + atom.renderOffsetEm,
2173
+ advanceEm: atom.renderAdvanceEm
2174
+ },
2175
+ disposition: hanging ? "hanging" : "placed",
2176
+ presentation: atom.presentation
2177
+ });
2178
+ offsetEm += layoutAdvanceEm;
2179
+ }
2180
+ const trailingSpacing = spacings.get(plan.end);
2181
+ if (trailingSpacing !== void 0) positionSpacing(trailingSpacing);
2182
+ const ranges = items.flatMap((item) => item.kind === "glyph" || item.kind === "suppressed" || item.kind === "glue" && item.origin === "source" ? [item.range] : []);
2183
+ return {
2184
+ range: ManuscriptRange.merge(ranges),
2185
+ inlineSizeEm: Math.max(0, plan.inlineSizeEm),
2186
+ items,
2187
+ break: plan.break,
2188
+ annotations: []
2189
+ };
2190
+ }
2191
+ function wrapSourceLine(sourceLine, annotations, settings, measurer, baseAdvances) {
2192
+ if (sourceLine.atoms.length === 0) return [NovelLine.empty()];
2193
+ const profile = TYPESETTING_PROFILE;
2194
+ const lines = layoutParagraph(sourceLine.atoms.map(({ grapheme, boxAdvanceEm, presentation }, index) => {
2195
+ const current = sourceLine.atoms[index];
2196
+ const next = sourceLine.atoms[index + 1];
2197
+ return {
2198
+ value: grapheme.value,
2199
+ boxAdvanceEm,
2200
+ sourceGap: sourceLine.suppressedIndexes.has(index),
2201
+ characterClass: profile.classify({
2202
+ value: grapheme.value,
2203
+ presentation: presentation.kind
2204
+ }),
2205
+ pairSpacingAfter: current === void 0 || next === void 0 || !samePresentationGroup(current, next) && !breakInsideFittableGroupRuby(sourceLine.atoms, annotations, index + 1, settings.flow.lineLengthEm)
2206
+ };
2207
+ }), settings.flow.lineLengthEm, profile, (leftIndex, rightIndex) => legalBoundary(sourceLine.atoms, annotations, leftIndex, rightIndex, settings.flow.lineLengthEm, profile)).map((plan) => positionedLine(sourceLine.atoms, sourceLine.suppressedIndexes, plan));
2208
+ const fragmentPlansByAnnotation = new Map(annotations.map((annotation) => {
2209
+ const ranges = lines.flatMap((line) => {
2210
+ const range = annotationFragmentRange(line.items.filter((item) => item.kind === "glyph"), line.items.filter((item) => item.kind === "suppressed"), line.items.flatMap((item) => item.kind === "glue" && item.origin === "source" ? [item] : []), annotation);
2211
+ return range === null ? [] : [range];
2212
+ });
2213
+ return [annotation, {
2214
+ ranges,
2215
+ groupReadings: annotation.kind === "ruby" ? groupReadingsForFragments(annotation, ranges, baseAdvances) : []
2216
+ }];
2217
+ }));
2218
+ return lines.map((line) => ({
2219
+ range: line.range,
2220
+ inlineSizeEm: line.inlineSizeEm,
2221
+ items: line.items,
2222
+ break: line.break,
2223
+ annotations: annotationFragments(line.items.filter((item) => item.kind === "glyph"), line.items.filter((item) => item.kind === "suppressed"), line.items.flatMap((item) => item.kind === "glue" && item.origin === "source" ? [item] : []), annotations, settings, measurer, fragmentPlansByAnnotation)
2224
+ }));
2225
+ }
2226
+ function blankLines(count) {
2227
+ return Array.from({ length: count }, NovelLine.empty);
1300
2228
  }
1301
- /**
1302
- * Walks every line position on every page in reading order, handing out content lines only where
1303
- * the stage and page offsets leave room. Always emits at least one page.
1304
- */
1305
2229
  function buildPages(contentLines, settings) {
1306
- const { charsPerLine, linesPerStage, stagesPerPage } = settings.grid;
2230
+ const { linesPerStage, stagesPerPage } = settings.flow;
1307
2231
  const { stage: stageOffset, page: pageOffset } = settings.offsets;
1308
2232
  const usablePerStage = linesPerStage - LineOffset.total(stageOffset);
1309
2233
  const usablePerPage = usablePerStage * stagesPerPage;
@@ -1319,47 +2243,51 @@ function buildPages(contentLines, settings) {
1319
2243
  const withinPage = usableIndex >= pageOffset.leading && usableIndex < usablePerPage - pageOffset.trailing;
1320
2244
  const next = contentLines[contentIndex];
1321
2245
  if (!withinStage || !withinPage || next === void 0) {
1322
- lines.push(GridLine.padded([], charsPerLine));
2246
+ lines.push(NovelLine.empty());
1323
2247
  continue;
1324
2248
  }
1325
2249
  lines.push(next);
1326
2250
  contentIndex += 1;
1327
2251
  }
1328
- stages.push(GridStage.of(lines));
2252
+ stages.push(NovelStage.of(lines));
1329
2253
  }
1330
- pages.push(GridPage.of(stages));
2254
+ pages.push(NovelPage.of(stages));
1331
2255
  }
1332
2256
  return pages;
1333
2257
  }
1334
- function composeGrid(manuscript, settings) {
1335
- const { charsPerLine } = settings.grid;
1336
- const sourceLines = displayedLines(manuscript.graphemes);
1337
- const manuscriptLines = sourceLines.flatMap((line) => wrapLine(toCells(line, manuscript.annotations), charsPerLine, hangingGapIndexes(line)));
1338
- const pages = buildPages([
1339
- ...blankLines(settings.offsets.document.leading, charsPerLine),
1340
- ...manuscriptLines,
1341
- ...blankLines(settings.offsets.document.trailing, charsPerLine)
1342
- ], settings);
1343
- return ManuscriptResult.succeed({
1344
- pages,
1345
- geometry: ManuscriptGeometry.of(settings.grid, settings.appearance),
1346
- stats: {
1347
- chars: sourceLines.reduce((total, line) => total + line.length, 0),
1348
- sourceLines: sourceLines.length,
1349
- pages: pages.length
1350
- }
1351
- });
2258
+ function createCompose(measurer) {
2259
+ return (manuscript, settings) => {
2260
+ const sourceLines = displayedLines(manuscript.graphemes);
2261
+ const measured = sourceLines.map((line) => measureSourceLine(line, manuscript.annotations, settings, measurer));
2262
+ if (measured.some((line) => line === void 0)) return ManuscriptResult.fail({ reason: "inline measurer returned a negative or non-finite value" });
2263
+ const baseAdvances = /* @__PURE__ */ new Map();
2264
+ for (const line of measured) for (const atom of line?.atoms ?? []) baseAdvances.set(atom.grapheme.range.graphemes.start, atom.boxAdvanceEm);
2265
+ const manuscriptLines = measured.flatMap((line) => line === void 0 ? [] : wrapSourceLine(line, manuscript.annotations, settings, measurer, baseAdvances));
2266
+ const pages = buildPages([
2267
+ ...blankLines(settings.offsets.document.leading),
2268
+ ...manuscriptLines,
2269
+ ...blankLines(settings.offsets.document.trailing)
2270
+ ], settings);
2271
+ return ManuscriptResult.succeed({
2272
+ pages,
2273
+ geometry: ManuscriptGeometry.of(settings.flow, settings.appearance),
2274
+ stats: {
2275
+ chars: sourceLines.reduce((total, line) => total + line.length, 0),
2276
+ sourceLines: sourceLines.length,
2277
+ pages: pages.length
2278
+ }
2279
+ });
2280
+ };
1352
2281
  }
1353
- /**
1354
- * Places text on Japanese manuscript paper: vertical lines grouped into stages, stages into pages.
1355
- * Settings arrive already validated by {@link composeManuscript}, so this never rejects.
1356
- */
1357
- const manuscriptGridComposer = {
1358
- id: COMPOSER_ID,
1359
- settingsSchema: ManuscriptCompositionSettings.schema,
1360
- layoutSchema: ManuscriptGridLayout.schema,
1361
- compose: composeGrid
1362
- };
2282
+ function createNovelComposer(options) {
2283
+ return {
2284
+ id: COMPOSER_ID,
2285
+ settingsSchema: NovelCompositionSettings.schema,
2286
+ layoutSchema: NovelLayout.schema,
2287
+ compose: createCompose(options.measurer)
2288
+ };
2289
+ }
2290
+ const novelComposer = createNovelComposer({ measurer: logicalInlineMeasurer });
1363
2291
  //#endregion
1364
2292
  //#region src/proofreading/invalid-rule-options.ts
1365
2293
  const InvalidRuleOptions = { describe: (error) => `ルール ${error.ruleId} のオプション "${error.option}" が不正です: ${error.issues.map(({ message }) => message).join(", ")}` };
@@ -2105,4 +3033,4 @@ function createDefaultProofreadingRules() {
2105
3033
  ];
2106
3034
  }
2107
3035
  //#endregion
2108
- export { BoldAnnotation, ComposeError, CompositionStatistics, DiagnosticOrigin, DiagnosticSeverity, DisplayRange, EmphasisAnnotation, FontPreset, FontPresetId, FontSizePt, GraphemeRange, GridCell, GridLine, GridPage, GridSettings, GridStage, InvalidRuleOptions, ItalicAnnotation, LineOffset, ManuscriptAnnotation, ManuscriptAppearanceSettings, ManuscriptCompositionSettings, ManuscriptDiagnostic, ManuscriptGeometry, ManuscriptGridLayout, ManuscriptOffsets, ManuscriptRange, ManuscriptResult, NamespacedId, PaperSize, PaperSizeId, ParseError, ParsedGrapheme, ParsedManuscript, ProofreadError, ProofreadingReport, ProofreadingRuleMeta, Rejection, RubyAnnotation, SourcePosition, SourceRange, TextRange, ValidationIssue, assertNever, composeManuscript, consistentKanjiOpeningRule, consistentLatinWidthRule, consistentNumeralWidthRule, createConsistentKanjiOpeningRule, createDashRule, createDefaultProofreadingRules, createMaxArabicNumeralDigitsRule, createParagraphOpeningRule, dashRule, ellipsisCharacterRule, evenEllipsisRule, fullwidthJapanesePunctuationRule, kakuyomuParser, manuscriptGridComposer, maxArabicNumeralDigitsRule, minusBeforeNumberRule, mmToPt, noConsecutiveInterpunctRule, noConsecutivePunctuationRule, paragraphOpeningRule, parseManuscript, pixivParser, plainTextParser, proofreadManuscript, ptToMm, punctuationBeforeClosingQuoteRule, spaceAfterQuestionOrExclamationRule, variantCharacterRule };
3036
+ export { BoldAnnotation, ComposeError, ComposedAnnotationFragment, ComposedInlineItem, CompositionStatistics, DiagnosticOrigin, DiagnosticSeverity, DisplayRange, EmphasisAnnotation, FontPreset, FontPresetId, FontSizePt, GraphemeRange, InlineMeasurement, InlineSpan, InvalidRuleOptions, ItalicAnnotation, LineBreakResult, LineOffset, ManuscriptAnnotation, ManuscriptAppearanceSettings, ManuscriptDiagnostic, ManuscriptGeometry, ManuscriptOffsets, ManuscriptRange, ManuscriptResult, NamespacedId, NovelCompositionSettings, NovelFlowSettings, NovelLayout, NovelLine, NovelPage, NovelStage, PaperSize, PaperSizeId, ParseError, ParsedGrapheme, ParsedManuscript, ProofreadError, ProofreadingReport, ProofreadingRuleMeta, Rejection, RubyAnnotation, RubyKind, RubyReading, SourcePosition, SourceRange, TextRange, ValidationIssue, VerticalTextPresentation, assertNever, composeManuscript, consistentKanjiOpeningRule, consistentLatinWidthRule, consistentNumeralWidthRule, createConsistentKanjiOpeningRule, createDashRule, createDefaultProofreadingRules, createMaxArabicNumeralDigitsRule, createNovelComposer, createParagraphOpeningRule, dashRule, ellipsisCharacterRule, evenEllipsisRule, fullwidthJapanesePunctuationRule, kakuyomuParser, logicalInlineMeasurer, maxArabicNumeralDigitsRule, minusBeforeNumberRule, mmToPt, noConsecutiveInterpunctRule, noConsecutivePunctuationRule, novelComposer, paragraphOpeningRule, parseManuscript, pixivParser, plainTextParser, proofreadManuscript, ptToMm, punctuationBeforeClosingQuoteRule, spaceAfterQuestionOrExclamationRule, variantCharacterRule };