@sushichan044/kg-core 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +44 -10
  2. package/dist/index.d.mts +20761 -6890
  3. package/dist/index.mjs +654 -198
  4. package/package.json +2 -1
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$1 = () => 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$1()
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$1(),
1043
+ baseAdvanceEm: emLength$1(),
1044
+ readingGraphemes: readonlyArray(PositionedReadingGraphemeSchema),
1045
+ ...common$1
1046
+ })
1047
+ ]) };
989
1048
  const LineOffset = {
990
1049
  schema: readonlyObject({
991
1050
  leading: nonNegativeInteger(),
@@ -1023,44 +1082,143 @@ 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)
1058
- }),
1059
- empty: () => ({
1060
- value: null,
1061
- range: null,
1062
- annotations: []
1063
- })
1142
+ //#endregion
1143
+ //#region src/composer/inline-measurer.ts
1144
+ function graphemeWidth(grapheme) {
1145
+ let width = 1;
1146
+ for (const character of grapheme) {
1147
+ const codePoint = character.codePointAt(0);
1148
+ if (codePoint !== void 0) width = Math.max(width, eastAsianWidth(codePoint, { ambiguousAsWide: true }));
1149
+ }
1150
+ return width / 2;
1151
+ }
1152
+ /**
1153
+ * Deterministic Japanese-context measurement in logical em units.
1154
+ */
1155
+ const logicalInlineMeasurer = (request) => {
1156
+ const { text } = request;
1157
+ const width = [...graphemeSegmenter.segment(text)].reduce((total, { segment }) => total + graphemeWidth(segment), 0);
1158
+ if (request.role === "ruby") return width / 2;
1159
+ return request.presentation === "upright" ? [...graphemeSegmenter.segment(text)].length : width;
1160
+ };
1161
+ //#endregion
1162
+ //#region src/composer/manuscript-geometry.ts
1163
+ const STAGE_GAP_CELLS = 2;
1164
+ const LINE_GAP_CELLS = .5;
1165
+ const nonNegativeLength = () => v.pipe(v.number(), v.finite(), v.minValue(0));
1166
+ const ManuscriptGeometrySchema = readonlyObject({
1167
+ paperWidthMm: nonNegativeLength(),
1168
+ paperHeightMm: nonNegativeLength(),
1169
+ cellSizeMm: nonNegativeLength(),
1170
+ lineGapMm: nonNegativeLength(),
1171
+ fontSizePt: FontSizePt.schema,
1172
+ contentWidthMm: nonNegativeLength(),
1173
+ contentHeightMm: nonNegativeLength(),
1174
+ marginInlineMm: v.pipe(v.number(), v.finite()),
1175
+ marginBlockMm: v.pipe(v.number(), v.finite()),
1176
+ fitsPaper: v.boolean()
1177
+ });
1178
+ /**
1179
+ * Every stage stacked vertically, plus the gap between adjacent stages.
1180
+ */
1181
+ function contentHeightInEm(flow) {
1182
+ return flow.stagesPerPage * flow.lineLengthEm + STAGE_GAP_CELLS * (flow.stagesPerPage - 1);
1183
+ }
1184
+ /**
1185
+ * Every vertical line side by side, plus the gap between adjacent lines.
1186
+ */
1187
+ function contentWidthInEm(flow) {
1188
+ return flow.linesPerStage + LINE_GAP_CELLS * (flow.linesPerStage - 1);
1189
+ }
1190
+ const ManuscriptGeometry = {
1191
+ schema: ManuscriptGeometrySchema,
1192
+ of: (flow, appearance) => {
1193
+ const paper = PaperSize.of(appearance.paperSize);
1194
+ const cellSizeMm = ptToMm(appearance.fontSizePt);
1195
+ const contentWidthMm = contentWidthInEm(flow) * cellSizeMm;
1196
+ const contentHeightMm = contentHeightInEm(flow) * cellSizeMm;
1197
+ const marginInlineMm = (paper.widthMm - contentWidthMm) / 2;
1198
+ const marginBlockMm = (paper.heightMm - contentHeightMm) / 2;
1199
+ return {
1200
+ paperWidthMm: paper.widthMm,
1201
+ paperHeightMm: paper.heightMm,
1202
+ cellSizeMm,
1203
+ lineGapMm: LINE_GAP_CELLS * cellSizeMm,
1204
+ fontSizePt: appearance.fontSizePt,
1205
+ contentWidthMm,
1206
+ contentHeightMm,
1207
+ marginInlineMm,
1208
+ marginBlockMm,
1209
+ fitsPaper: marginInlineMm >= 0 && marginBlockMm >= 0
1210
+ };
1211
+ },
1212
+ /**
1213
+ * The largest step-aligned point size whose text area still fits the paper.
1214
+ */
1215
+ maxFontSizePt: (flow, paperSizeId) => {
1216
+ const paper = PaperSize.of(paperSizeId);
1217
+ const maxCellSizeMm = Math.min(paper.widthMm / contentWidthInEm(flow), paper.heightMm / contentHeightInEm(flow));
1218
+ const steps = Math.floor(mmToPt(maxCellSizeMm) / FontSizePt.range.step + 1e-9);
1219
+ const aligned = Math.round(steps * FontSizePt.range.step * 1e3) / 1e3;
1220
+ return Math.min(FontSizePt.range.max, Math.max(FontSizePt.range.min, aligned));
1221
+ }
1064
1222
  };
