@sushichan044/kg-core 0.6.1 → 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 +21865 -7994
  3. package/dist/index.mjs +757 -291
  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,78 +1082,81 @@ 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
- })
1064
- };
1065
- const GridLine = {
1066
- schema: readonlyObject({
1067
- range: v.nullable(ManuscriptRange.schema),
1068
- cells: readonlyArray(GridCell.schema)
1069
- }),
1070
- of: (cells) => ({
1071
- cells,
1072
- range: ManuscriptRange.merge(cells.map(({ range }) => range))
1073
- }),
1074
- /**
1075
- * Pads with empty cells so every line in a stage has the same length.
1076
- */
1077
- padded: (cells, charsPerLine) => GridLine.of([...cells, ...Array.from({ length: Math.max(0, charsPerLine - cells.length) }, GridCell.empty)])
1078
- };
1079
- const GridStage = {
1080
- schema: readonlyObject({
1081
- range: v.nullable(ManuscriptRange.schema),
1082
- lines: readonlyArray(GridLine.schema)
1083
- }),
1084
- of: (lines) => ({
1085
- lines,
1086
- range: ManuscriptRange.merge(lines.map(({ range }) => range))
1087
- })
1088
- };
1089
- const GridPage = {
1090
- schema: readonlyObject({
1091
- range: v.nullable(ManuscriptRange.schema),
1092
- stages: readonlyArray(GridStage.schema)
1093
- }),
1094
- of: (stages) => ({
1095
- stages,
1096
- range: ManuscriptRange.merge(stages.map(({ range }) => range))
1097
- })
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;
1098
1160
  };
1099
1161
  //#endregion
1100
1162
  //#region src/composer/manuscript-geometry.ts
