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