1065
1223
  //#endregion
1066
1224
  //#region src/internal/japanese-brackets.ts
@@ -1139,113 +1297,136 @@ function analyzeMatch(line, marks) {
1139
1297
  function questionOrExclamationSpacings(line) {
1140
1298
  return [...line.matchAll(MARKS)].map((marks) => analyzeMatch(line, marks)).filter((spacing) => spacing !== void 0);
1141
1299
  }
1142
- const GridLine = {
1300
+ //#endregion
1301
+ //#region src/composer/vertical-text-presentation.ts
1302
+ const common = { groupRange: ManuscriptRange.schema };
1303
+ const VerticalTextPresentation = { schema: v.variant("kind", [
1304
+ readonlyObject({
1305
+ kind: v.literal("mixed"),
1306
+ ...common
1307
+ }),
1308
+ readonlyObject({
1309
+ kind: v.literal("upright"),
1310
+ ...common
1311
+ }),
1312
+ readonlyObject({
1313
+ kind: v.literal("sideways"),
1314
+ ...common
1315
+ }),
1316
+ readonlyObject({
1317
+ kind: v.literal("tate-chu-yoko"),
1318
+ ...common
1319
+ })
1320
+ ]) };
1321
+ //#endregion
1322
+ //#region src/composer/positioned-grapheme.ts
1323
+ const emLength = () => v.pipe(v.number(), v.finite(), v.minValue(0));
1324
+ const PositionedGrapheme = { schema: readonlyObject({
1325
+ kind: v.literal("grapheme"),
1326
+ value: v.string(),
1327
+ range: ManuscriptRange.schema,
1328
+ offsetEm: emLength(),
1329
+ advanceEm: emLength(),
1330
+ disposition: v.union([v.literal("placed"), v.literal("hanging")]),
1331
+ presentation: VerticalTextPresentation.schema
1332
+ }) };
1333
+ const SuppressedGrapheme = { schema: readonlyObject({
1334
+ kind: v.literal("suppressed"),
1335
+ value: v.string(),
1336
+ range: ManuscriptRange.schema,
1337
+ reason: v.literal("question-or-exclamation-gap")
1338
+ }) };
1339
+ const NovelLine = {
1143
1340
  schema: readonlyObject({
1144
1341
  range: v.nullable(ManuscriptRange.schema),
1145
- cells: readonlyArray(GridCell.schema)
1342
+ advanceEm: v.pipe(v.number(), v.finite(), v.minValue(0)),
1343
+ graphemes: readonlyArray(PositionedGrapheme.schema),
1344
+ suppressed: readonlyArray(SuppressedGrapheme.schema),
1345
+ annotations: readonlyArray(ComposedAnnotationFragment.schema)
1146
1346
  }),
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)])
1347
+ empty: () => ({
1348
+ range: null,
1349
+ advanceEm: 0,
1350
+ graphemes: [],
1351
+ suppressed: [],
1352
+ annotations: []
1353
+ })
1155
1354
  };
