@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.
- package/README.md +44 -10
- package/dist/index.d.mts +20761 -6890
- package/dist/index.mjs +654 -198
- 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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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/
|
|
961
|
-
const
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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: (
|
|
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: (
|
|
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
|
|
1035
|
-
|
|
1123
|
+
const NovelCompositionSettingsSchema = v.pipe(readonlyObject({
|
|
1124
|
+
flow: NovelFlowSettings.schema,
|
|
1036
1125
|
offsets: ManuscriptOffsets.schema,
|
|
1037
1126
|
appearance: ManuscriptAppearanceSettings.schema
|
|
1038
|
-
}), v.check(({
|
|
1039
|
-
const
|
|
1040
|
-
schema:
|
|
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
|
-
|
|
1131
|
+
flow: NovelFlowSettings.defaults,
|
|
1043
1132
|
offsets: ManuscriptOffsets.defaults,
|
|
1044
1133
|
appearance: ManuscriptAppearanceSettings.defaults
|
|
1045
1134
|
},
|
|
1046
|
-
is: (value) => v.is(
|
|
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
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
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
|
|
1355
|
+
const NovelStage = {
|
|
1157
1356
|
schema: readonlyObject({
|
|
1158
1357
|
range: v.nullable(ManuscriptRange.schema),
|
|
1159
|
-
lines: readonlyArray(
|
|
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
|
|
1365
|
+
const NovelPage = {
|
|
1167
1366
|
schema: readonlyObject({
|
|
1168
1367
|
range: v.nullable(ManuscriptRange.schema),
|
|
1169
|
-
stages: readonlyArray(
|
|
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
|
-
|
|
1177
|
-
|
|
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/
|
|
1245
|
-
const COMPOSER_ID = NamespacedId.of("kg/
|
|
1246
|
-
|
|
1247
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
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 <
|
|
1291
|
-
|
|
1292
|
-
if (
|
|
1293
|
-
|
|
1294
|
-
|
|
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
|
-
|
|
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
|
|
1299
|
-
return Array.from({ length: count },
|
|
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 {
|
|
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(
|
|
1774
|
+
lines.push(NovelLine.empty());
|
|
1323
1775
|
continue;
|
|
1324
1776
|
}
|
|
1325
1777
|
lines.push(next);
|
|
1326
1778
|
contentIndex += 1;
|
|
1327
1779
|
}
|
|
1328
|
-
stages.push(
|
|
1780
|
+
stages.push(NovelStage.of(lines));
|
|
1329
1781
|
}
|
|
1330
|
-
pages.push(
|
|
1782
|
+
pages.push(NovelPage.of(stages));
|
|
1331
1783
|
}
|
|
1332
1784
|
return pages;
|
|
1333
1785
|
}
|
|
1334
|
-
function
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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,
|
|
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 };
|