@@ -1107,8 +1169,8 @@ const ManuscriptGeometrySchema = readonlyObject({
1107
1169
  cellSizeMm: nonNegativeLength(),
1108
1170
  lineGapMm: nonNegativeLength(),
1109
1171
  fontSizePt: FontSizePt.schema,
1110
- gridWidthMm: nonNegativeLength(),
1111
- gridHeightMm: nonNegativeLength(),
1172
+ contentWidthMm: nonNegativeLength(),
1173
+ contentHeightMm: nonNegativeLength(),
1112
1174
  marginInlineMm: v.pipe(v.number(), v.finite()),
1113
1175
  marginBlockMm: v.pipe(v.number(), v.finite()),
1114
1176
  fitsPaper: v.boolean()
@@ -1116,59 +1178,255 @@ const ManuscriptGeometrySchema = readonlyObject({
1116
1178
  /**
1117
1179
  * Every stage stacked vertically, plus the gap between adjacent stages.
1118
1180
  */
1119
- function gridHeightInCells(grid) {
1120
- return grid.stagesPerPage * grid.charsPerLine + STAGE_GAP_CELLS * (grid.stagesPerPage - 1);
1181
+ function contentHeightInEm(flow) {
1182
+ return flow.stagesPerPage * flow.lineLengthEm + STAGE_GAP_CELLS * (flow.stagesPerPage - 1);
1121
1183
  }
1122
1184
  /**
1123
1185
  * Every vertical line side by side, plus the gap between adjacent lines.
1124
1186
  */
1125
- function gridWidthInCells(grid) {
1126
- return grid.linesPerStage + LINE_GAP_CELLS * (grid.linesPerStage - 1);
1187
+ function contentWidthInEm(flow) {
1188
+ return flow.linesPerStage + LINE_GAP_CELLS * (flow.linesPerStage - 1);
1127
1189
  }
1128
1190
  const ManuscriptGeometry = {
1129
1191
  schema: ManuscriptGeometrySchema,
1130
- of: (grid, appearance) => {
1192
+ of: (flow, appearance) => {
1131
1193
  const paper = PaperSize.of(appearance.paperSize);
1132
1194
  const cellSizeMm = ptToMm(appearance.fontSizePt);
1133
- const gridWidthMm = gridWidthInCells(grid) * cellSizeMm;
1134
- const gridHeightMm = gridHeightInCells(grid) * cellSizeMm;
1135
- const marginInlineMm = (paper.widthMm - gridWidthMm) / 2;
1136
- const marginBlockMm = (paper.heightMm - gridHeightMm) / 2;
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;
1137
1199
  return {
1138
1200
  paperWidthMm: paper.widthMm,
1139
1201
  paperHeightMm: paper.heightMm,
1140
1202
  cellSizeMm,
1141
1203
  lineGapMm: LINE_GAP_CELLS * cellSizeMm,
1142
1204
  fontSizePt: appearance.fontSizePt,
1143
- gridWidthMm,
1144
- gridHeightMm,
1205
+ contentWidthMm,
1206
+ contentHeightMm,
1145
1207
  marginInlineMm,
1146
1208
  marginBlockMm,
1147
1209
  fitsPaper: marginInlineMm >= 0 && marginBlockMm >= 0
1148
1210
  };
1149
1211
  },
1150
1212
  /**
1151
- * The largest step-aligned point size whose grid still fits the paper, clamped to the range.
1213
+ * The largest step-aligned point size whose text area still fits the paper.
1152
1214
  */
1153
- maxFontSizePt: (grid, paperSizeId) => {
1215
+ maxFontSizePt: (flow, paperSizeId) => {
1154
1216
  const paper = PaperSize.of(paperSizeId);
1155
- const maxCellSizeMm = Math.min(paper.widthMm / gridWidthInCells(grid), paper.heightMm / gridHeightInCells(grid));
1217
+ const maxCellSizeMm = Math.min(paper.widthMm / contentWidthInEm(flow), paper.heightMm / contentHeightInEm(flow));
1156
1218
  const steps = Math.floor(mmToPt(maxCellSizeMm) / FontSizePt.range.step + 1e-9);
1157
1219
  const aligned = Math.round(steps * FontSizePt.range.step * 1e3) / 1e3;
1158
1220
  return Math.min(FontSizePt.range.max, Math.max(FontSizePt.range.min, aligned));
1159
1221
  }
1160
1222
  };
1161
- const ManuscriptGridLayout = { schema: readonlyObject({
1162
- pages: readonlyArray(GridPage.schema),
1223
+ //#endregion
1224
+ //#region src/internal/japanese-brackets.ts
1225
+ /**
1226
+ * Brackets and quotes a Japanese paragraph may open with. Halfwidth forms are included because a
1227
+ * manuscript that uses them is still opening a paragraph, not writing prose.
1228
+ */
1229
+ const OPENING_BRACKETS = "「『〖〈《((“\"‘'[[〔{{<<";
1230
+ /**
1231
+ * The closing counterparts, in the same order.
1232
+ */
1233
+ const CLOSING_BRACKETS = "」』〗〉》))”\"’']]〕}}>>";
1234
+ /**
1235
+ * Escapes the characters a regular expression character class would otherwise read as syntax, so a
1236
+ * set declared above can be dropped into a pattern.
1237
+ */
1238
+ function characterClass(characters) {
1239
+ return characters.replaceAll(/[\\\]^-]/gu, "\\$&");
1240
+ }
1241
+ //#endregion
1242
+ //#region src/internal/japanese-spaces.ts
1243
+ const LEADING_SPACES = /^[  \t]+/u;
1244
+ /**
1245
+ * The run of spaces the text begins with, or an empty string. Halfwidth spaces and tabs count: a
1246
+ * rule has to see them to say they are the wrong space.
1247
+ */
1248
+ function leadingSpaces(text) {
1249
+ return LEADING_SPACES.exec(text)?.[0] ?? "";
1250
+ }
1251
+ //#endregion
1252
+ //#region src/internal/question-or-exclamation-spacing.ts
1253
+ /**
1254
+ * Runs, not single marks: `!?` is one mark and takes one gap, so the gap belongs after the run.
1255
+ */
1256
+ const MARKS = /[?!]+/gu;
1257
+ function analyzeMatch(line, marks) {
1258
+ const start = marks.index;
1259
+ if (start === void 0) return void 0;
1260
+ const marksRange = {
1261
+ start,
1262
+ end: start + marks[0].length
1263
+ };
1264
+ const next = line[marksRange.end];
1265
+ if (next === void 0 || "」』〗〉》))”\"’']]〕}}>>".includes(next)) return void 0;
1266
+ const spaces = leadingSpaces(line.slice(marksRange.end));
1267
+ if (spaces === "") return {
1268
+ kind: "missing",
1269
+ marks: marksRange
1270
+ };
1271
+ const gap = {
1272
+ start: marksRange.end,
1273
+ end: marksRange.end + spaces.length
1274
+ };
1275
+ const following = line[gap.end];
1276
+ if (following !== void 0 && "」』〗〉》))”\"’']]〕}}>>".includes(following)) return {
1277
+ kind: "before-closing-bracket",
1278
+ marks: marksRange,
1279
+ gap
1280
+ };
1281
+ if (spaces !== " ") return {
1282
+ kind: "invalid-space",
1283
+ marks: marksRange,
1284
+ gap
1285
+ };
1286
+ return {
1287
+ kind: "valid",
1288
+ marks: marksRange,
1289
+ gap
1290
+ };
1291
+ }
1292
+ /**
1293
+ * Classifies the spacing after each run of fullwidth question or exclamation marks in one display
1294
+ * line. Runs at the end of the line or immediately before a closing bracket need no gap and are
1295
+ * omitted from the result.
1296
+ */
1297
+ function questionOrExclamationSpacings(line) {
1298
+ return [...line.matchAll(MARKS)].map((marks) => analyzeMatch(line, marks)).filter((spacing) => spacing !== void 0);
1299
+ }
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 = {
1340
+ schema: readonlyObject({
1341
+ range: v.nullable(ManuscriptRange.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)
1346
+ }),
1347
+ empty: () => ({
1348
+ range: null,
1349
+ advanceEm: 0,
1350
+ graphemes: [],
1351
+ suppressed: [],
1352
+ annotations: []
1353
+ })
1354
+ };
1355
+ const NovelStage = {
1356
+ schema: readonlyObject({
1357
+ range: v.nullable(ManuscriptRange.schema),
1358
+ lines: readonlyArray(NovelLine.schema)
1359
+ }),
1360
+ of: (lines) => ({
1361
+ lines,
1362
+ range: ManuscriptRange.merge(lines.map(({ range }) => range))
1363
+ })
1364
+ };
1365
+ const NovelPage = {
1366
+ schema: readonlyObject({
1367
+ range: v.nullable(ManuscriptRange.schema),
1368
+ stages: readonlyArray(NovelStage.schema)
1369
+ }),
1370
+ of: (stages) => ({
1371
+ stages,
1372
+ range: ManuscriptRange.merge(stages.map(({ range }) => range))
1373
+ })
1374
+ };
1375
+ const NovelLayout = { schema: readonlyObject({
1376
+ pages: readonlyArray(NovelPage.schema),
1163
1377
  geometry: ManuscriptGeometry.schema,
1164
1378
  stats: CompositionStatistics.schema
1165
1379
  }) };
1166
1380
  //#endregion
1167
- //#region src/composer/grid-composer.ts
1168
- const COMPOSER_ID = NamespacedId.of("kg/grid");
1169
- /**
1170
- * Splits display graphemes on line breaks. A trailing break does not create an empty last line.
1171
- */
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
+ }
1172
1430
  function displayedLines(graphemes) {
1173
1431
  const lines = [[]];
1174
1432
  let endedWithLineBreak = false;
@@ -1178,35 +1436,326 @@ function displayedLines(graphemes) {
1178
1436
  endedWithLineBreak = true;
1179
1437
  continue;
1180
1438
  }
1181
- lines.at(-1).push(grapheme);
1439
+ lines.at(-1)?.push(grapheme);
1182
1440
  endedWithLineBreak = false;
1183
1441
  }
1184
1442
  if (lines.length > 1 && endedWithLineBreak) lines.pop();
1185
1443
  return lines;
1186
1444
  }
1187
- function toCells(sourceLine, annotations) {
1188
- return sourceLine.map((grapheme) => ({
1189
- value: grapheme.value,
1190
- range: grapheme.range,
1191
- annotations: ManuscriptAnnotation.overlapping(annotations, grapheme.range)
1445
+ function validGapIndexes(sourceLine) {
1446
+ const line = sourceLine.map(({ value }) => value).join("");
1447
+ const gapStarts = new Set(questionOrExclamationSpacings(line).filter((spacing) => spacing.kind === "valid").map(({ gap }) => gap.start));
1448
+ const indexes = /* @__PURE__ */ new Set();
1449
+ let offset = 0;
1450
+ for (const [index, grapheme] of sourceLine.entries()) {
1451
+ if (gapStarts.has(offset)) indexes.add(index);
1452
+ offset += grapheme.value.length;
1453
+ }
1454
+ return indexes;
1455
+ }
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
1192
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
+ });
1193
1604
  }
1194
- /**
1195
- * Wraps one source line across as many grid lines as its length requires.
1196
- */
1197
- function wrapLine(cells, charsPerLine) {
1198
- if (cells.length === 0) return [GridLine.padded([], charsPerLine)];
1199
- return Array.from({ length: Math.ceil(cells.length / charsPerLine) }, (_unused, index) => GridLine.padded(cells.slice(index * charsPerLine, (index + 1) * charsPerLine), charsPerLine));
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()];
1682
+ const lines = [];
1683
+ let cursor = 0;
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;
1735
+ }
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
+ }));
1200
1753
  }
1201
- function blankLines(count, charsPerLine) {
1202
- return Array.from({ length: count }, () => GridLine.padded([], charsPerLine));
1754
+ function blankLines(count) {
1755
+ return Array.from({ length: count }, NovelLine.empty);
1203
1756
  }
1204
- /**
1205
- * Walks every line position on every page in reading order, handing out content lines only where
1206
- * the stage and page offsets leave room. Always emits at least one page.
1207
- */
1208
1757
  function buildPages(contentLines, settings) {
1209
- const { charsPerLine, linesPerStage, stagesPerPage } = settings.grid;
1758
+ const { linesPerStage, stagesPerPage } = settings.flow;
1210
1759
  const { stage: stageOffset, page: pageOffset } = settings.offsets;
1211
1760
  const usablePerStage = linesPerStage - LineOffset.total(stageOffset);
1212
1761
  const usablePerPage = usablePerStage * stagesPerPage;
@@ -1222,48 +1771,51 @@ function buildPages(contentLines, settings) {
1222
1771
  const withinPage = usableIndex >= pageOffset.leading && usableIndex < usablePerPage - pageOffset.trailing;
1223
1772
  const next = contentLines[contentIndex];
1224
1773
  if (!withinStage || !withinPage || next === void 0) {
1225
- lines.push(GridLine.padded([], charsPerLine));
1774
+ lines.push(NovelLine.empty());
1226
1775
  continue;
1227
1776
  }
1228
1777
  lines.push(next);
1229
1778
  contentIndex += 1;
1230
1779
  }
1231
- stages.push(GridStage.of(lines));
1780
+ stages.push(NovelStage.of(lines));
1232
1781
  }
1233
- pages.push(GridPage.of(stages));
1782
+ pages.push(NovelPage.of(stages));
1234
1783
  }
1235
1784
  return pages;
1236
1785
  }
1237
- function composeGrid(manuscript, settings) {
1238
- const { charsPerLine } = settings.grid;
1239
- const sourceLines = displayedLines(manuscript.graphemes);
1240
- const cellLines = sourceLines.map((line) => toCells(line, manuscript.annotations));
1241
- const manuscriptLines = cellLines.flatMap((cells) => wrapLine(cells, charsPerLine));
1242
- const pages = buildPages([
1243
- ...blankLines(settings.offsets.document.leading, charsPerLine),
1244
- ...manuscriptLines,
1245
- ...blankLines(settings.offsets.document.trailing, charsPerLine)
1246
- ], settings);
1247
- return ManuscriptResult.succeed({
1248
- pages,
1249
- geometry: ManuscriptGeometry.of(settings.grid, settings.appearance),
1250
- stats: {
1251
- chars: cellLines.reduce((total, cells) => total + cells.length, 0),
1252
- sourceLines: sourceLines.length,
1253
- pages: pages.length
1254
- }
1255
- });
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
+ };
1256
1809
  }
1257
- /**
1258
- * Places text on Japanese manuscript paper: vertical lines grouped into stages, stages into pages.
1259
- * Settings arrive already validated by {@link composeManuscript}, so this never rejects.
1260
- */
1261
- const manuscriptGridComposer = {
1262
- id: COMPOSER_ID,
1263
- settingsSchema: ManuscriptCompositionSettings.schema,
1264
- layoutSchema: ManuscriptGridLayout.schema,
1265
- compose: composeGrid
1266
- };
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 });
1267
1819
  //#endregion