1156
- const GridStage = {
1355
+ const NovelStage = {
1157
1356
  schema: readonlyObject({
1158
1357
  range: v.nullable(ManuscriptRange.schema),
1159
- lines: readonlyArray(GridLine.schema)
1358
+ lines: readonlyArray(NovelLine.schema)
1160
1359
  }),
1161
1360
  of: (lines) => ({
1162
1361
  lines,
1163
1362
  range: ManuscriptRange.merge(lines.map(({ range }) => range))
1164
1363
  })
1165
1364
  };
1166
- const GridPage = {
1365
+ const NovelPage = {
1167
1366
  schema: readonlyObject({
1168
1367
  range: v.nullable(ManuscriptRange.schema),
1169
- stages: readonlyArray(GridStage.schema)
1368
+ stages: readonlyArray(NovelStage.schema)
1170
1369
  }),
1171
1370
  of: (stages) => ({
1172
1371
  stages,
1173
1372
  range: ManuscriptRange.merge(stages.map(({ range }) => range))
1174
1373
  })
1175
1374
  };
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),
1375
+ const NovelLayout = { schema: readonlyObject({
1376
+ pages: readonlyArray(NovelPage.schema),
1240
1377
  geometry: ManuscriptGeometry.schema,
1241
1378
  stats: CompositionStatistics.schema
1242
1379
  }) };
1243
1380
  //#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
- */
1381
+ //#region src/composer/novel-composer.ts
1382
+ const COMPOSER_ID = NamespacedId.of("kg/novel");
1383
+ const LINE_START_PROHIBITED = /* @__PURE__ */ new Set(`、。,.・:;?!‼⁇⁈⁉ヽヾゝゞ々ーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖ${CLOSING_BRACKETS}】〙〟⦆»`);
1384
+ const LINE_END_PROHIBITED = /* @__PURE__ */ new Set(`${OPENING_BRACKETS}【〘〝⦅«`);
1385
+ const HANGING_PUNCTUATION = /* @__PURE__ */ new Set("、。,.!?");
1386
+ const INSEPARABLE_PAIRS = /* @__PURE__ */ new Set([
1387
+ "……",
1388
+ "――",
1389
+ "──",
1390
+ "〳〵"
1391
+ ]);
1392
+ const ASCII_ALPHANUMERIC = /^[A-Za-z0-9]$/u;
1393
+ const ASCII_TWO_DIGITS = /^[0-9]{2}$/u;
1394
+ const UPRIGHT_LATIN_ABBREVIATION = /^(?:[A-Z]+|[A-Z][a-z]{1,2})$/u;
1395
+ const FULLWIDTH_ALPHANUMERIC = /^[A-Za-z0-9]$/u;
1396
+ function presentation(kind, graphemes) {
1397
+ const groupRange = ManuscriptRange.merge(graphemes.map(({ range }) => range));
1398
+ return groupRange === null ? void 0 : {
1399
+ kind,
1400
+ groupRange
1401
+ };
1402
+ }
1403
+ function presentedSourceLine(sourceLine) {
1404
+ const presented = [];
1405
+ let cursor = 0;
1406
+ while (cursor < sourceLine.length) {
1407
+ const first = sourceLine[cursor];
1408
+ if (first === void 0) break;
1409
+ if (!ASCII_ALPHANUMERIC.test(first.value)) {
1410
+ const itemPresentation = presentation(FULLWIDTH_ALPHANUMERIC.test(first.value) ? "upright" : "mixed", [first]);
1411
+ if (itemPresentation !== void 0) presented.push({
1412
+ grapheme: first,
1413
+ presentation: itemPresentation
1414
+ });
1415
+ cursor += 1;
1416
+ continue;
1417
+ }
1418
+ const start = cursor;
1419
+ while (ASCII_ALPHANUMERIC.test(sourceLine[cursor]?.value ?? "")) cursor += 1;
1420
+ const run = sourceLine.slice(start, cursor);
1421
+ const text = run.map(({ value }) => value).join("");
1422
+ const runPresentation = presentation(ASCII_TWO_DIGITS.test(text) ? "tate-chu-yoko" : text.length === 1 || UPRIGHT_LATIN_ABBREVIATION.test(text) ? "upright" : "sideways", run);
1423
+ if (runPresentation !== void 0) presented.push(...run.map((grapheme) => ({
1424
+ grapheme,
1425
+ presentation: runPresentation
1426
+ })));
1427
+ }
1428
+ return presented;
1429
+ }
1249
1430
  function displayedLines(graphemes) {
1250
1431
  const lines = [[]];
1251
1432
  let endedWithLineBreak = false;
@@ -1255,20 +1436,13 @@ function displayedLines(graphemes) {
1255
1436
  endedWithLineBreak = true;
1256
1437
  continue;
1257
1438
  }
1258
- lines.at(-1).push(grapheme);
1439
+ lines.at(-1)?.push(grapheme);
1259
1440
  endedWithLineBreak = false;
1260
1441
  }
1261
1442
  if (lines.length > 1 && endedWithLineBreak) lines.pop();
1262
1443
  return lines;
1263
1444
  }
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) {
1445
+ function validGapIndexes(sourceLine) {
1272
1446
  const line = sourceLine.map(({ value }) => value).join("");
1273
1447
  const gapStarts = new Set(questionOrExclamationSpacings(line).filter((spacing) => spacing.kind === "valid").map(({ gap }) => gap.start));
1274
1448
  const indexes = /* @__PURE__ */ new Set();
@@ -1279,31 +1453,309 @@ function hangingGapIndexes(sourceLine) {
1279
1453
  }
1280
1454
  return indexes;
1281
1455
  }
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)];
1456
+ function indicesInside(atoms, annotation) {
1457
+ return atoms.flatMap(({ grapheme }, index) => ManuscriptRange.overlaps(grapheme.range, annotation.range) ? [index] : []);
1458
+ }
1459
+ function measure(measurer, text, role, settings, presentationKind) {
1460
+ const context = {
1461
+ text,
1462
+ fontPreset: settings.appearance.fontPreset,
1463
+ writingMode: "vertical-rl"
1464
+ };
1465
+ const value = role === "base" ? measurer({
1466
+ ...context,
1467
+ role,
1468
+ presentation: presentationKind ?? "mixed"
1469
+ }) : measurer({
1470
+ ...context,
1471
+ role
1472
+ });
1473
+ return Number.isFinite(value) && value >= 0 ? value : void 0;
1474
+ }
1475
+ function measureSourceLine(sourceLine, annotations, settings, measurer) {
1476
+ const mutable = presentedSourceLine(sourceLine).map(({ grapheme, presentation }) => {
1477
+ const advanceEm = measure(measurer, grapheme.value, "base", settings, presentation.kind);
1478
+ return advanceEm === void 0 ? void 0 : {
1479
+ grapheme,
1480
+ advanceEm,
1481
+ presentation
1482
+ };
1483
+ });
1484
+ if (mutable.some((atom) => atom === void 0)) return void 0;
1485
+ const atoms = mutable.flatMap((atom) => atom === void 0 ? [] : [atom]);
1486
+ for (const annotation of annotations) {
1487
+ if (annotation.kind !== "ruby") continue;
1488
+ const indexes = indicesInside(atoms, annotation);
1489
+ if (indexes.length === 0) continue;
1490
+ 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;
1491
+ if (annotation.reading.kind === "group") {
1492
+ const readingAdvance = measure(measurer, annotation.reading.text, "ruby", settings);
1493
+ if (readingAdvance === void 0) return void 0;
1494
+ const baseAdvance = indexes.reduce((total, index) => total + (atoms[index]?.advanceEm ?? 0), 0);
1495
+ const extra = Math.max(0, readingAdvance - baseAdvance) / indexes.length;
1496
+ for (const index of indexes) {
1497
+ const atom = atoms[index];
1498
+ if (atom !== void 0) atom.advanceEm += extra;
1499
+ }
1500
+ continue;
1501
+ }
1502
+ for (const [segmentIndex, index] of indexes.entries()) {
1503
+ const segment = annotation.reading.segments[segmentIndex];
1504
+ const atom = atoms[index];
1505
+ if (segment === void 0 || atom === void 0) continue;
1506
+ const readingAdvance = measure(measurer, segment, "ruby", settings);
1507
+ if (readingAdvance === void 0) return void 0;
1508
+ atom.advanceEm = Math.max(atom.advanceEm, readingAdvance);
1509
+ }
1510
+ }
1511
+ return {
1512
+ atoms,
1513
+ suppressedIndexes: validGapIndexes(sourceLine)
1514
+ };
1515
+ }
1516
+ function breakInsideFittableGroupRuby(atoms, annotations, boundary, lineLengthEm) {
1517
+ const right = atoms[boundary];
1518
+ if (right === void 0) return false;
1519
+ const graphemeIndex = right.grapheme.range.graphemes.start;
1520
+ return annotations.some((annotation) => {
1521
+ if (annotation.kind !== "ruby" || annotation.reading.kind !== "group") return false;
1522
+ if (graphemeIndex <= annotation.range.graphemes.start || graphemeIndex >= annotation.range.graphemes.end) return false;
1523
+ return indicesInside(atoms, annotation).reduce((total, index) => total + (atoms[index]?.advanceEm ?? 0), 0) <= lineLengthEm;
1524
+ });
1525
+ }
1526
+ function samePresentationGroup(left, right) {
1527
+ return left.presentation.groupRange.graphemes.start === right.presentation.groupRange.graphemes.start && left.presentation.groupRange.graphemes.end === right.presentation.groupRange.graphemes.end;
1528
+ }
1529
+ function legalBoundary(atoms, annotations, boundary, lineLengthEm) {
1530
+ const left = atoms[boundary - 1];
1531
+ const right = atoms[boundary];
1532
+ if (left === void 0 || right === void 0) return true;
1533
+ return !samePresentationGroup(left, right) && !LINE_END_PROHIBITED.has(left.grapheme.value) && !LINE_START_PROHIBITED.has(right.grapheme.value) && !INSEPARABLE_PAIRS.has(left.grapheme.value + right.grapheme.value) && !breakInsideFittableGroupRuby(atoms, annotations, boundary, lineLengthEm);
1534
+ }
1535
+ function continuationFor(annotation, range) {
1536
+ const starts = range.graphemes.start === annotation.range.graphemes.start;
1537
+ const ends = range.graphemes.end === annotation.range.graphemes.end;
1538
+ if (starts && ends) return "whole";
1539
+ if (starts) return "start";
1540
+ if (ends) return "end";
1541
+ return "middle";
1542
+ }
1543
+ function groupReadingsForFragments(annotation, fragmentRanges, baseAdvances) {
1544
+ const baseLength = annotation.range.graphemes.end - annotation.range.graphemes.start;
1545
+ if (annotation.reading.kind !== "group") return [];
1546
+ const reading = [...graphemeSegmenter.segment(annotation.reading.text)].map(({ segment }) => segment);
1547
+ const advances = Array.from({ length: baseLength }, (_, index) => baseAdvances.get(annotation.range.graphemes.start + index));
1548
+ const measured = advances.every((advance) => advance !== void 0);
1549
+ const advanceFor = (fragment) => {
1550
+ const fragmentStart = fragment.graphemes.start - annotation.range.graphemes.start;
1551
+ const fragmentLength = fragment.graphemes.end - fragment.graphemes.start;
1552
+ return measured ? advances.slice(fragmentStart, fragmentStart + fragmentLength).reduce((total, advance) => total + advance, 0) : fragmentLength;
1553
+ };
1554
+ const weights = fragmentRanges.map(advanceFor);
1555
+ const effectiveWeights = weights.reduce((total, weight) => total + weight, 0) > 0 ? weights : fragmentRanges.map(({ graphemes }) => graphemes.end - graphemes.start);
1556
+ const totalWeight = effectiveWeights.reduce((total, weight) => total + weight, 0);
1557
+ const reserveEveryFragment = reading.length >= fragmentRanges.length;
1558
+ const fragments = [];
1559
+ let readingStart = 0;
1560
+ let accumulatedWeight = 0;
1561
+ for (const [index, weight] of effectiveWeights.entries()) {
1562
+ accumulatedWeight += weight;
1563
+ const remainingFragments = effectiveWeights.length - index - 1;
1564
+ const proportionalEnd = index === effectiveWeights.length - 1 ? reading.length : Math.round(accumulatedWeight * reading.length / totalWeight);
1565
+ const minimumEnd = reserveEveryFragment ? readingStart + 1 : readingStart;
1566
+ const maximumEnd = reserveEveryFragment ? reading.length - remainingFragments : reading.length;
1567
+ const readingEnd = Math.min(maximumEnd, Math.max(minimumEnd, proportionalEnd));
1568
+ fragments.push(reading.slice(readingStart, readingEnd).join(""));
1569
+ readingStart = readingEnd;
1570
+ }
1571
+ return fragments;
1572
+ }
1573
+ function readingForFragment(annotation, range, fragmentPlan) {
1574
+ const start = range.graphemes.start - annotation.range.graphemes.start;
1575
+ const length = range.graphemes.end - range.graphemes.start;
1576
+ if (annotation.reading.kind !== "group") return {
1577
+ kind: annotation.reading.kind,
1578
+ text: annotation.reading.segments.slice(start, start + length).join("")
1579
+ };
1580
+ const fragmentIndex = fragmentPlan.ranges.findIndex(({ graphemes }) => graphemes.start === range.graphemes.start && graphemes.end === range.graphemes.end);
1581
+ return {
1582
+ kind: "group",
1583
+ text: fragmentPlan.groupReadings[fragmentIndex] ?? ""
1584
+ };
1585
+ }
1586
+ function positionReading(text, baseOffsetEm, baseAdvanceEm, settings, measurer) {
1587
+ const measured = [...graphemeSegmenter.segment(text)].map(({ segment }) => ({
1588
+ value: segment,
1589
+ advanceEm: measure(measurer, segment, "ruby", settings) ?? 0
1590
+ }));
1591
+ const total = measured.reduce((sum, grapheme) => sum + grapheme.advanceEm, 0);
1592
+ const edgeGap = measured.length > 0 && total < baseAdvanceEm ? (baseAdvanceEm - total) / (2 * measured.length) : 0;
1593
+ let offsetEm = baseOffsetEm + (total > baseAdvanceEm ? (baseAdvanceEm - total) / 2 : edgeGap);
1594
+ return measured.map((grapheme, index) => {
1595
+ const positioned = {
1596
+ value: grapheme.value,
1597
+ advanceEm: grapheme.advanceEm,
1598
+ offsetEm
1599
+ };
1600
+ offsetEm += grapheme.advanceEm;
1601
+ if (index < measured.length - 1) offsetEm += edgeGap * 2;
1602
+ return positioned;
1603
+ });
1604
+ }
1605
+ function annotationFragmentRange(graphemes, suppressed, annotation) {
1606
+ return ManuscriptRange.merge([...graphemes.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range), ...suppressed.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range)]);
1607
+ }
1608
+ function annotationFragments(graphemes, suppressed, annotations, settings, measurer, fragmentPlansByAnnotation) {
1609
+ const fragments = [];
1610
+ for (const annotation of annotations) {
1611
+ const covered = graphemes.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range));
1612
+ const fragmentRange = annotationFragmentRange(graphemes, suppressed, annotation);
1613
+ const first = covered[0];
1614
+ const last = covered.at(-1);
1615
+ if (fragmentRange === null || first === void 0 || last === void 0) continue;
1616
+ const common = {
1617
+ annotationRange: annotation.range,
1618
+ fragmentRange,
1619
+ continuation: continuationFor(annotation, fragmentRange)
1620
+ };
1621
+ switch (annotation.kind) {
1622
+ case "bold":
1623
+ case "italic":
1624
+ fragments.push({
1625
+ kind: annotation.kind,
1626
+ ...common
1627
+ });
1628
+ break;
1629
+ case "emphasis":
1630
+ fragments.push({
1631
+ kind: "emphasis",
1632
+ mark: annotation.mark,
1633
+ ...common
1634
+ });
1635
+ break;
1636
+ case "ruby": {
1637
+ const fragmentPlan = fragmentPlansByAnnotation.get(annotation);
1638
+ if (fragmentPlan === void 0) break;
1639
+ const baseOffsetEm = first.offsetEm;
1640
+ const baseAdvanceEm = last.offsetEm + last.advanceEm - baseOffsetEm;
1641
+ const reading = readingForFragment(annotation, fragmentRange, fragmentPlan);
1642
+ fragments.push({
1643
+ kind: "ruby",
1644
+ rubyKind: reading.kind,
1645
+ reading: reading.text,
1646
+ baseOffsetEm,
1647
+ baseAdvanceEm,
1648
+ readingGraphemes: positionReading(reading.text, baseOffsetEm, baseAdvanceEm, settings, measurer),
1649
+ ...common
1650
+ });
1651
+ break;
1652
+ }
1653
+ }
1654
+ }
1655
+ return fragments;
1656
+ }
1657
+ function positionedLine(atoms, dispositions, suppressed) {
1658
+ let offsetEm = 0;
1659
+ const graphemes = atoms.map(({ grapheme, advanceEm, presentation }, index) => {
1660
+ const positioned = {
1661
+ kind: "grapheme",
1662
+ value: grapheme.value,
1663
+ range: grapheme.range,
1664
+ offsetEm,
1665
+ advanceEm,
1666
+ disposition: dispositions.has(index) ? "hanging" : "placed",
1667
+ presentation
1668
+ };
1669
+ offsetEm += advanceEm;
1670
+ return positioned;
1671
+ });
1672
+ return {
1673
+ range: ManuscriptRange.merge([...graphemes.map(({ range }) => range), ...suppressed.map(({ range }) => range)]),
1674
+ advanceEm: offsetEm,
1675
+ graphemes,
1676
+ suppressed,
1677
+ annotations: []
1678
+ };
1679
+ }
1680
+ function wrapSourceLine(sourceLine, annotations, settings, measurer, baseAdvances) {
1681
+ if (sourceLine.atoms.length === 0) return [NovelLine.empty()];
1288
1682
  const lines = [];
1289
1683
  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;
1684
+ while (cursor < sourceLine.atoms.length) {
1685
+ const suppressed = [];
1686
+ if (lines.length > 0 && sourceLine.suppressedIndexes.has(cursor)) {
1687
+ const atom = sourceLine.atoms[cursor];
1688
+ if (atom !== void 0) suppressed.push({
1689
+ kind: "suppressed",
1690
+ value: atom.grapheme.value,
1691
+ range: atom.grapheme.range,
1692
+ reason: "question-or-exclamation-gap"
1693
+ });
1694
+ cursor += 1;
1695
+ }
1696
+ if (cursor >= sourceLine.atoms.length) {
1697
+ const previous = lines.at(-1);
1698
+ if (previous !== void 0 && suppressed.length > 0) lines[lines.length - 1] = {
1699
+ ...previous,
1700
+ range: ManuscriptRange.merge([...previous.range === null ? [] : [previous.range], ...suppressed.map(({ range }) => range)]),
1701
+ suppressed: [...previous.suppressed, ...suppressed]
1702
+ };
1703
+ break;
1704
+ }
1705
+ const start = cursor;
1706
+ let advance = 0;
1707
+ while (cursor < sourceLine.atoms.length) {
1708
+ const atom = sourceLine.atoms[cursor];
1709
+ if (atom === void 0) break;
1710
+ const previous = sourceLine.atoms[cursor - 1];
1711
+ const beginsPresentationGroup = previous === void 0 || !samePresentationGroup(previous, atom);
1712
+ if (cursor > start && beginsPresentationGroup) {
1713
+ let groupAdvance = 0;
1714
+ for (let index = cursor; index < sourceLine.atoms.length; index += 1) {
1715
+ const member = sourceLine.atoms[index];
1716
+ if (member === void 0 || !samePresentationGroup(atom, member)) break;
1717
+ groupAdvance += member.advanceEm;
1718
+ }
1719
+ if (advance + groupAdvance > settings.flow.lineLengthEm) break;
1720
+ }
1721
+ if (cursor > start && advance + atom.advanceEm > settings.flow.lineLengthEm && (previous === void 0 || !samePresentationGroup(previous, atom))) break;
1722
+ advance += atom.advanceEm;
1723
+ cursor += 1;
1724
+ }
1725
+ let end = Math.max(start + 1, cursor);
1726
+ const hanging = /* @__PURE__ */ new Set();
1727
+ const next = sourceLine.atoms[end];
1728
+ if (next !== void 0 && HANGING_PUNCTUATION.has(next.grapheme.value)) {
1729
+ hanging.add(end - start);
1730
+ end += 1;
1731
+ } else while (end > start + 1 && !legalBoundary(sourceLine.atoms, annotations, end, settings.flow.lineLengthEm)) end -= 1;
1732
+ const atoms = sourceLine.atoms.slice(start, end);
1733
+ lines.push(positionedLine(atoms, hanging, suppressed));
1734
+ cursor = end;
1295
1735
  }
1296
- return lines;
1736
+ const fragmentPlansByAnnotation = new Map(annotations.map((annotation) => {
1737
+ const ranges = lines.flatMap((line) => {
1738
+ const range = annotationFragmentRange(line.graphemes, line.suppressed, annotation);
1739
+ return range === null ? [] : [range];
1740
+ });
1741
+ return [annotation, {
1742
+ ranges,
1743
+ groupReadings: annotation.kind === "ruby" ? groupReadingsForFragments(annotation, ranges, baseAdvances) : []
1744
+ }];
1745
+ }));
1746
+ return lines.map((line) => ({
1747
+ range: line.range,
1748
+ advanceEm: line.advanceEm,
1749
+ graphemes: line.graphemes,
1750
+ suppressed: line.suppressed,
1751
+ annotations: annotationFragments(line.graphemes, line.suppressed, annotations, settings, measurer, fragmentPlansByAnnotation)
1752
+ }));
1297
1753
  }
