@sushichan044/kg-core 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -10
- package/dist/index.d.mts +21865 -7994
- package/dist/index.mjs +757 -291
- 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,78 +1082,81 @@ const ManuscriptOffsets = {
|
|
|
1023
1082
|
/**
|
|
1024
1083
|
* Stage offsets must leave at least one usable line in the stage.
|
|
1025
1084
|
*/
|
|
1026
|
-
maxStageTotal: (
|
|
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
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
cells,
|
|
1072
|
-
range: ManuscriptRange.merge(cells.map(({ range }) => range))
|
|
1073
|
-
}),
|
|
1074
|
-
/**
|
|
1075
|
-
* Pads with empty cells so every line in a stage has the same length.
|
|
1076
|
-
*/
|
|
1077
|
-
padded: (cells, charsPerLine) => GridLine.of([...cells, ...Array.from({ length: Math.max(0, charsPerLine - cells.length) }, GridCell.empty)])
|
|
1078
|
-
};
|
|
1079
|
-
const GridStage = {
|
|
1080
|
-
schema: readonlyObject({
|
|
1081
|
-
range: v.nullable(ManuscriptRange.schema),
|
|
1082
|
-
lines: readonlyArray(GridLine.schema)
|
|
1083
|
-
}),
|
|
1084
|
-
of: (lines) => ({
|
|
1085
|
-
lines,
|
|
1086
|
-
range: ManuscriptRange.merge(lines.map(({ range }) => range))
|
|
1087
|
-
})
|
|
1088
|
-
};
|
|
1089
|
-
const GridPage = {
|
|
1090
|
-
schema: readonlyObject({
|
|
1091
|
-
range: v.nullable(ManuscriptRange.schema),
|
|
1092
|
-
stages: readonlyArray(GridStage.schema)
|
|
1093
|
-
}),
|
|
1094
|
-
of: (stages) => ({
|
|
1095
|
-
stages,
|
|
1096
|
-
range: ManuscriptRange.merge(stages.map(({ range }) => range))
|
|
1097
|
-
})
|
|
1142
|
+
//#endregion
|
|
1143
|
+
//#region src/composer/inline-measurer.ts
|
|
1144
|
+
function graphemeWidth(grapheme) {
|
|
1145
|
+
let width = 1;
|
|
1146
|
+
for (const character of grapheme) {
|
|
1147
|
+
const codePoint = character.codePointAt(0);
|
|
1148
|
+
if (codePoint !== void 0) width = Math.max(width, eastAsianWidth(codePoint, { ambiguousAsWide: true }));
|
|
1149
|
+
}
|
|
1150
|
+
return width / 2;
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Deterministic Japanese-context measurement in logical em units.
|
|
1154
|
+
*/
|
|
1155
|
+
const logicalInlineMeasurer = (request) => {
|
|
1156
|
+
const { text } = request;
|
|
1157
|
+
const width = [...graphemeSegmenter.segment(text)].reduce((total, { segment }) => total + graphemeWidth(segment), 0);
|
|
1158
|
+
if (request.role === "ruby") return width / 2;
|
|
1159
|
+
return request.presentation === "upright" ? [...graphemeSegmenter.segment(text)].length : width;
|
|
1098
1160
|
};
|
|
1099
1161
|
//#endregion
|
|
1100
1162
|
//#region src/composer/manuscript-geometry.ts
|
|
@@ -1107,8 +1169,8 @@ const ManuscriptGeometrySchema = readonlyObject({
|
|
|
1107
1169
|
cellSizeMm: nonNegativeLength(),
|
|
1108
1170
|
lineGapMm: nonNegativeLength(),
|
|
1109
1171
|
fontSizePt: FontSizePt.schema,
|
|
1110
|
-
|
|
1111
|
-
|
|
1172
|
+
contentWidthMm: nonNegativeLength(),
|
|
1173
|
+
contentHeightMm: nonNegativeLength(),
|
|
1112
1174
|
marginInlineMm: v.pipe(v.number(), v.finite()),
|
|
1113
1175
|
marginBlockMm: v.pipe(v.number(), v.finite()),
|
|
1114
1176
|
fitsPaper: v.boolean()
|
|
@@ -1116,59 +1178,255 @@ const ManuscriptGeometrySchema = readonlyObject({
|
|
|
1116
1178
|
/**
|
|
1117
1179
|
* Every stage stacked vertically, plus the gap between adjacent stages.
|
|
1118
1180
|
*/
|
|
1119
|
-
function
|
|
1120
|
-
return
|
|
1181
|
+
function contentHeightInEm(flow) {
|
|
1182
|
+
return flow.stagesPerPage * flow.lineLengthEm + STAGE_GAP_CELLS * (flow.stagesPerPage - 1);
|
|
1121
1183
|
}
|
|
1122
1184
|
/**
|
|
1123
1185
|
* Every vertical line side by side, plus the gap between adjacent lines.
|
|
1124
1186
|
*/
|
|
1125
|
-
function
|
|
1126
|
-
return
|
|
1187
|
+
function contentWidthInEm(flow) {
|
|
1188
|
+
return flow.linesPerStage + LINE_GAP_CELLS * (flow.linesPerStage - 1);
|
|
1127
1189
|
}
|
|
1128
1190
|
const ManuscriptGeometry = {
|
|
1129
1191
|
schema: ManuscriptGeometrySchema,
|
|
1130
|
-
of: (
|
|
1192
|
+
of: (flow, appearance) => {
|
|
1131
1193
|
const paper = PaperSize.of(appearance.paperSize);
|
|
1132
1194
|
const cellSizeMm = ptToMm(appearance.fontSizePt);
|
|
1133
|
-
const
|
|
1134
|
-
const
|
|
1135
|
-
const marginInlineMm = (paper.widthMm -
|
|
1136
|
-
const marginBlockMm = (paper.heightMm -
|
|
1195
|
+
const contentWidthMm = contentWidthInEm(flow) * cellSizeMm;
|
|
1196
|
+
const contentHeightMm = contentHeightInEm(flow) * cellSizeMm;
|
|
1197
|
+
const marginInlineMm = (paper.widthMm - contentWidthMm) / 2;
|
|
1198
|
+
const marginBlockMm = (paper.heightMm - contentHeightMm) / 2;
|
|
1137
1199
|
return {
|
|
1138
1200
|
paperWidthMm: paper.widthMm,
|
|
1139
1201
|
paperHeightMm: paper.heightMm,
|
|
1140
1202
|
cellSizeMm,
|
|
1141
1203
|
lineGapMm: LINE_GAP_CELLS * cellSizeMm,
|
|
1142
1204
|
fontSizePt: appearance.fontSizePt,
|
|
1143
|
-
|
|
1144
|
-
|
|
1205
|
+
contentWidthMm,
|
|
1206
|
+
contentHeightMm,
|
|
1145
1207
|
marginInlineMm,
|
|
1146
1208
|
marginBlockMm,
|
|
1147
1209
|
fitsPaper: marginInlineMm >= 0 && marginBlockMm >= 0
|
|
1148
1210
|
};
|
|
1149
1211
|
},
|
|
1150
1212
|
/**
|
|
1151
|
-
* The largest step-aligned point size whose
|
|
1213
|
+
* The largest step-aligned point size whose text area still fits the paper.
|
|
1152
1214
|
*/
|
|
1153
|
-
maxFontSizePt: (
|
|
1215
|
+
maxFontSizePt: (flow, paperSizeId) => {
|
|
1154
1216
|
const paper = PaperSize.of(paperSizeId);
|
|
1155
|
-
const maxCellSizeMm = Math.min(paper.widthMm /
|
|
1217
|
+
const maxCellSizeMm = Math.min(paper.widthMm / contentWidthInEm(flow), paper.heightMm / contentHeightInEm(flow));
|
|
1156
1218
|
const steps = Math.floor(mmToPt(maxCellSizeMm) / FontSizePt.range.step + 1e-9);
|
|
1157
1219
|
const aligned = Math.round(steps * FontSizePt.range.step * 1e3) / 1e3;
|
|
1158
1220
|
return Math.min(FontSizePt.range.max, Math.max(FontSizePt.range.min, aligned));
|
|
1159
1221
|
}
|
|
1160
1222
|
};
|
|
1161
|
-
|
|
1162
|
-
|
|
1223
|
+
//#endregion
|
|
1224
|
+
//#region src/internal/japanese-brackets.ts
|
|
1225
|
+
/**
|
|
1226
|
+
* Brackets and quotes a Japanese paragraph may open with. Halfwidth forms are included because a
|
|
1227
|
+
* manuscript that uses them is still opening a paragraph, not writing prose.
|
|
1228
|
+
*/
|
|
1229
|
+
const OPENING_BRACKETS = "「『〖〈《((“\"‘'[[〔{{<<";
|
|
1230
|
+
/**
|
|
1231
|
+
* The closing counterparts, in the same order.
|
|
1232
|
+
*/
|
|
1233
|
+
const CLOSING_BRACKETS = "」』〗〉》))”\"’']]〕}}>>";
|
|
1234
|
+
/**
|
|
1235
|
+
* Escapes the characters a regular expression character class would otherwise read as syntax, so a
|
|
1236
|
+
* set declared above can be dropped into a pattern.
|
|
1237
|
+
*/
|
|
1238
|
+
function characterClass(characters) {
|
|
1239
|
+
return characters.replaceAll(/[\\\]^-]/gu, "\\$&");
|
|
1240
|
+
}
|
|
1241
|
+
//#endregion
|
|
1242
|
+
//#region src/internal/japanese-spaces.ts
|
|
1243
|
+
const LEADING_SPACES = /^[ \t]+/u;
|
|
1244
|
+
/**
|
|
1245
|
+
* The run of spaces the text begins with, or an empty string. Halfwidth spaces and tabs count: a
|
|
1246
|
+
* rule has to see them to say they are the wrong space.
|
|
1247
|
+
*/
|
|
1248
|
+
function leadingSpaces(text) {
|
|
1249
|
+
return LEADING_SPACES.exec(text)?.[0] ?? "";
|
|
1250
|
+
}
|
|
1251
|
+
//#endregion
|
|
1252
|
+
//#region src/internal/question-or-exclamation-spacing.ts
|
|
1253
|
+
/**
|
|
1254
|
+
* Runs, not single marks: `!?` is one mark and takes one gap, so the gap belongs after the run.
|
|
1255
|
+
*/
|
|
1256
|
+
const MARKS = /[?!]+/gu;
|
|
1257
|
+
function analyzeMatch(line, marks) {
|
|
1258
|
+
const start = marks.index;
|
|
1259
|
+
if (start === void 0) return void 0;
|
|
1260
|
+
const marksRange = {
|
|
1261
|
+
start,
|
|
1262
|
+
end: start + marks[0].length
|
|
1263
|
+
};
|
|
1264
|
+
const next = line[marksRange.end];
|
|
1265
|
+
if (next === void 0 || "」』〗〉》))”\"’']]〕}}>>".includes(next)) return void 0;
|
|
1266
|
+
const spaces = leadingSpaces(line.slice(marksRange.end));
|
|
1267
|
+
if (spaces === "") return {
|
|
1268
|
+
kind: "missing",
|
|
1269
|
+
marks: marksRange
|
|
1270
|
+
};
|
|
1271
|
+
const gap = {
|
|
1272
|
+
start: marksRange.end,
|
|
1273
|
+
end: marksRange.end + spaces.length
|
|
1274
|
+
};
|
|
1275
|
+
const following = line[gap.end];
|
|
1276
|
+
if (following !== void 0 && "」』〗〉》))”\"’']]〕}}>>".includes(following)) return {
|
|
1277
|
+
kind: "before-closing-bracket",
|
|
1278
|
+
marks: marksRange,
|
|
1279
|
+
gap
|
|
1280
|
+
};
|
|
1281
|
+
if (spaces !== " ") return {
|
|
1282
|
+
kind: "invalid-space",
|
|
1283
|
+
marks: marksRange,
|
|
1284
|
+
gap
|
|
1285
|
+
};
|
|
1286
|
+
return {
|
|
1287
|
+
kind: "valid",
|
|
1288
|
+
marks: marksRange,
|
|
1289
|
+
gap
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Classifies the spacing after each run of fullwidth question or exclamation marks in one display
|
|
1294
|
+
* line. Runs at the end of the line or immediately before a closing bracket need no gap and are
|
|
1295
|
+
* omitted from the result.
|
|
1296
|
+
*/
|
|
1297
|
+
function questionOrExclamationSpacings(line) {
|
|
1298
|
+
return [...line.matchAll(MARKS)].map((marks) => analyzeMatch(line, marks)).filter((spacing) => spacing !== void 0);
|
|
1299
|
+
}
|
|
1300
|
+
//#endregion
|
|
1301
|
+
//#region src/composer/vertical-text-presentation.ts
|
|
1302
|
+
const common = { groupRange: ManuscriptRange.schema };
|
|
1303
|
+
const VerticalTextPresentation = { schema: v.variant("kind", [
|
|
1304
|
+
readonlyObject({
|
|
1305
|
+
kind: v.literal("mixed"),
|
|
1306
|
+
...common
|
|
1307
|
+
}),
|
|
1308
|
+
readonlyObject({
|
|
1309
|
+
kind: v.literal("upright"),
|
|
1310
|
+
...common
|
|
1311
|
+
}),
|
|
1312
|
+
readonlyObject({
|
|
1313
|
+
kind: v.literal("sideways"),
|
|
1314
|
+
...common
|
|
1315
|
+
}),
|
|
1316
|
+
readonlyObject({
|
|
1317
|
+
kind: v.literal("tate-chu-yoko"),
|
|
1318
|
+
...common
|
|
1319
|
+
})
|
|
1320
|
+
]) };
|
|
1321
|
+
//#endregion
|
|
1322
|
+
//#region src/composer/positioned-grapheme.ts
|
|
1323
|
+
const emLength = () => v.pipe(v.number(), v.finite(), v.minValue(0));
|
|
1324
|
+
const PositionedGrapheme = { schema: readonlyObject({
|
|
1325
|
+
kind: v.literal("grapheme"),
|
|
1326
|
+
value: v.string(),
|
|
1327
|
+
range: ManuscriptRange.schema,
|
|
1328
|
+
offsetEm: emLength(),
|
|
1329
|
+
advanceEm: emLength(),
|
|
1330
|
+
disposition: v.union([v.literal("placed"), v.literal("hanging")]),
|
|
1331
|
+
presentation: VerticalTextPresentation.schema
|
|
1332
|
+
}) };
|
|
1333
|
+
const SuppressedGrapheme = { schema: readonlyObject({
|
|
1334
|
+
kind: v.literal("suppressed"),
|
|
1335
|
+
value: v.string(),
|
|
1336
|
+
range: ManuscriptRange.schema,
|
|
1337
|
+
reason: v.literal("question-or-exclamation-gap")
|
|
1338
|
+
}) };
|
|
1339
|
+
const NovelLine = {
|
|
1340
|
+
schema: readonlyObject({
|
|
1341
|
+
range: v.nullable(ManuscriptRange.schema),
|
|
1342
|
+
advanceEm: v.pipe(v.number(), v.finite(), v.minValue(0)),
|
|
1343
|
+
graphemes: readonlyArray(PositionedGrapheme.schema),
|
|
1344
|
+
suppressed: readonlyArray(SuppressedGrapheme.schema),
|
|
1345
|
+
annotations: readonlyArray(ComposedAnnotationFragment.schema)
|
|
1346
|
+
}),
|
|
1347
|
+
empty: () => ({
|
|
1348
|
+
range: null,
|
|
1349
|
+
advanceEm: 0,
|
|
1350
|
+
graphemes: [],
|
|
1351
|
+
suppressed: [],
|
|
1352
|
+
annotations: []
|
|
1353
|
+
})
|
|
1354
|
+
};
|
|
1355
|
+
const NovelStage = {
|
|
1356
|
+
schema: readonlyObject({
|
|
1357
|
+
range: v.nullable(ManuscriptRange.schema),
|
|
1358
|
+
lines: readonlyArray(NovelLine.schema)
|
|
1359
|
+
}),
|
|
1360
|
+
of: (lines) => ({
|
|
1361
|
+
lines,
|
|
1362
|
+
range: ManuscriptRange.merge(lines.map(({ range }) => range))
|
|
1363
|
+
})
|
|
1364
|
+
};
|
|
1365
|
+
const NovelPage = {
|
|
1366
|
+
schema: readonlyObject({
|
|
1367
|
+
range: v.nullable(ManuscriptRange.schema),
|
|
1368
|
+
stages: readonlyArray(NovelStage.schema)
|
|
1369
|
+
}),
|
|
1370
|
+
of: (stages) => ({
|
|
1371
|
+
stages,
|
|
1372
|
+
range: ManuscriptRange.merge(stages.map(({ range }) => range))
|
|
1373
|
+
})
|
|
1374
|
+
};
|
|
1375
|
+
const NovelLayout = { schema: readonlyObject({
|
|
1376
|
+
pages: readonlyArray(NovelPage.schema),
|
|
1163
1377
|
geometry: ManuscriptGeometry.schema,
|
|
1164
1378
|
stats: CompositionStatistics.schema
|
|
1165
1379
|
}) };
|
|
1166
1380
|
//#endregion
|
|
1167
|
-
//#region src/composer/
|
|
1168
|
-
const COMPOSER_ID = NamespacedId.of("kg/
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
*/
|
|
1381
|
+
//#region src/composer/novel-composer.ts
|
|
1382
|
+
const COMPOSER_ID = NamespacedId.of("kg/novel");
|
|
1383
|
+
const LINE_START_PROHIBITED = /* @__PURE__ */ new Set(`、。,.・:;?!‼⁇⁈⁉ヽヾゝゞ々ーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖ${CLOSING_BRACKETS}】〙〟⦆»`);
|
|
1384
|
+
const LINE_END_PROHIBITED = /* @__PURE__ */ new Set(`${OPENING_BRACKETS}【〘〝⦅«`);
|
|
1385
|
+
const HANGING_PUNCTUATION = /* @__PURE__ */ new Set("、。,.!?");
|
|
1386
|
+
const INSEPARABLE_PAIRS = /* @__PURE__ */ new Set([
|
|
1387
|
+
"……",
|
|
1388
|
+
"――",
|
|
1389
|
+
"──",
|
|
1390
|
+
"〳〵"
|
|
1391
|
+
]);
|
|
1392
|
+
const ASCII_ALPHANUMERIC = /^[A-Za-z0-9]$/u;
|
|
1393
|
+
const ASCII_TWO_DIGITS = /^[0-9]{2}$/u;
|
|
1394
|
+
const UPRIGHT_LATIN_ABBREVIATION = /^(?:[A-Z]+|[A-Z][a-z]{1,2})$/u;
|
|
1395
|
+
const FULLWIDTH_ALPHANUMERIC = /^[A-Za-z0-9]$/u;
|
|
1396
|
+
function presentation(kind, graphemes) {
|
|
1397
|
+
const groupRange = ManuscriptRange.merge(graphemes.map(({ range }) => range));
|
|
1398
|
+
return groupRange === null ? void 0 : {
|
|
1399
|
+
kind,
|
|
1400
|
+
groupRange
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
function presentedSourceLine(sourceLine) {
|
|
1404
|
+
const presented = [];
|
|
1405
|
+
let cursor = 0;
|
|
1406
|
+
while (cursor < sourceLine.length) {
|
|
1407
|
+
const first = sourceLine[cursor];
|
|
1408
|
+
if (first === void 0) break;
|
|
1409
|
+
if (!ASCII_ALPHANUMERIC.test(first.value)) {
|
|
1410
|
+
const itemPresentation = presentation(FULLWIDTH_ALPHANUMERIC.test(first.value) ? "upright" : "mixed", [first]);
|
|
1411
|
+
if (itemPresentation !== void 0) presented.push({
|
|
1412
|
+
grapheme: first,
|
|
1413
|
+
presentation: itemPresentation
|
|
1414
|
+
});
|
|
1415
|
+
cursor += 1;
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
const start = cursor;
|
|
1419
|
+
while (ASCII_ALPHANUMERIC.test(sourceLine[cursor]?.value ?? "")) cursor += 1;
|
|
1420
|
+
const run = sourceLine.slice(start, cursor);
|
|
1421
|
+
const text = run.map(({ value }) => value).join("");
|
|
1422
|
+
const runPresentation = presentation(ASCII_TWO_DIGITS.test(text) ? "tate-chu-yoko" : text.length === 1 || UPRIGHT_LATIN_ABBREVIATION.test(text) ? "upright" : "sideways", run);
|
|
1423
|
+
if (runPresentation !== void 0) presented.push(...run.map((grapheme) => ({
|
|
1424
|
+
grapheme,
|
|
1425
|
+
presentation: runPresentation
|
|
1426
|
+
})));
|
|
1427
|
+
}
|
|
1428
|
+
return presented;
|
|
1429
|
+
}
|
|
1172
1430
|
function displayedLines(graphemes) {
|
|
1173
1431
|
const lines = [[]];
|
|
1174
1432
|
let endedWithLineBreak = false;
|
|
@@ -1178,35 +1436,326 @@ function displayedLines(graphemes) {
|
|
|
1178
1436
|
endedWithLineBreak = true;
|
|
1179
1437
|
continue;
|
|
1180
1438
|
}
|
|
1181
|
-
lines.at(-1)
|
|
1439
|
+
lines.at(-1)?.push(grapheme);
|
|
1182
1440
|
endedWithLineBreak = false;
|
|
1183
1441
|
}
|
|
1184
1442
|
if (lines.length > 1 && endedWithLineBreak) lines.pop();
|
|
1185
1443
|
return lines;
|
|
1186
1444
|
}
|
|
1187
|
-
function
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1445
|
+
function validGapIndexes(sourceLine) {
|
|
1446
|
+
const line = sourceLine.map(({ value }) => value).join("");
|
|
1447
|
+
const gapStarts = new Set(questionOrExclamationSpacings(line).filter((spacing) => spacing.kind === "valid").map(({ gap }) => gap.start));
|
|
1448
|
+
const indexes = /* @__PURE__ */ new Set();
|
|
1449
|
+
let offset = 0;
|
|
1450
|
+
for (const [index, grapheme] of sourceLine.entries()) {
|
|
1451
|
+
if (gapStarts.has(offset)) indexes.add(index);
|
|
1452
|
+
offset += grapheme.value.length;
|
|
1453
|
+
}
|
|
1454
|
+
return indexes;
|
|
1455
|
+
}
|
|
1456
|
+
function indicesInside(atoms, annotation) {
|
|
1457
|
+
return atoms.flatMap(({ grapheme }, index) => ManuscriptRange.overlaps(grapheme.range, annotation.range) ? [index] : []);
|
|
1458
|
+
}
|
|
1459
|
+
function measure(measurer, text, role, settings, presentationKind) {
|
|
1460
|
+
const context = {
|
|
1461
|
+
text,
|
|
1462
|
+
fontPreset: settings.appearance.fontPreset,
|
|
1463
|
+
writingMode: "vertical-rl"
|
|
1464
|
+
};
|
|
1465
|
+
const value = role === "base" ? measurer({
|
|
1466
|
+
...context,
|
|
1467
|
+
role,
|
|
1468
|
+
presentation: presentationKind ?? "mixed"
|
|
1469
|
+
}) : measurer({
|
|
1470
|
+
...context,
|
|
1471
|
+
role
|
|
1472
|
+
});
|
|
1473
|
+
return Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
1474
|
+
}
|
|
1475
|
+
function measureSourceLine(sourceLine, annotations, settings, measurer) {
|
|
1476
|
+
const mutable = presentedSourceLine(sourceLine).map(({ grapheme, presentation }) => {
|
|
1477
|
+
const advanceEm = measure(measurer, grapheme.value, "base", settings, presentation.kind);
|
|
1478
|
+
return advanceEm === void 0 ? void 0 : {
|
|
1479
|
+
grapheme,
|
|
1480
|
+
advanceEm,
|
|
1481
|
+
presentation
|
|
1482
|
+
};
|
|
1483
|
+
});
|
|
1484
|
+
if (mutable.some((atom) => atom === void 0)) return void 0;
|
|
1485
|
+
const atoms = mutable.flatMap((atom) => atom === void 0 ? [] : [atom]);
|
|
1486
|
+
for (const annotation of annotations) {
|
|
1487
|
+
if (annotation.kind !== "ruby") continue;
|
|
1488
|
+
const indexes = indicesInside(atoms, annotation);
|
|
1489
|
+
if (indexes.length === 0) continue;
|
|
1490
|
+
if ((annotation.reading.kind === "group" ? [annotation.reading.text] : annotation.reading.segments).some((text) => [...graphemeSegmenter.segment(text)].some(({ segment }) => measure(measurer, segment, "ruby", settings) === void 0))) return void 0;
|
|
1491
|
+
if (annotation.reading.kind === "group") {
|
|
1492
|
+
const readingAdvance = measure(measurer, annotation.reading.text, "ruby", settings);
|
|
1493
|
+
if (readingAdvance === void 0) return void 0;
|
|
1494
|
+
const baseAdvance = indexes.reduce((total, index) => total + (atoms[index]?.advanceEm ?? 0), 0);
|
|
1495
|
+
const extra = Math.max(0, readingAdvance - baseAdvance) / indexes.length;
|
|
1496
|
+
for (const index of indexes) {
|
|
1497
|
+
const atom = atoms[index];
|
|
1498
|
+
if (atom !== void 0) atom.advanceEm += extra;
|
|
1499
|
+
}
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
for (const [segmentIndex, index] of indexes.entries()) {
|
|
1503
|
+
const segment = annotation.reading.segments[segmentIndex];
|
|
1504
|
+
const atom = atoms[index];
|
|
1505
|
+
if (segment === void 0 || atom === void 0) continue;
|
|
1506
|
+
const readingAdvance = measure(measurer, segment, "ruby", settings);
|
|
1507
|
+
if (readingAdvance === void 0) return void 0;
|
|
1508
|
+
atom.advanceEm = Math.max(atom.advanceEm, readingAdvance);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
return {
|
|
1512
|
+
atoms,
|
|
1513
|
+
suppressedIndexes: validGapIndexes(sourceLine)
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
function breakInsideFittableGroupRuby(atoms, annotations, boundary, lineLengthEm) {
|
|
1517
|
+
const right = atoms[boundary];
|
|
1518
|
+
if (right === void 0) return false;
|
|
1519
|
+
const graphemeIndex = right.grapheme.range.graphemes.start;
|
|
1520
|
+
return annotations.some((annotation) => {
|
|
1521
|
+
if (annotation.kind !== "ruby" || annotation.reading.kind !== "group") return false;
|
|
1522
|
+
if (graphemeIndex <= annotation.range.graphemes.start || graphemeIndex >= annotation.range.graphemes.end) return false;
|
|
1523
|
+
return indicesInside(atoms, annotation).reduce((total, index) => total + (atoms[index]?.advanceEm ?? 0), 0) <= lineLengthEm;
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
function samePresentationGroup(left, right) {
|
|
1527
|
+
return left.presentation.groupRange.graphemes.start === right.presentation.groupRange.graphemes.start && left.presentation.groupRange.graphemes.end === right.presentation.groupRange.graphemes.end;
|
|
1528
|
+
}
|
|
1529
|
+
function legalBoundary(atoms, annotations, boundary, lineLengthEm) {
|
|
1530
|
+
const left = atoms[boundary - 1];
|
|
1531
|
+
const right = atoms[boundary];
|
|
1532
|
+
if (left === void 0 || right === void 0) return true;
|
|
1533
|
+
return !samePresentationGroup(left, right) && !LINE_END_PROHIBITED.has(left.grapheme.value) && !LINE_START_PROHIBITED.has(right.grapheme.value) && !INSEPARABLE_PAIRS.has(left.grapheme.value + right.grapheme.value) && !breakInsideFittableGroupRuby(atoms, annotations, boundary, lineLengthEm);
|
|
1534
|
+
}
|
|
1535
|
+
function continuationFor(annotation, range) {
|
|
1536
|
+
const starts = range.graphemes.start === annotation.range.graphemes.start;
|
|
1537
|
+
const ends = range.graphemes.end === annotation.range.graphemes.end;
|
|
1538
|
+
if (starts && ends) return "whole";
|
|
1539
|
+
if (starts) return "start";
|
|
1540
|
+
if (ends) return "end";
|
|
1541
|
+
return "middle";
|
|
1542
|
+
}
|
|
1543
|
+
function groupReadingsForFragments(annotation, fragmentRanges, baseAdvances) {
|
|
1544
|
+
const baseLength = annotation.range.graphemes.end - annotation.range.graphemes.start;
|
|
1545
|
+
if (annotation.reading.kind !== "group") return [];
|
|
1546
|
+
const reading = [...graphemeSegmenter.segment(annotation.reading.text)].map(({ segment }) => segment);
|
|
1547
|
+
const advances = Array.from({ length: baseLength }, (_, index) => baseAdvances.get(annotation.range.graphemes.start + index));
|
|
1548
|
+
const measured = advances.every((advance) => advance !== void 0);
|
|
1549
|
+
const advanceFor = (fragment) => {
|
|
1550
|
+
const fragmentStart = fragment.graphemes.start - annotation.range.graphemes.start;
|
|
1551
|
+
const fragmentLength = fragment.graphemes.end - fragment.graphemes.start;
|
|
1552
|
+
return measured ? advances.slice(fragmentStart, fragmentStart + fragmentLength).reduce((total, advance) => total + advance, 0) : fragmentLength;
|
|
1553
|
+
};
|
|
1554
|
+
const weights = fragmentRanges.map(advanceFor);
|
|
1555
|
+
const effectiveWeights = weights.reduce((total, weight) => total + weight, 0) > 0 ? weights : fragmentRanges.map(({ graphemes }) => graphemes.end - graphemes.start);
|
|
1556
|
+
const totalWeight = effectiveWeights.reduce((total, weight) => total + weight, 0);
|
|
1557
|
+
const reserveEveryFragment = reading.length >= fragmentRanges.length;
|
|
1558
|
+
const fragments = [];
|
|
1559
|
+
let readingStart = 0;
|
|
1560
|
+
let accumulatedWeight = 0;
|
|
1561
|
+
for (const [index, weight] of effectiveWeights.entries()) {
|
|
1562
|
+
accumulatedWeight += weight;
|
|
1563
|
+
const remainingFragments = effectiveWeights.length - index - 1;
|
|
1564
|
+
const proportionalEnd = index === effectiveWeights.length - 1 ? reading.length : Math.round(accumulatedWeight * reading.length / totalWeight);
|
|
1565
|
+
const minimumEnd = reserveEveryFragment ? readingStart + 1 : readingStart;
|
|
1566
|
+
const maximumEnd = reserveEveryFragment ? reading.length - remainingFragments : reading.length;
|
|
1567
|
+
const readingEnd = Math.min(maximumEnd, Math.max(minimumEnd, proportionalEnd));
|
|
1568
|
+
fragments.push(reading.slice(readingStart, readingEnd).join(""));
|
|
1569
|
+
readingStart = readingEnd;
|
|
1570
|
+
}
|
|
1571
|
+
return fragments;
|
|
1572
|
+
}
|
|
1573
|
+
function readingForFragment(annotation, range, fragmentPlan) {
|
|
1574
|
+
const start = range.graphemes.start - annotation.range.graphemes.start;
|
|
1575
|
+
const length = range.graphemes.end - range.graphemes.start;
|
|
1576
|
+
if (annotation.reading.kind !== "group") return {
|
|
1577
|
+
kind: annotation.reading.kind,
|
|
1578
|
+
text: annotation.reading.segments.slice(start, start + length).join("")
|
|
1579
|
+
};
|
|
1580
|
+
const fragmentIndex = fragmentPlan.ranges.findIndex(({ graphemes }) => graphemes.start === range.graphemes.start && graphemes.end === range.graphemes.end);
|
|
1581
|
+
return {
|
|
1582
|
+
kind: "group",
|
|
1583
|
+
text: fragmentPlan.groupReadings[fragmentIndex] ?? ""
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
function positionReading(text, baseOffsetEm, baseAdvanceEm, settings, measurer) {
|
|
1587
|
+
const measured = [...graphemeSegmenter.segment(text)].map(({ segment }) => ({
|
|
1588
|
+
value: segment,
|
|
1589
|
+
advanceEm: measure(measurer, segment, "ruby", settings) ?? 0
|
|
1192
1590
|
}));
|
|
1591
|
+
const total = measured.reduce((sum, grapheme) => sum + grapheme.advanceEm, 0);
|
|
1592
|
+
const edgeGap = measured.length > 0 && total < baseAdvanceEm ? (baseAdvanceEm - total) / (2 * measured.length) : 0;
|
|
1593
|
+
let offsetEm = baseOffsetEm + (total > baseAdvanceEm ? (baseAdvanceEm - total) / 2 : edgeGap);
|
|
1594
|
+
return measured.map((grapheme, index) => {
|
|
1595
|
+
const positioned = {
|
|
1596
|
+
value: grapheme.value,
|
|
1597
|
+
advanceEm: grapheme.advanceEm,
|
|
1598
|
+
offsetEm
|
|
1599
|
+
};
|
|
1600
|
+
offsetEm += grapheme.advanceEm;
|
|
1601
|
+
if (index < measured.length - 1) offsetEm += edgeGap * 2;
|
|
1602
|
+
return positioned;
|
|
1603
|
+
});
|
|
1193
1604
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
function
|
|
1198
|
-
|
|
1199
|
-
|
|
1605
|
+
function annotationFragmentRange(graphemes, suppressed, annotation) {
|
|
1606
|
+
return ManuscriptRange.merge([...graphemes.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range), ...suppressed.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range)]);
|
|
1607
|
+
}
|
|
1608
|
+
function annotationFragments(graphemes, suppressed, annotations, settings, measurer, fragmentPlansByAnnotation) {
|
|
1609
|
+
const fragments = [];
|
|
1610
|
+
for (const annotation of annotations) {
|
|
1611
|
+
const covered = graphemes.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range));
|
|
1612
|
+
const fragmentRange = annotationFragmentRange(graphemes, suppressed, annotation);
|
|
1613
|
+
const first = covered[0];
|
|
1614
|
+
const last = covered.at(-1);
|
|
1615
|
+
if (fragmentRange === null || first === void 0 || last === void 0) continue;
|
|
1616
|
+
const common = {
|
|
1617
|
+
annotationRange: annotation.range,
|
|
1618
|
+
fragmentRange,
|
|
1619
|
+
continuation: continuationFor(annotation, fragmentRange)
|
|
1620
|
+
};
|
|
1621
|
+
switch (annotation.kind) {
|
|
1622
|
+
case "bold":
|
|
1623
|
+
case "italic":
|
|
1624
|
+
fragments.push({
|
|
1625
|
+
kind: annotation.kind,
|
|
1626
|
+
...common
|
|
1627
|
+
});
|
|
1628
|
+
break;
|
|
1629
|
+
case "emphasis":
|
|
1630
|
+
fragments.push({
|
|
1631
|
+
kind: "emphasis",
|
|
1632
|
+
mark: annotation.mark,
|
|
1633
|
+
...common
|
|
1634
|
+
});
|
|
1635
|
+
break;
|
|
1636
|
+
case "ruby": {
|
|
1637
|
+
const fragmentPlan = fragmentPlansByAnnotation.get(annotation);
|
|
1638
|
+
if (fragmentPlan === void 0) break;
|
|
1639
|
+
const baseOffsetEm = first.offsetEm;
|
|
1640
|
+
const baseAdvanceEm = last.offsetEm + last.advanceEm - baseOffsetEm;
|
|
1641
|
+
const reading = readingForFragment(annotation, fragmentRange, fragmentPlan);
|
|
1642
|
+
fragments.push({
|
|
1643
|
+
kind: "ruby",
|
|
1644
|
+
rubyKind: reading.kind,
|
|
1645
|
+
reading: reading.text,
|
|
1646
|
+
baseOffsetEm,
|
|
1647
|
+
baseAdvanceEm,
|
|
1648
|
+
readingGraphemes: positionReading(reading.text, baseOffsetEm, baseAdvanceEm, settings, measurer),
|
|
1649
|
+
...common
|
|
1650
|
+
});
|
|
1651
|
+
break;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
return fragments;
|
|
1656
|
+
}
|
|
1657
|
+
function positionedLine(atoms, dispositions, suppressed) {
|
|
1658
|
+
let offsetEm = 0;
|
|
1659
|
+
const graphemes = atoms.map(({ grapheme, advanceEm, presentation }, index) => {
|
|
1660
|
+
const positioned = {
|
|
1661
|
+
kind: "grapheme",
|
|
1662
|
+
value: grapheme.value,
|
|
1663
|
+
range: grapheme.range,
|
|
1664
|
+
offsetEm,
|
|
1665
|
+
advanceEm,
|
|
1666
|
+
disposition: dispositions.has(index) ? "hanging" : "placed",
|
|
1667
|
+
presentation
|
|
1668
|
+
};
|
|
1669
|
+
offsetEm += advanceEm;
|
|
1670
|
+
return positioned;
|
|
1671
|
+
});
|
|
1672
|
+
return {
|
|
1673
|
+
range: ManuscriptRange.merge([...graphemes.map(({ range }) => range), ...suppressed.map(({ range }) => range)]),
|
|
1674
|
+
advanceEm: offsetEm,
|
|
1675
|
+
graphemes,
|
|
1676
|
+
suppressed,
|
|
1677
|
+
annotations: []
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
function wrapSourceLine(sourceLine, annotations, settings, measurer, baseAdvances) {
|
|
1681
|
+
if (sourceLine.atoms.length === 0) return [NovelLine.empty()];
|
|
1682
|
+
const lines = [];
|
|
1683
|
+
let cursor = 0;
|
|
1684
|
+
while (cursor < sourceLine.atoms.length) {
|
|
1685
|
+
const suppressed = [];
|
|
1686
|
+
if (lines.length > 0 && sourceLine.suppressedIndexes.has(cursor)) {
|
|
1687
|
+
const atom = sourceLine.atoms[cursor];
|
|
1688
|
+
if (atom !== void 0) suppressed.push({
|
|
1689
|
+
kind: "suppressed",
|
|
1690
|
+
value: atom.grapheme.value,
|
|
1691
|
+
range: atom.grapheme.range,
|
|
1692
|
+
reason: "question-or-exclamation-gap"
|
|
1693
|
+
});
|
|
1694
|
+
cursor += 1;
|
|
1695
|
+
}
|
|
1696
|
+
if (cursor >= sourceLine.atoms.length) {
|
|
1697
|
+
const previous = lines.at(-1);
|
|
1698
|
+
if (previous !== void 0 && suppressed.length > 0) lines[lines.length - 1] = {
|
|
1699
|
+
...previous,
|
|
1700
|
+
range: ManuscriptRange.merge([...previous.range === null ? [] : [previous.range], ...suppressed.map(({ range }) => range)]),
|
|
1701
|
+
suppressed: [...previous.suppressed, ...suppressed]
|
|
1702
|
+
};
|
|
1703
|
+
break;
|
|
1704
|
+
}
|
|
1705
|
+
const start = cursor;
|
|
1706
|
+
let advance = 0;
|
|
1707
|
+
while (cursor < sourceLine.atoms.length) {
|
|
1708
|
+
const atom = sourceLine.atoms[cursor];
|
|
1709
|
+
if (atom === void 0) break;
|
|
1710
|
+
const previous = sourceLine.atoms[cursor - 1];
|
|
1711
|
+
const beginsPresentationGroup = previous === void 0 || !samePresentationGroup(previous, atom);
|
|
1712
|
+
if (cursor > start && beginsPresentationGroup) {
|
|
1713
|
+
let groupAdvance = 0;
|
|
1714
|
+
for (let index = cursor; index < sourceLine.atoms.length; index += 1) {
|
|
1715
|
+
const member = sourceLine.atoms[index];
|
|
1716
|
+
if (member === void 0 || !samePresentationGroup(atom, member)) break;
|
|
1717
|
+
groupAdvance += member.advanceEm;
|
|
1718
|
+
}
|
|
1719
|
+
if (advance + groupAdvance > settings.flow.lineLengthEm) break;
|
|
1720
|
+
}
|
|
1721
|
+
if (cursor > start && advance + atom.advanceEm > settings.flow.lineLengthEm && (previous === void 0 || !samePresentationGroup(previous, atom))) break;
|
|
1722
|
+
advance += atom.advanceEm;
|
|
1723
|
+
cursor += 1;
|
|
1724
|
+
}
|
|
1725
|
+
let end = Math.max(start + 1, cursor);
|
|
1726
|
+
const hanging = /* @__PURE__ */ new Set();
|
|
1727
|
+
const next = sourceLine.atoms[end];
|
|
1728
|
+
if (next !== void 0 && HANGING_PUNCTUATION.has(next.grapheme.value)) {
|
|
1729
|
+
hanging.add(end - start);
|
|
1730
|
+
end += 1;
|
|
1731
|
+
} else while (end > start + 1 && !legalBoundary(sourceLine.atoms, annotations, end, settings.flow.lineLengthEm)) end -= 1;
|
|
1732
|
+
const atoms = sourceLine.atoms.slice(start, end);
|
|
1733
|
+
lines.push(positionedLine(atoms, hanging, suppressed));
|
|
1734
|
+
cursor = end;
|
|
1735
|
+
}
|
|
1736
|
+
const fragmentPlansByAnnotation = new Map(annotations.map((annotation) => {
|
|
1737
|
+
const ranges = lines.flatMap((line) => {
|
|
1738
|
+
const range = annotationFragmentRange(line.graphemes, line.suppressed, annotation);
|
|
1739
|
+
return range === null ? [] : [range];
|
|
1740
|
+
});
|
|
1741
|
+
return [annotation, {
|
|
1742
|
+
ranges,
|
|
1743
|
+
groupReadings: annotation.kind === "ruby" ? groupReadingsForFragments(annotation, ranges, baseAdvances) : []
|
|
1744
|
+
}];
|
|
1745
|
+
}));
|
|
1746
|
+
return lines.map((line) => ({
|
|
1747
|
+
range: line.range,
|
|
1748
|
+
advanceEm: line.advanceEm,
|
|
1749
|
+
graphemes: line.graphemes,
|
|
1750
|
+
suppressed: line.suppressed,
|
|
1751
|
+
annotations: annotationFragments(line.graphemes, line.suppressed, annotations, settings, measurer, fragmentPlansByAnnotation)
|
|
1752
|
+
}));
|
|
1200
1753
|
}
|
|
1201
|
-
function blankLines(count
|
|
1202
|
-
return Array.from({ length: count },
|
|
1754
|
+
function blankLines(count) {
|
|
1755
|
+
return Array.from({ length: count }, NovelLine.empty);
|
|
1203
1756
|
}
|
|
1204
|
-
/**
|
|
1205
|
-
* Walks every line position on every page in reading order, handing out content lines only where
|
|
1206
|
-
* the stage and page offsets leave room. Always emits at least one page.
|
|
1207
|
-
*/
|
|
1208
1757
|
function buildPages(contentLines, settings) {
|
|
1209
|
-
const {
|
|
1758
|
+
const { linesPerStage, stagesPerPage } = settings.flow;
|
|
1210
1759
|
const { stage: stageOffset, page: pageOffset } = settings.offsets;
|
|
1211
1760
|
const usablePerStage = linesPerStage - LineOffset.total(stageOffset);
|
|
1212
1761
|
const usablePerPage = usablePerStage * stagesPerPage;
|
|
@@ -1222,48 +1771,51 @@ function buildPages(contentLines, settings) {
|
|
|
1222
1771
|
const withinPage = usableIndex >= pageOffset.leading && usableIndex < usablePerPage - pageOffset.trailing;
|
|
1223
1772
|
const next = contentLines[contentIndex];
|
|
1224
1773
|
if (!withinStage || !withinPage || next === void 0) {
|
|
1225
|
-
lines.push(
|
|
1774
|
+
lines.push(NovelLine.empty());
|
|
1226
1775
|
continue;
|
|
1227
1776
|
}
|
|
1228
1777
|
lines.push(next);
|
|
1229
1778
|
contentIndex += 1;
|
|
1230
1779
|
}
|
|
1231
|
-
stages.push(
|
|
1780
|
+
stages.push(NovelStage.of(lines));
|
|
1232
1781
|
}
|
|
1233
|
-
pages.push(
|
|
1782
|
+
pages.push(NovelPage.of(stages));
|
|
1234
1783
|
}
|
|
1235
1784
|
return pages;
|
|
1236
1785
|
}
|
|
1237
|
-
function
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1786
|
+
function createCompose(measurer) {
|
|
1787
|
+
return (manuscript, settings) => {
|
|
1788
|
+
const sourceLines = displayedLines(manuscript.graphemes);
|
|
1789
|
+
const measured = sourceLines.map((line) => measureSourceLine(line, manuscript.annotations, settings, measurer));
|
|
1790
|
+
if (measured.some((line) => line === void 0)) return ManuscriptResult.fail({ reason: "inline measurer returned a negative or non-finite value" });
|
|
1791
|
+
const baseAdvances = /* @__PURE__ */ new Map();
|
|
1792
|
+
for (const line of measured) for (const atom of line?.atoms ?? []) baseAdvances.set(atom.grapheme.range.graphemes.start, atom.advanceEm);
|
|
1793
|
+
const manuscriptLines = measured.flatMap((line) => line === void 0 ? [] : wrapSourceLine(line, manuscript.annotations, settings, measurer, baseAdvances));
|
|
1794
|
+
const pages = buildPages([
|
|
1795
|
+
...blankLines(settings.offsets.document.leading),
|
|
1796
|
+
...manuscriptLines,
|
|
1797
|
+
...blankLines(settings.offsets.document.trailing)
|
|
1798
|
+
], settings);
|
|
1799
|
+
return ManuscriptResult.succeed({
|
|
1800
|
+
pages,
|
|
1801
|
+
geometry: ManuscriptGeometry.of(settings.flow, settings.appearance),
|
|
1802
|
+
stats: {
|
|
1803
|
+
chars: sourceLines.reduce((total, line) => total + line.length, 0),
|
|
1804
|
+
sourceLines: sourceLines.length,
|
|
1805
|
+
pages: pages.length
|
|
1806
|
+
}
|
|
1807
|
+
});
|
|
1808
|
+
};
|
|
1256
1809
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
};
|
|
1810
|
+
function createNovelComposer(options) {
|
|
1811
|
+
return {
|
|
1812
|
+
id: COMPOSER_ID,
|
|
1813
|
+
settingsSchema: NovelCompositionSettings.schema,
|
|
1814
|
+
layoutSchema: NovelLayout.schema,
|
|
1815
|
+
compose: createCompose(options.measurer)
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
const novelComposer = createNovelComposer({ measurer: logicalInlineMeasurer });
|
|
1267
1819
|
//#endregion
|
|
1268
1820
|
//#region src/proofreading/invalid-rule-options.ts
|
|
1269
1821
|
const InvalidRuleOptions = { describe: (error) => `ルール ${error.ruleId} のオプション "${error.option}" が不正です: ${error.issues.map(({ message }) => message).join(", ")}` };
|
|
@@ -1544,7 +2096,7 @@ function defineMatchRule(options) {
|
|
|
1544
2096
|
}
|
|
1545
2097
|
//#endregion
|
|
1546
2098
|
//#region src/proofreading/rules/consistent-kanji-opening.ts
|
|
1547
|
-
const RULE_ID$
|
|
2099
|
+
const RULE_ID$5 = "kg/consistent-kanji-opening";
|
|
1548
2100
|
const MESSAGE = "「{{ closed }}」と「{{ opened }}」が混在しています。文脈上の使い分けか表記ゆれかを確認してください";
|
|
1549
2101
|
const KanjiOpeningPairSchema = v.pipe(readonlyObject({
|
|
1550
2102
|
closed: v.pipe(v.string(), v.nonEmpty()),
|
|
@@ -1598,7 +2150,7 @@ const DEFAULT_PAIRS = [
|
|
|
1598
2150
|
* two spellings are a deliberate distinction is the author's call, so the finding is a warning.
|
|
1599
2151
|
*/
|
|
1600
2152
|
function build$3(pairs) {
|
|
1601
|
-
return defineParsedRule(RULE_ID$
|
|
2153
|
+
return defineParsedRule(RULE_ID$5, MESSAGE, (manuscript, report) => {
|
|
1602
2154
|
for (const { closed, opened } of pairs) {
|
|
1603
2155
|
if (!manuscript.displayText.includes(opened)) continue;
|
|
1604
2156
|
const index = manuscript.displayText.indexOf(closed);
|
|
@@ -1619,7 +2171,7 @@ function createConsistentKanjiOpeningRule(options = {}) {
|
|
|
1619
2171
|
const pairs = v.safeParse(PairsSchema, options.pairs);
|
|
1620
2172
|
if (!pairs.success) return ManuscriptResult.fail({
|
|
1621
2173
|
kind: "InvalidRuleOptions",
|
|
1622
|
-
ruleId: RULE_ID$
|
|
2174
|
+
ruleId: RULE_ID$5,
|
|
1623
2175
|
option: "pairs",
|
|
1624
2176
|
issues: ValidationIssue.from(pairs.issues)
|
|
1625
2177
|
});
|
|
@@ -1664,7 +2216,7 @@ const consistentNumeralWidthRule = () => defineMixedWidthRule({
|
|
|
1664
2216
|
});
|
|
1665
2217
|
//#endregion
|
|
1666
2218
|
//#region src/proofreading/rules/dash.ts
|
|
1667
|
-
const RULE_ID$
|
|
2219
|
+
const RULE_ID$4 = "kg/dash";
|
|
1668
2220
|
const DEFAULT_PREFERRED = "―";
|
|
1669
2221
|
const DashCharacterSchema = v.picklist([
|
|
1670
2222
|
"―",
|
|
@@ -1686,7 +2238,7 @@ function build$2(preferred) {
|
|
|
1686
2238
|
return {
|
|
1687
2239
|
kind: "parsed",
|
|
1688
2240
|
meta: {
|
|
1689
|
-
id: RULE_ID$
|
|
2241
|
+
id: RULE_ID$4,
|
|
1690
2242
|
messages
|
|
1691
2243
|
},
|
|
1692
2244
|
check: (manuscript, context) => {
|
|
@@ -1715,7 +2267,7 @@ function createDashRule(options = {}) {
|
|
|
1715
2267
|
const preferred = v.safeParse(DashCharacterSchema, options.preferred);
|
|
1716
2268
|
if (!preferred.success) return ManuscriptResult.fail({
|
|
1717
2269
|
kind: "InvalidRuleOptions",
|
|
1718
|
-
ruleId: RULE_ID$
|
|
2270
|
+
ruleId: RULE_ID$4,
|
|
1719
2271
|
option: "preferred",
|
|
1720
2272
|
issues: ValidationIssue.from(preferred.issues)
|
|
1721
2273
|
});
|
|
@@ -1742,8 +2294,8 @@ const evenEllipsisRule = () => defineMatchRule({
|
|
|
1742
2294
|
});
|
|
1743
2295
|
//#endregion
|
|
1744
2296
|
//#region src/proofreading/rules/fullwidth-japanese-punctuation.ts
|
|
1745
|
-
const RULE_ID$
|
|
1746
|
-
const MESSAGES$
|
|
2297
|
+
const RULE_ID$3 = "kg/fullwidth-japanese-punctuation";
|
|
2298
|
+
const MESSAGES$2 = {
|
|
1747
2299
|
"halfwidth-japanese-punctuation": "半角の「{{ character }}」があります。日本語の句読点・括弧・感嘆符・疑問符は全角にしてください",
|
|
1748
2300
|
"halfwidth-near-japanese": "日本語の前後に半角の「{{ character }}」があります。全角にすべきか確認してください"
|
|
1749
2301
|
};
|
|
@@ -1788,8 +2340,8 @@ function isEllipsisSubstitute(text, index) {
|
|
|
1788
2340
|
const fullwidthJapanesePunctuationRule = () => ({
|
|
1789
2341
|
kind: "parsed",
|
|
1790
2342
|
meta: {
|
|
1791
|
-
id: RULE_ID$
|
|
1792
|
-
messages: MESSAGES$
|
|
2343
|
+
id: RULE_ID$3,
|
|
2344
|
+
messages: MESSAGES$2
|
|
1793
2345
|
},
|
|
1794
2346
|
check: (manuscript, context) => {
|
|
1795
2347
|
for (const line of splitDisplayLines(manuscript.displayText)) for (const { pattern, messageId, severity, requiresJapanese } of CHECKS) for (const match of line.text.matchAll(pattern)) {
|
|
@@ -1807,12 +2359,12 @@ const fullwidthJapanesePunctuationRule = () => ({
|
|
|
1807
2359
|
});
|
|
1808
2360
|
//#endregion
|
|
1809
2361
|
//#region src/proofreading/rules/max-arabic-numeral-digits.ts
|
|
1810
|
-
const RULE_ID$
|
|
2362
|
+
const RULE_ID$2 = "kg/max-arabic-numeral-digits";
|
|
1811
2363
|
const DEFAULT_MAX_DIGITS = 2;
|
|
1812
2364
|
const MaxDigitsSchema = v.pipe(v.number(), v.finite(), v.integer(), v.minValue(1));
|
|
1813
2365
|
function build$1(maxDigits) {
|
|
1814
2366
|
return defineMatchRule({
|
|
1815
|
-
id: RULE_ID$
|
|
2367
|
+
id: RULE_ID$2,
|
|
1816
2368
|
pattern: /([0-90-9]+)(?:[..]([0-90-9]+))?/gu,
|
|
1817
2369
|
accept: (match) => (match[1]?.length ?? 0) > maxDigits || (match[2]?.length ?? 0) > maxDigits,
|
|
1818
2370
|
message: `${maxDigits}桁を超えるアラビア数字が使われています`
|
|
@@ -1824,7 +2376,7 @@ function createMaxArabicNumeralDigitsRule(options = {}) {
|
|
|
1824
2376
|
const maxDigits = v.safeParse(MaxDigitsSchema, options.maxDigits);
|
|
1825
2377
|
if (!maxDigits.success) return ManuscriptResult.fail({
|
|
1826
2378
|
kind: "InvalidRuleOptions",
|
|
1827
|
-
ruleId: RULE_ID$
|
|
2379
|
+
ruleId: RULE_ID$2,
|
|
1828
2380
|
option: "maxDigits",
|
|
1829
2381
|
issues: ValidationIssue.from(maxDigits.issues)
|
|
1830
2382
|
});
|
|
@@ -1852,24 +2404,6 @@ const noConsecutivePunctuationRule = () => defineMatchRule({
|
|
|
1852
2404
|
message: "句読点が連続しています"
|
|
1853
2405
|
});
|
|
1854
2406
|
//#endregion
|
|
1855
|
-
//#region src/proofreading/rules/internal/brackets.ts
|
|
1856
|
-
/**
|
|
1857
|
-
* Brackets and quotes a Japanese paragraph may open with. Halfwidth forms are included because a
|
|
1858
|
-
* manuscript that uses them is still opening a paragraph, not writing prose.
|
|
1859
|
-
*/
|
|
1860
|
-
const OPENING_BRACKETS = "「『〖〈《((“\"‘'[[〔{{<<";
|
|
1861
|
-
/**
|
|
1862
|
-
* The closing counterparts, in the same order.
|
|
1863
|
-
*/
|
|
1864
|
-
const CLOSING_BRACKETS = "」』〗〉》))”\"’']]〕}}>>";
|
|
1865
|
-
/**
|
|
1866
|
-
* Escapes the characters a regular expression character class would otherwise read as syntax, so a
|
|
1867
|
-
* set declared above can be dropped into a pattern.
|
|
1868
|
-
*/
|
|
1869
|
-
function characterClass(characters) {
|
|
1870
|
-
return characters.replaceAll(/[\\\]^-]/gu, "\\$&");
|
|
1871
|
-
}
|
|
1872
|
-
//#endregion
|
|
1873
2407
|
//#region src/proofreading/rules/internal/decoration-line.ts
|
|
1874
2408
|
const DECORATION_LINE = /^[**◆◇■□●○▽▼△▲☆★・…―─━ー\-==~~\s]+$/u;
|
|
1875
2409
|
/**
|
|
@@ -1880,19 +2414,9 @@ function isDecorationLine(text) {
|
|
|
1880
2414
|
return DECORATION_LINE.test(text);
|
|
1881
2415
|
}
|
|
1882
2416
|
//#endregion
|
|
1883
|
-
//#region src/proofreading/rules/internal/spaces.ts
|
|
1884
|
-
const LEADING_SPACES = /^[ \t]+/u;
|
|
1885
|
-
/**
|
|
1886
|
-
* The run of spaces the text begins with, or an empty string. Halfwidth spaces and tabs count: a
|
|
1887
|
-
* rule has to see them to say they are the wrong space.
|
|
1888
|
-
*/
|
|
1889
|
-
function leadingSpaces(text) {
|
|
1890
|
-
return LEADING_SPACES.exec(text)?.[0] ?? "";
|
|
1891
|
-
}
|
|
1892
|
-
//#endregion
|
|
1893
2417
|
//#region src/proofreading/rules/paragraph-opening.ts
|
|
1894
|
-
const RULE_ID$
|
|
1895
|
-
const MESSAGES$
|
|
2418
|
+
const RULE_ID$1 = "kg/paragraph-opening";
|
|
2419
|
+
const MESSAGES$1 = {
|
|
1896
2420
|
"leading-character": "段落の先頭には全角スペースまたは開き括弧が必要です",
|
|
1897
2421
|
"indent-width": "段落冒頭の字下げは全角スペース1字にしてください",
|
|
1898
2422
|
"unexpected-indent": "括弧から始まる段落を字下げすることはできません"
|
|
@@ -1907,8 +2431,8 @@ function build(openingBrackets) {
|
|
|
1907
2431
|
return {
|
|
1908
2432
|
kind: "parsed",
|
|
1909
2433
|
meta: {
|
|
1910
|
-
id: RULE_ID$
|
|
1911
|
-
messages: MESSAGES$
|
|
2434
|
+
id: RULE_ID$1,
|
|
2435
|
+
messages: MESSAGES$1
|
|
1912
2436
|
},
|
|
1913
2437
|
check: (manuscript, context) => {
|
|
1914
2438
|
const report = (start, end, messageId) => {
|
|
@@ -1938,7 +2462,7 @@ function createParagraphOpeningRule(options = {}) {
|
|
|
1938
2462
|
const brackets = v.safeParse(OpeningBracketsSchema, options.openingBrackets);
|
|
1939
2463
|
if (!brackets.success) return ManuscriptResult.fail({
|
|
1940
2464
|
kind: "InvalidRuleOptions",
|
|
1941
|
-
ruleId: RULE_ID$
|
|
2465
|
+
ruleId: RULE_ID$1,
|
|
1942
2466
|
option: "openingBrackets",
|
|
1943
2467
|
issues: ValidationIssue.from(brackets.issues)
|
|
1944
2468
|
});
|
|
@@ -1954,25 +2478,21 @@ const punctuationBeforeClosingQuoteRule = () => defineMatchRule({
|
|
|
1954
2478
|
});
|
|
1955
2479
|
//#endregion
|
|
1956
2480
|
//#region src/proofreading/rules/space-after-question-or-exclamation.ts
|
|
1957
|
-
const RULE_ID
|
|
1958
|
-
const MESSAGES
|
|
2481
|
+
const RULE_ID = "kg/space-after-question-or-exclamation";
|
|
2482
|
+
const MESSAGES = {
|
|
1959
2483
|
default: "感嘆符または疑問符の直後には全角スペースか閉じ括弧が必要です",
|
|
1960
2484
|
"space-width": "感嘆符または疑問符の直後の空白は全角スペース1字にしてください",
|
|
1961
2485
|
"before-closing-bracket": "閉じ括弧の直前に空白を置くことはできません"
|
|
1962
2486
|
};
|
|
1963
2487
|
/**
|
|
1964
|
-
* Runs, not single marks: `!?` is one mark and takes one gap, so the gap belongs after the run.
|
|
1965
|
-
*/
|
|
1966
|
-
const MARKS = /[?!]+/gu;
|
|
1967
|
-
/**
|
|
1968
2488
|
* Checks the gap that follows every run of ! or ?. A run at the end of a line or right before a
|
|
1969
2489
|
* closing bracket needs no gap; anywhere else it takes exactly one ideographic space.
|
|
1970
2490
|
*/
|
|
1971
2491
|
const spaceAfterQuestionOrExclamationRule = () => ({
|
|
1972
2492
|
kind: "parsed",
|
|
1973
2493
|
meta: {
|
|
1974
|
-
id: RULE_ID
|
|
1975
|
-
messages: MESSAGES
|
|
2494
|
+
id: RULE_ID,
|
|
2495
|
+
messages: MESSAGES
|
|
1976
2496
|
},
|
|
1977
2497
|
check: (manuscript, context) => {
|
|
1978
2498
|
const report = (start, end, messageId) => {
|
|
@@ -1981,93 +2501,39 @@ const spaceAfterQuestionOrExclamationRule = () => ({
|
|
|
1981
2501
|
messageId
|
|
1982
2502
|
});
|
|
1983
2503
|
};
|
|
1984
|
-
for (const line of splitDisplayLines(manuscript.displayText)) for (const
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
report(line.start +
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
if (following !== void 0 && "」』〗〉》))”\"’']]〕}}>>".includes(following)) report(spacesStart, spacesEnd, "before-closing-bracket");
|
|
1997
|
-
else if (spaces !== " ") report(spacesStart, spacesEnd, "space-width");
|
|
2504
|
+
for (const line of splitDisplayLines(manuscript.displayText)) for (const spacing of questionOrExclamationSpacings(line.text)) switch (spacing.kind) {
|
|
2505
|
+
case "valid": break;
|
|
2506
|
+
case "missing":
|
|
2507
|
+
report(line.start + spacing.marks.start, line.start + spacing.marks.end, "default");
|
|
2508
|
+
break;
|
|
2509
|
+
case "invalid-space":
|
|
2510
|
+
report(line.start + spacing.gap.start, line.start + spacing.gap.end, "space-width");
|
|
2511
|
+
break;
|
|
2512
|
+
case "before-closing-bracket":
|
|
2513
|
+
report(line.start + spacing.gap.start, line.start + spacing.gap.end, "before-closing-bracket");
|
|
2514
|
+
break;
|
|
2515
|
+
default: assertNever(spacing);
|
|
1998
2516
|
}
|
|
1999
2517
|
}
|
|
2000
2518
|
});
|
|
2001
2519
|
//#endregion
|
|
2002
2520
|
//#region src/proofreading/rules/variant-character.ts
|
|
2003
|
-
const RULE_ID = "kg/variant-character";
|
|
2004
|
-
const MESSAGES = {
|
|
2005
|
-
default: "異体字または字形選択子が使われています",
|
|
2006
|
-
"with-suggestion": "異体字または字形選択子が使われています。「{{ suggestion }}」ではありませんか?"
|
|
2007
|
-
};
|
|
2008
2521
|
/**
|
|
2009
|
-
* Mongolian free variation selectors, variation selectors
|
|
2010
|
-
* combine with a preceding base character rather than standing on their own.
|
|
2522
|
+
* Mongolian free variation selectors, variation selectors, CJK compatibility ideographs, IVSes.
|
|
2011
2523
|
*/
|
|
2012
|
-
function isSelector(codePoint) {
|
|
2013
|
-
return codePoint >= 6155 && codePoint <= 6157 || codePoint === 6159 || codePoint >= 65024 && codePoint <= 65039 || codePoint >= 917760 && codePoint <= 917999;
|
|
2014
|
-
}
|
|
2015
|
-
/**
|
|
2016
|
-
* CJK compatibility ideographs stand alone; each has a canonical unified form reachable via NFKC.
|
|
2017
|
-
*/
|
|
2018
|
-
function isCompatibilityIdeograph(codePoint) {
|
|
2019
|
-
return codePoint >= 63744 && codePoint <= 64255 || codePoint >= 194560 && codePoint <= 195103;
|
|
2020
|
-
}
|
|
2021
2524
|
function isVariantCharacter(character) {
|
|
2022
2525
|
const codePoint = character.codePointAt(0);
|
|
2023
2526
|
if (codePoint === void 0) return false;
|
|
2024
|
-
return
|
|
2025
|
-
}
|
|
2026
|
-
/**
|
|
2027
|
-
* The plain form a reader would expect: the selector stripped away, or the compatibility
|
|
2028
|
-
* ideograph's canonical unified form. Undefined when that plain form is identical to the original,
|
|
2029
|
-
* since NFKC does not touch selectors and a suggestion equal to the input helps no one.
|
|
2030
|
-
*/
|
|
2031
|
-
function suggestPlainForm(segment) {
|
|
2032
|
-
const withoutSelectors = Array.from(segment).filter((character) => {
|
|
2033
|
-
const codePoint = character.codePointAt(0);
|
|
2034
|
-
return codePoint !== void 0 && !isSelector(codePoint);
|
|
2035
|
-
}).join("");
|
|
2036
|
-
if (withoutSelectors === "") return void 0;
|
|
2037
|
-
const normalized = Array.from(withoutSelectors).map((character) => {
|
|
2038
|
-
const codePoint = character.codePointAt(0);
|
|
2039
|
-
return codePoint !== void 0 && isCompatibilityIdeograph(codePoint) ? character.normalize("NFKC") : character;
|
|
2040
|
-
}).join("");
|
|
2041
|
-
return normalized === segment ? void 0 : normalized;
|
|
2527
|
+
return codePoint >= 6155 && codePoint <= 6157 || codePoint === 6159 || codePoint >= 65024 && codePoint <= 65039 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 194560 && codePoint <= 195103 || codePoint >= 917760 && codePoint <= 917999;
|
|
2042
2528
|
}
|
|
2043
2529
|
/**
|
|
2044
2530
|
* Scans the source rather than the display text: notation removal must not hide a variant.
|
|
2045
2531
|
*/
|
|
2046
|
-
const variantCharacterRule = () => ({
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
},
|
|
2052
|
-
check: (manuscript, context) => {
|
|
2053
|
-
for (const { index, segment } of graphemeSegmenter.segment(manuscript.source)) {
|
|
2054
|
-
if (!Array.from(segment).some(isVariantCharacter)) continue;
|
|
2055
|
-
const range = sourceRange(manuscript, {
|
|
2056
|
-
start: index,
|
|
2057
|
-
end: index + segment.length
|
|
2058
|
-
});
|
|
2059
|
-
const suggestion = suggestPlainForm(segment);
|
|
2060
|
-
if (suggestion === void 0) context.report({
|
|
2061
|
-
range,
|
|
2062
|
-
messageId: "default"
|
|
2063
|
-
});
|
|
2064
|
-
else context.report({
|
|
2065
|
-
range,
|
|
2066
|
-
messageId: "with-suggestion",
|
|
2067
|
-
data: { suggestion }
|
|
2068
|
-
});
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2532
|
+
const variantCharacterRule = () => defineParsedRule("kg/variant-character", "異体字または字形選択子が使われています", (manuscript, report) => {
|
|
2533
|
+
for (const { index, segment } of graphemeSegmenter.segment(manuscript.source)) if (Array.from(segment).some(isVariantCharacter)) report(sourceRange(manuscript, {
|
|
2534
|
+
start: index,
|
|
2535
|
+
end: index + segment.length
|
|
2536
|
+
}));
|
|
2071
2537
|
});
|
|
2072
2538
|
//#endregion
|
|
2073
2539
|
//#region src/proofreading/rules/default-rules.ts
|
|
@@ -2095,4 +2561,4 @@ function createDefaultProofreadingRules() {
|
|
|
2095
2561
|
];
|
|
2096
2562
|
}
|
|
2097
2563
|
//#endregion
|
|
2098
|
-
export { BoldAnnotation, ComposeError, CompositionStatistics, DiagnosticOrigin, DiagnosticSeverity, DisplayRange, EmphasisAnnotation, FontPreset, FontPresetId, FontSizePt, GraphemeRange,
|
|
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 };
|