1268
1820
  //#region src/proofreading/invalid-rule-options.ts
1269
1821
  const InvalidRuleOptions = { describe: (error) => `ルール ${error.ruleId} のオプション "${error.option}" が不正です: ${error.issues.map(({ message }) => message).join(", ")}` };
@@ -1544,7 +2096,7 @@ function defineMatchRule(options) {
1544
2096
  }
1545
2097
  //#endregion
1546
2098
  //#region src/proofreading/rules/consistent-kanji-opening.ts
1547
- const RULE_ID$6 = "kg/consistent-kanji-opening";
2099
+ const RULE_ID$5 = "kg/consistent-kanji-opening";
1548
2100
  const MESSAGE = "「{{ closed }}」と「{{ opened }}」が混在しています。文脈上の使い分けか表記ゆれかを確認してください";
1549
2101
  const KanjiOpeningPairSchema = v.pipe(readonlyObject({
1550
2102
  closed: v.pipe(v.string(), v.nonEmpty()),
@@ -1598,7 +2150,7 @@ const DEFAULT_PAIRS = [
1598
2150
  * two spellings are a deliberate distinction is the author's call, so the finding is a warning.
1599
2151
  */
1600
2152
  function build$3(pairs) {
1601
- return defineParsedRule(RULE_ID$6, MESSAGE, (manuscript, report) => {
2153
+ return defineParsedRule(RULE_ID$5, MESSAGE, (manuscript, report) => {
1602
2154
  for (const { closed, opened } of pairs) {
1603
2155
  if (!manuscript.displayText.includes(opened)) continue;
1604
2156
  const index = manuscript.displayText.indexOf(closed);
@@ -1619,7 +2171,7 @@ function createConsistentKanjiOpeningRule(options = {}) {
1619
2171
  const pairs = v.safeParse(PairsSchema, options.pairs);
1620
2172
  if (!pairs.success) return ManuscriptResult.fail({
1621
2173
  kind: "InvalidRuleOptions",
1622
- ruleId: RULE_ID$6,
2174
+ ruleId: RULE_ID$5,
1623
2175
  option: "pairs",
1624
2176
  issues: ValidationIssue.from(pairs.issues)
1625
2177
  });
@@ -1664,7 +2216,7 @@ const consistentNumeralWidthRule = () => defineMixedWidthRule({
1664
2216
  });
1665
2217
  //#endregion
1666
2218
  //#region src/proofreading/rules/dash.ts
1667
- const RULE_ID$5 = "kg/dash";
2219
+ const RULE_ID$4 = "kg/dash";
1668
2220
  const DEFAULT_PREFERRED = "―";
1669
2221
  const DashCharacterSchema = v.picklist([
1670
2222
  "―",
@@ -1686,7 +2238,7 @@ function build$2(preferred) {
1686
2238
  return {
1687
2239
  kind: "parsed",
1688
2240
  meta: {
1689
- id: RULE_ID$5,
2241
+ id: RULE_ID$4,
1690
2242
  messages
1691
2243
  },
1692
2244
  check: (manuscript, context) => {
@@ -1715,7 +2267,7 @@ function createDashRule(options = {}) {
1715
2267
  const preferred = v.safeParse(DashCharacterSchema, options.preferred);
1716
2268
  if (!preferred.success) return ManuscriptResult.fail({
1717
2269
  kind: "InvalidRuleOptions",
1718
- ruleId: RULE_ID$5,
2270
+ ruleId: RULE_ID$4,
1719
2271
  option: "preferred",
1720
2272
  issues: ValidationIssue.from(preferred.issues)
1721
2273
  });
@@ -1742,8 +2294,8 @@ const evenEllipsisRule = () => defineMatchRule({
1742
2294
  });
1743
2295
  //#endregion
1744
2296
  //#region src/proofreading/rules/fullwidth-japanese-punctuation.ts
1745
- const RULE_ID$4 = "kg/fullwidth-japanese-punctuation";
1746
- const MESSAGES$3 = {
2297
+ const RULE_ID$3 = "kg/fullwidth-japanese-punctuation";
2298
+ const MESSAGES$2 = {
1747
2299
  "halfwidth-japanese-punctuation": "半角の「{{ character }}」があります。日本語の句読点・括弧・感嘆符・疑問符は全角にしてください",
1748
2300
  "halfwidth-near-japanese": "日本語の前後に半角の「{{ character }}」があります。全角にすべきか確認してください"
1749
2301
  };
@@ -1788,8 +2340,8 @@ function isEllipsisSubstitute(text, index) {
1788
2340
  const fullwidthJapanesePunctuationRule = () => ({
1789
2341
  kind: "parsed",
1790
2342
  meta: {
1791
- id: RULE_ID$4,
1792
- messages: MESSAGES$3
2343
+ id: RULE_ID$3,
2344
+ messages: MESSAGES$2
1793
2345
  },
1794
2346
  check: (manuscript, context) => {
1795
2347
  for (const line of splitDisplayLines(manuscript.displayText)) for (const { pattern, messageId, severity, requiresJapanese } of CHECKS) for (const match of line.text.matchAll(pattern)) {
@@ -1807,12 +2359,12 @@ const fullwidthJapanesePunctuationRule = () => ({
1807
2359
  });
1808
2360
  //#endregion
1809
2361
  //#region src/proofreading/rules/max-arabic-numeral-digits.ts
1810
- const RULE_ID$3 = "kg/max-arabic-numeral-digits";
2362
+ const RULE_ID$2 = "kg/max-arabic-numeral-digits";
1811
2363
  const DEFAULT_MAX_DIGITS = 2;
1812
2364
  const MaxDigitsSchema = v.pipe(v.number(), v.finite(), v.integer(), v.minValue(1));
1813
2365
  function build$1(maxDigits) {
1814
2366
  return defineMatchRule({
1815
- id: RULE_ID$3,
2367
+ id: RULE_ID$2,
1816
2368
  pattern: /([0-90-9]+)(?:[..]([0-90-9]+))?/gu,
1817
2369
  accept: (match) => (match[1]?.length ?? 0) > maxDigits || (match[2]?.length ?? 0) > maxDigits,
1818
2370
  message: `${maxDigits}桁を超えるアラビア数字が使われています`
@@ -1824,7 +2376,7 @@ function createMaxArabicNumeralDigitsRule(options = {}) {
1824
2376
  const maxDigits = v.safeParse(MaxDigitsSchema, options.maxDigits);
1825
2377
  if (!maxDigits.success) return ManuscriptResult.fail({
1826
2378
  kind: "InvalidRuleOptions",
1827
- ruleId: RULE_ID$3,
2379
+ ruleId: RULE_ID$2,
1828
2380
  option: "maxDigits",
1829
2381
  issues: ValidationIssue.from(maxDigits.issues)
1830
2382
  });
@@ -1852,24 +2404,6 @@ const noConsecutivePunctuationRule = () => defineMatchRule({
1852
2404
  message: "句読点が連続しています"
1853
2405
  });
1854
2406
  //#endregion
1855
- //#region src/proofreading/rules/internal/brackets.ts
1856
- /**
1857
- * Brackets and quotes a Japanese paragraph may open with. Halfwidth forms are included because a
1858
- * manuscript that uses them is still opening a paragraph, not writing prose.
1859
- */
1860
- const OPENING_BRACKETS = "「『〖〈《((“\"‘'[[〔{{<<";
1861
- /**
1862
- * The closing counterparts, in the same order.
1863
- */
1864
- const CLOSING_BRACKETS = "」』〗〉》))”\"’']]〕}}>>";
1865
- /**
1866
- * Escapes the characters a regular expression character class would otherwise read as syntax, so a
1867
- * set declared above can be dropped into a pattern.
1868
- */
1869
- function characterClass(characters) {
1870
- return characters.replaceAll(/[\\\]^-]/gu, "\\$&");
1871
- }
1872
- //#endregion
1873
2407
  //#region src/proofreading/rules/internal/decoration-line.ts
1874
2408
  const DECORATION_LINE = /^[**◆◇■□●○▽▼△▲☆★・…―─━ー\-==~~\s]+$/u;
1875
2409
  /**
@@ -1880,19 +2414,9 @@ function isDecorationLine(text) {
1880
2414
  return DECORATION_LINE.test(text);
1881
2415
  }
1882
2416
  //#endregion
1883
- //#region src/proofreading/rules/internal/spaces.ts
1884
- const LEADING_SPACES = /^[  \t]+/u;
1885
- /**
1886
- * The run of spaces the text begins with, or an empty string. Halfwidth spaces and tabs count: a
1887
- * rule has to see them to say they are the wrong space.
1888
- */
1889
- function leadingSpaces(text) {
1890
- return LEADING_SPACES.exec(text)?.[0] ?? "";
1891
- }
1892
- //#endregion
1893
2417
  //#region src/proofreading/rules/paragraph-opening.ts
1894
- const RULE_ID$2 = "kg/paragraph-opening";
1895
- const MESSAGES$2 = {
2418
+ const RULE_ID$1 = "kg/paragraph-opening";
2419
+ const MESSAGES$1 = {
1896
2420
  "leading-character": "段落の先頭には全角スペースまたは開き括弧が必要です",
1897
2421
  "indent-width": "段落冒頭の字下げは全角スペース1字にしてください",
1898
2422
  "unexpected-indent": "括弧から始まる段落を字下げすることはできません"
@@ -1907,8 +2431,8 @@ function build(openingBrackets) {
1907
2431
  return {
1908
2432
  kind: "parsed",
1909
2433
  meta: {
1910
- id: RULE_ID$2,
1911
- messages: MESSAGES$2
2434
+ id: RULE_ID$1,
2435
+ messages: MESSAGES$1
1912
2436
  },
1913
2437
  check: (manuscript, context) => {
1914
2438
  const report = (start, end, messageId) => {
@@ -1938,7 +2462,7 @@ function createParagraphOpeningRule(options = {}) {
1938
2462
  const brackets = v.safeParse(OpeningBracketsSchema, options.openingBrackets);
1939
2463
  if (!brackets.success) return ManuscriptResult.fail({
1940
2464
  kind: "InvalidRuleOptions",
1941
- ruleId: RULE_ID$2,
2465
+ ruleId: RULE_ID$1,
1942
2466
  option: "openingBrackets",
1943
2467
  issues: ValidationIssue.from(brackets.issues)
1944
2468
  });
@@ -1954,25 +2478,21 @@ const punctuationBeforeClosingQuoteRule = () => defineMatchRule({
1954
2478
  });
1955
2479
  //#endregion
1956
2480
  //#region src/proofreading/rules/space-after-question-or-exclamation.ts
1957
- const RULE_ID$1 = "kg/space-after-question-or-exclamation";
1958
- const MESSAGES$1 = {
2481
+ const RULE_ID = "kg/space-after-question-or-exclamation";
2482
+ const MESSAGES = {
1959
2483
  default: "感嘆符または疑問符の直後には全角スペースか閉じ括弧が必要です",
1960
2484
  "space-width": "感嘆符または疑問符の直後の空白は全角スペース1字にしてください",
1961
2485
  "before-closing-bracket": "閉じ括弧の直前に空白を置くことはできません"
1962
2486
  };
1963
2487
  /**
1964
- * Runs, not single marks: `!?` is one mark and takes one gap, so the gap belongs after the run.
1965
- */
1966
- const MARKS = /[?!]+/gu;
1967
- /**
1968
2488
  * Checks the gap that follows every run of ! or ?. A run at the end of a line or right before a
1969
2489
  * closing bracket needs no gap; anywhere else it takes exactly one ideographic space.
1970
2490
  */
1971
2491
  const spaceAfterQuestionOrExclamationRule = () => ({
1972
2492
  kind: "parsed",
1973
2493
  meta: {
1974
- id: RULE_ID$1,
1975
- messages: MESSAGES$1
2494
+ id: RULE_ID,
2495
+ messages: MESSAGES
1976
2496
  },
1977
2497
  check: (manuscript, context) => {
1978
2498
  const report = (start, end, messageId) => {
@@ -1981,93 +2501,39 @@ const spaceAfterQuestionOrExclamationRule = () => ({
1981
2501
  messageId
1982
2502
  });
1983
2503
  };
1984
- for (const line of splitDisplayLines(manuscript.displayText)) for (const marks of line.text.matchAll(MARKS)) {
1985
- const marksEnd = marks.index + marks[0].length;
1986
- const next = line.text[marksEnd];
1987
- if (next === void 0 || "」』〗〉》))”\"’']]〕}}>>".includes(next)) continue;
1988
- const spaces = leadingSpaces(line.text.slice(marksEnd));
1989
- if (spaces === "") {
1990
- report(line.start + marks.index, line.start + marksEnd, "default");
1991
- continue;
1992
- }
1993
- const following = line.text[marksEnd + spaces.length];
1994
- const spacesStart = line.start + marksEnd;
1995
- const spacesEnd = spacesStart + spaces.length;
1996
- if (following !== void 0 && "」』〗〉》))”\"’']]〕}}>>".includes(following)) report(spacesStart, spacesEnd, "before-closing-bracket");
1997
- else if (spaces !== " ") report(spacesStart, spacesEnd, "space-width");
2504
+ for (const line of splitDisplayLines(manuscript.displayText)) for (const spacing of questionOrExclamationSpacings(line.text)) switch (spacing.kind) {
2505
+ case "valid": break;
2506
+ case "missing":
2507
+ report(line.start + spacing.marks.start, line.start + spacing.marks.end, "default");
2508
+ break;
2509
+ case "invalid-space":
2510
+ report(line.start + spacing.gap.start, line.start + spacing.gap.end, "space-width");
2511
+ break;
2512
+ case "before-closing-bracket":
2513
+ report(line.start + spacing.gap.start, line.start + spacing.gap.end, "before-closing-bracket");
2514
+ break;
2515
+ default: assertNever(spacing);
1998
2516
  }
1999
2517
  }
2000
2518
  });
2001
2519
  //#endregion
2002
2520
  //#region src/proofreading/rules/variant-character.ts
2003
- const RULE_ID = "kg/variant-character";
2004
- const MESSAGES = {
2005
- default: "異体字または字形選択子が使われています",
2006
- "with-suggestion": "異体字または字形選択子が使われています。「{{ suggestion }}」ではありませんか?"
2007
- };
2008
2521
  /**
2009
- * Mongolian free variation selectors, variation selectors (incl. emoji presentation), IVSes: all
2010
- * combine with a preceding base character rather than standing on their own.
2522
+ * Mongolian free variation selectors, variation selectors, CJK compatibility ideographs, IVSes.
2011
2523
  */
2012
- function isSelector(codePoint) {
2013
- return codePoint >= 6155 && codePoint <= 6157 || codePoint === 6159 || codePoint >= 65024 && codePoint <= 65039 || codePoint >= 917760 && codePoint <= 917999;
2014
- }
2015
- /**
2016
- * CJK compatibility ideographs stand alone; each has a canonical unified form reachable via NFKC.
2017
- */
2018
- function isCompatibilityIdeograph(codePoint) {
2019
- return codePoint >= 63744 && codePoint <= 64255 || codePoint >= 194560 && codePoint <= 195103;
2020
- }
2021
2524
  function isVariantCharacter(character) {
2022
2525
  const codePoint = character.codePointAt(0);
2023
2526
  if (codePoint === void 0) return false;
2024
- return isSelector(codePoint) || isCompatibilityIdeograph(codePoint);
2025
- }
2026
- /**
2027
- * The plain form a reader would expect: the selector stripped away, or the compatibility
2028
- * ideograph's canonical unified form. Undefined when that plain form is identical to the original,
2029
- * since NFKC does not touch selectors and a suggestion equal to the input helps no one.
2030
- */
2031
- function suggestPlainForm(segment) {
2032
- const withoutSelectors = Array.from(segment).filter((character) => {
2033
- const codePoint = character.codePointAt(0);
2034
- return codePoint !== void 0 && !isSelector(codePoint);
2035
- }).join("");
2036
- if (withoutSelectors === "") return void 0;
2037
- const normalized = Array.from(withoutSelectors).map((character) => {
2038
- const codePoint = character.codePointAt(0);
2039
- return codePoint !== void 0 && isCompatibilityIdeograph(codePoint) ? character.normalize("NFKC") : character;
2040
- }).join("");
2041
- return normalized === segment ? void 0 : normalized;
2527
+ return codePoint >= 6155 && codePoint <= 6157 || codePoint === 6159 || codePoint >= 65024 && codePoint <= 65039 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 194560 && codePoint <= 195103 || codePoint >= 917760 && codePoint <= 917999;
2042
2528
  }
2043
2529
  /**
2044
2530
  * Scans the source rather than the display text: notation removal must not hide a variant.
2045
2531
  */
2046
- const variantCharacterRule = () => ({
2047
- kind: "parsed",
2048
- meta: {
2049
- id: RULE_ID,
2050
- messages: MESSAGES
2051
- },
2052
- check: (manuscript, context) => {
2053
- for (const { index, segment } of graphemeSegmenter.segment(manuscript.source)) {
2054
- if (!Array.from(segment).some(isVariantCharacter)) continue;
2055
- const range = sourceRange(manuscript, {
2056
- start: index,
2057
- end: index + segment.length
2058
- });
2059
- const suggestion = suggestPlainForm(segment);
2060
- if (suggestion === void 0) context.report({
2061
- range,
2062
- messageId: "default"
2063
- });
2064
- else context.report({
2065
- range,
2066
- messageId: "with-suggestion",
2067
- data: { suggestion }
2068
- });
2069
- }
2070
- }
2532
+ const variantCharacterRule = () => defineParsedRule("kg/variant-character", "異体字または字形選択子が使われています", (manuscript, report) => {
2533
+ for (const { index, segment } of graphemeSegmenter.segment(manuscript.source)) if (Array.from(segment).some(isVariantCharacter)) report(sourceRange(manuscript, {
2534
+ start: index,
2535
+ end: index + segment.length
2536
+ }));
2071
2537
  });
2072
2538
  //#endregion
2073
2539
  //#region src/proofreading/rules/default-rules.ts
@@ -2095,4 +2561,4 @@ function createDefaultProofreadingRules() {
2095
2561
  ];
2096
2562
  }
2097
2563
  //#endregion
2098
- 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 };