1298
- function blankLines(count, charsPerLine) {
1299
- return Array.from({ length: count }, () => GridLine.padded([], charsPerLine));
1754
+ function blankLines(count) {
1755
+ return Array.from({ length: count }, NovelLine.empty);
1300
1756
  }
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
1757
  function buildPages(contentLines, settings) {
1306
- const { charsPerLine, linesPerStage, stagesPerPage } = settings.grid;
1758
+ const { linesPerStage, stagesPerPage } = settings.flow;
1307
1759
  const { stage: stageOffset, page: pageOffset } = settings.offsets;
1308
1760
  const usablePerStage = linesPerStage - LineOffset.total(stageOffset);
1309
1761
  const usablePerPage = usablePerStage * stagesPerPage;
@@ -1319,47 +1771,51 @@ function buildPages(contentLines, settings) {
1319
1771
  const withinPage = usableIndex >= pageOffset.leading && usableIndex < usablePerPage - pageOffset.trailing;
1320
1772
  const next = contentLines[contentIndex];
1321
1773
  if (!withinStage || !withinPage || next === void 0) {
1322
- lines.push(GridLine.padded([], charsPerLine));
1774
+ lines.push(NovelLine.empty());
1323
1775
  continue;
1324
1776
  }
1325
1777
  lines.push(next);
1326
1778
  contentIndex += 1;
1327
1779
  }
1328
- stages.push(GridStage.of(lines));
1780
+ stages.push(NovelStage.of(lines));
1329
1781
  }
1330
- pages.push(GridPage.of(stages));
1782
+ pages.push(NovelPage.of(stages));
1331
1783
  }
1332
1784
  return pages;
1333
1785
  }
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
- });
1786
+ function createCompose(measurer) {
1787
+ return (manuscript, settings) => {
1788
+ const sourceLines = displayedLines(manuscript.graphemes);
1789
+ const measured = sourceLines.map((line) => measureSourceLine(line, manuscript.annotations, settings, measurer));
1790
+ if (measured.some((line) => line === void 0)) return ManuscriptResult.fail({ reason: "inline measurer returned a negative or non-finite value" });
1791
+ const baseAdvances = /* @__PURE__ */ new Map();
1792
+ for (const line of measured) for (const atom of line?.atoms ?? []) baseAdvances.set(atom.grapheme.range.graphemes.start, atom.advanceEm);
1793
+ const manuscriptLines = measured.flatMap((line) => line === void 0 ? [] : wrapSourceLine(line, manuscript.annotations, settings, measurer, baseAdvances));
1794
+ const pages = buildPages([
1795
+ ...blankLines(settings.offsets.document.leading),
1796
+ ...manuscriptLines,
1797
+ ...blankLines(settings.offsets.document.trailing)
1798
+ ], settings);
1799
+ return ManuscriptResult.succeed({
1800
+ pages,
1801
+ geometry: ManuscriptGeometry.of(settings.flow, settings.appearance),
1802
+ stats: {
1803
+ chars: sourceLines.reduce((total, line) => total + line.length, 0),
1804
+ sourceLines: sourceLines.length,
1805
+ pages: pages.length
1806
+ }
1807
+ });
1808
+ };
1352
1809
  }
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
- };
1810
+ function createNovelComposer(options) {
1811
+ return {
1812
+ id: COMPOSER_ID,
1813
+ settingsSchema: NovelCompositionSettings.schema,
1814
+ layoutSchema: NovelLayout.schema,
1815
+ compose: createCompose(options.measurer)
1816
+ };
1817
+ }
1818
+ const novelComposer = createNovelComposer({ measurer: logicalInlineMeasurer });
1363
1819
  //#endregion
1364
1820
  //#region src/proofreading/invalid-rule-options.ts
1365
1821
  const InvalidRuleOptions = { describe: (error) => `ルール ${error.ruleId} のオプション "${error.option}" が不正です: ${error.issues.map(({ message }) => message).join(", ")}` };
@@ -2105,4 +2561,4 @@ function createDefaultProofreadingRules() {
2105
2561
  ];
2106
2562
  }
2107
2563
  //#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 };
2564
+ export { BoldAnnotation, ComposeError, ComposedAnnotationFragment, CompositionStatistics, DiagnosticOrigin, DiagnosticSeverity, DisplayRange, EmphasisAnnotation, FontPreset, FontPresetId, FontSizePt, GraphemeRange, InvalidRuleOptions, ItalicAnnotation, LineOffset, ManuscriptAnnotation, ManuscriptAppearanceSettings, ManuscriptDiagnostic, ManuscriptGeometry, ManuscriptOffsets, ManuscriptRange, ManuscriptResult, NamespacedId, NovelCompositionSettings, NovelFlowSettings, NovelLayout, NovelLine, NovelPage, NovelStage, PaperSize, PaperSizeId, ParseError, ParsedGrapheme, ParsedManuscript, PositionedGrapheme, ProofreadError, ProofreadingReport, ProofreadingRuleMeta, Rejection, RubyAnnotation, RubyKind, RubyReading, SourcePosition, SourceRange, SuppressedGrapheme, 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 };