@sushichan044/kg-core 0.6.5 → 0.8.0
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 +62 -13
- package/dist/composed-manuscript-QgIdae8p.d.mts +1740 -0
- package/dist/describe-cause-BV8mZ3Um.mjs +18 -0
- package/dist/index.d.mts +6481 -18298
- package/dist/index.mjs +50 -2475
- package/dist/lint.d.mts +461 -0
- package/dist/lint.mjs +812 -0
- package/dist/novel-composer-BCt9g172.d.mts +9781 -0
- package/dist/novel-composer-CROZLBqt.mjs +1929 -0
- package/dist/plugin.d.mts +3 -0
- package/dist/plugin.mjs +3 -0
- package/dist/question-or-exclamation-spacing-B5o23ZLf.mjs +251 -0
- package/package.json +11 -4
package/dist/index.mjs
CHANGED
|
@@ -1,429 +1,8 @@
|
|
|
1
|
+
import { c as ManuscriptResult, d as NamespacedId, f as ManuscriptRange, h as readonlyObject, l as ManuscriptDiagnostic, o as graphemeSegmenter, s as ValidationIssue } from "./question-or-exclamation-spacing-B5o23ZLf.mjs";
|
|
2
|
+
import { c as ManuscriptOffsets, d as FontSizePt, f as FontPresetId, i as PaperSize, l as ManuscriptAppearanceSettings, n as novelComposer, o as NovelCompositionSettings, p as ParsedManuscript, r as ManuscriptGeometry, s as NovelFlowSettings, u as PaperSizeId } from "./novel-composer-CROZLBqt.mjs";
|
|
3
|
+
import { n as assertNever, t as describeCause } from "./describe-cause-BV8mZ3Um.mjs";
|
|
1
4
|
import * as v from "valibot";
|
|
2
|
-
|
|
3
|
-
//#region src/assert-never.ts
|
|
4
|
-
/**
|
|
5
|
-
* Marks a branch the compiler proved unreachable. Reaching it at runtime means a variant was added
|
|
6
|
-
* to a union without updating the switch that produced this call.
|
|
7
|
-
*/
|
|
8
|
-
function assertNever(value) {
|
|
9
|
-
throw new TypeError(`unhandled variant: ${JSON.stringify(value)}`);
|
|
10
|
-
}
|
|
11
|
-
//#endregion
|
|
12
|
-
//#region src/namespaced-id.ts
|
|
13
|
-
const NamespacedIdSchema = v.pipe(v.string(), v.nonEmpty(), v.regex(/^[^/]+\/[^/]+$/u, "ID must use namespace/name"), v.brand("NamespacedId"));
|
|
14
|
-
const NamespacedId = {
|
|
15
|
-
schema: NamespacedIdSchema,
|
|
16
|
-
/**
|
|
17
|
-
* For IDs written as literals in code. Throws on a malformed literal, at import time.
|
|
18
|
-
*/
|
|
19
|
-
of: (literal) => v.parse(NamespacedIdSchema, literal),
|
|
20
|
-
/**
|
|
21
|
-
* For IDs supplied by a plugin, where a malformed value is bad input rather than a bug.
|
|
22
|
-
*/
|
|
23
|
-
parse: (raw) => {
|
|
24
|
-
const result = v.safeParse(NamespacedIdSchema, raw);
|
|
25
|
-
return result.success ? result.output : void 0;
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
//#endregion
|
|
29
|
-
//#region src/internal/schema.ts
|
|
30
|
-
/**
|
|
31
|
-
* Every DTO in this package is a closed, frozen object: unknown keys are rejected, output is
|
|
32
|
-
* readonly.
|
|
33
|
-
*/
|
|
34
|
-
const readonlyObject = (entries) => v.pipe(v.strictObject(entries), v.readonly());
|
|
35
|
-
const readonlyArray = (item) => v.pipe(v.array(item), v.readonly());
|
|
36
|
-
const nonNegativeInteger = (maximum = Number.POSITIVE_INFINITY) => v.pipe(v.number(), v.finite(), v.integer(), v.minValue(0), v.maxValue(maximum));
|
|
37
|
-
const positiveInteger = () => v.pipe(v.number(), v.finite(), v.integer(), v.minValue(1));
|
|
38
|
-
const TextRange = { schema: v.pipe(readonlyObject({
|
|
39
|
-
start: nonNegativeInteger(),
|
|
40
|
-
end: nonNegativeInteger()
|
|
41
|
-
}), v.check(({ start, end }) => end >= start, "range end must not precede its start")) };
|
|
42
|
-
const DisplayRange = { schema: v.pipe(TextRange.schema, v.brand("DisplayRange")) };
|
|
43
|
-
const GraphemeRange = { schema: v.pipe(TextRange.schema, v.brand("GraphemeRange")) };
|
|
44
|
-
const SourceRange = { schema: v.pipe(TextRange.schema, v.brand("SourceRange")) };
|
|
45
|
-
//#endregion
|
|
46
|
-
//#region src/range/manuscript-range.ts
|
|
47
|
-
const ManuscriptRangeSchema = readonlyObject({
|
|
48
|
-
source: SourceRange.schema,
|
|
49
|
-
display: DisplayRange.schema,
|
|
50
|
-
graphemes: GraphemeRange.schema
|
|
51
|
-
});
|
|
52
|
-
const ManuscriptRange = {
|
|
53
|
-
schema: ManuscriptRangeSchema,
|
|
54
|
-
/**
|
|
55
|
-
* Brands a span core computed itself. Throws rather than returning a failure: the inputs come
|
|
56
|
-
* from core's own arithmetic, so a violated bound is a bug here, not bad data from a caller.
|
|
57
|
-
*/
|
|
58
|
-
of: (input) => v.parse(ManuscriptRangeSchema, input),
|
|
59
|
-
merge: (ranges) => {
|
|
60
|
-
const present = ranges.filter((range) => range !== null);
|
|
61
|
-
if (present.length === 0) return null;
|
|
62
|
-
return ManuscriptRange.of({
|
|
63
|
-
source: {
|
|
64
|
-
start: Math.min(...present.map(({ source }) => source.start)),
|
|
65
|
-
end: Math.max(...present.map(({ source }) => source.end))
|
|
66
|
-
},
|
|
67
|
-
display: {
|
|
68
|
-
start: Math.min(...present.map(({ display }) => display.start)),
|
|
69
|
-
end: Math.max(...present.map(({ display }) => display.end))
|
|
70
|
-
},
|
|
71
|
-
graphemes: {
|
|
72
|
-
start: Math.min(...present.map(({ graphemes }) => graphemes.start)),
|
|
73
|
-
end: Math.max(...present.map(({ graphemes }) => graphemes.end))
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
},
|
|
77
|
-
overlaps: (range, other) => range.graphemes.start < other.graphemes.end && range.graphemes.end > other.graphemes.start
|
|
78
|
-
};
|
|
79
|
-
const SourcePosition = {
|
|
80
|
-
schema: readonlyObject({
|
|
81
|
-
offset: nonNegativeInteger(),
|
|
82
|
-
line: positiveInteger(),
|
|
83
|
-
column: positiveInteger()
|
|
84
|
-
}),
|
|
85
|
-
/**
|
|
86
|
-
* Counts CRLF, CR and LF as one line break each; column is UTF-16 code units from line start.
|
|
87
|
-
*/
|
|
88
|
-
at: (source, offset) => {
|
|
89
|
-
let line = 1;
|
|
90
|
-
let lineStart = 0;
|
|
91
|
-
let index = 0;
|
|
92
|
-
while (index < offset) if (source[index] === "\r") {
|
|
93
|
-
index += source[index + 1] === "\n" ? 2 : 1;
|
|
94
|
-
line += 1;
|
|
95
|
-
lineStart = index;
|
|
96
|
-
} else if (source[index] === "\n") {
|
|
97
|
-
index += 1;
|
|
98
|
-
line += 1;
|
|
99
|
-
lineStart = index;
|
|
100
|
-
} else index += 1;
|
|
101
|
-
return {
|
|
102
|
-
offset,
|
|
103
|
-
line,
|
|
104
|
-
column: offset - lineStart + 1
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
const DiagnosticOrigin = { schema: v.variant("kind", [readonlyObject({
|
|
109
|
-
kind: v.literal("parser"),
|
|
110
|
-
id: NamespacedId.schema
|
|
111
|
-
}), readonlyObject({
|
|
112
|
-
kind: v.literal("rule"),
|
|
113
|
-
id: NamespacedId.schema
|
|
114
|
-
})]) };
|
|
115
|
-
const DiagnosticSeverity = { schema: v.picklist(["warning", "error"]) };
|
|
116
|
-
const ManuscriptDiagnostic = {
|
|
117
|
-
schema: readonlyObject({
|
|
118
|
-
id: v.string(),
|
|
119
|
-
origin: DiagnosticOrigin.schema,
|
|
120
|
-
severity: DiagnosticSeverity.schema,
|
|
121
|
-
message: v.string(),
|
|
122
|
-
range: ManuscriptRange.schema,
|
|
123
|
-
location: readonlyObject({
|
|
124
|
-
start: SourcePosition.schema,
|
|
125
|
-
end: SourcePosition.schema
|
|
126
|
-
})
|
|
127
|
-
}),
|
|
128
|
-
of: ({ source, origin, severity, code, message, range }) => ({
|
|
129
|
-
id: `${origin.kind}:${origin.id}:${code}:${range.source.start}:${range.source.end}`,
|
|
130
|
-
origin,
|
|
131
|
-
severity,
|
|
132
|
-
message,
|
|
133
|
-
range,
|
|
134
|
-
location: {
|
|
135
|
-
start: SourcePosition.at(source, range.source.start),
|
|
136
|
-
end: SourcePosition.at(source, range.source.end)
|
|
137
|
-
}
|
|
138
|
-
}),
|
|
139
|
-
/**
|
|
140
|
-
* Stable order for display: by source span, then by producing plugin.
|
|
141
|
-
*/
|
|
142
|
-
compare: (left, right) => left.range.source.start - right.range.source.start || left.range.source.end - right.range.source.end || left.origin.id.localeCompare(right.origin.id)
|
|
143
|
-
};
|
|
144
|
-
//#endregion
|
|
145
|
-
//#region src/result/manuscript-result.ts
|
|
146
|
-
const ManuscriptResult = {
|
|
147
|
-
succeed: (value, warnings = []) => ({
|
|
148
|
-
ok: true,
|
|
149
|
-
value,
|
|
150
|
-
warnings
|
|
151
|
-
}),
|
|
152
|
-
fail: (error) => ({
|
|
153
|
-
ok: false,
|
|
154
|
-
error
|
|
155
|
-
}),
|
|
156
|
-
/**
|
|
157
|
-
* Validates a result that crossed a plugin boundary, including the diagnostics it carries.
|
|
158
|
-
*/
|
|
159
|
-
schema: (value, error) => v.variant("ok", [readonlyObject({
|
|
160
|
-
ok: v.literal(true),
|
|
161
|
-
value,
|
|
162
|
-
warnings: readonlyArray(ManuscriptDiagnostic.schema)
|
|
163
|
-
}), readonlyObject({
|
|
164
|
-
ok: v.literal(false),
|
|
165
|
-
error
|
|
166
|
-
})])
|
|
167
|
-
};
|
|
168
|
-
const Rejection = {
|
|
169
|
-
schema: readonlyObject({ reason: v.string() }),
|
|
170
|
-
of: (reason) => ({ reason })
|
|
171
|
-
};
|
|
172
|
-
const ValidationIssue = {
|
|
173
|
-
schema: readonlyObject({
|
|
174
|
-
path: v.string(),
|
|
175
|
-
message: v.string()
|
|
176
|
-
}),
|
|
177
|
-
from: (issues) => issues.map((issue) => ({
|
|
178
|
-
path: (issue.path ?? []).map((item) => String(item.key)).join("."),
|
|
179
|
-
message: issue.message
|
|
180
|
-
}))
|
|
181
|
-
};
|
|
182
|
-
//#endregion
|
|
183
|
-
//#region src/appearance/font-preset-id.ts
|
|
184
|
-
const FontPresetIdSchema = v.picklist(["mincho", "gothic"]);
|
|
185
|
-
const FontPresetId = {
|
|
186
|
-
schema: FontPresetIdSchema,
|
|
187
|
-
is: (value) => v.is(FontPresetIdSchema, value)
|
|
188
|
-
};
|
|
189
|
-
//#endregion
|
|
190
|
-
//#region src/appearance/font-size-pt.ts
|
|
191
|
-
const RANGE = {
|
|
192
|
-
min: 6,
|
|
193
|
-
max: 24,
|
|
194
|
-
step: .5
|
|
195
|
-
};
|
|
196
|
-
const FontSizePtSchema = v.pipe(v.number(), v.finite(), v.minValue(RANGE.min), v.maxValue(RANGE.max), v.multipleOf(RANGE.step));
|
|
197
|
-
const FontSizePt = {
|
|
198
|
-
schema: FontSizePtSchema,
|
|
199
|
-
range: RANGE,
|
|
200
|
-
is: (value) => v.is(FontSizePtSchema, value)
|
|
201
|
-
};
|
|
202
|
-
//#endregion
|
|
203
|
-
//#region src/appearance/paper-size-id.ts
|
|
204
|
-
const PaperSizeIdSchema = v.picklist([
|
|
205
|
-
"a4",
|
|
206
|
-
"a5",
|
|
207
|
-
"a6",
|
|
208
|
-
"jis-b5",
|
|
209
|
-
"jis-b6",
|
|
210
|
-
"shinsho"
|
|
211
|
-
]);
|
|
212
|
-
const PaperSizeId = {
|
|
213
|
-
schema: PaperSizeIdSchema,
|
|
214
|
-
is: (value) => v.is(PaperSizeIdSchema, value)
|
|
215
|
-
};
|
|
216
|
-
const ManuscriptAppearanceSettings = {
|
|
217
|
-
schema: readonlyObject({
|
|
218
|
-
paperSize: PaperSizeId.schema,
|
|
219
|
-
fontSizePt: FontSizePt.schema,
|
|
220
|
-
fontPreset: FontPresetId.schema
|
|
221
|
-
}),
|
|
222
|
-
defaults: {
|
|
223
|
-
paperSize: "a5",
|
|
224
|
-
fontSizePt: 9,
|
|
225
|
-
fontPreset: "mincho"
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
//#endregion
|
|
229
|
-
//#region src/appearance/font-preset.ts
|
|
230
|
-
const FONT_PRESETS = {
|
|
231
|
-
mincho: {
|
|
232
|
-
id: "mincho",
|
|
233
|
-
label: "明朝",
|
|
234
|
-
family: "\"Yu Mincho\", \"Hiragino Mincho ProN\", \"Hiragino Mincho Pro\", serif"
|
|
235
|
-
},
|
|
236
|
-
gothic: {
|
|
237
|
-
id: "gothic",
|
|
238
|
-
label: "ゴシック",
|
|
239
|
-
family: "\"Yu Gothic\", \"Hiragino Kaku Gothic ProN\", system-ui, sans-serif"
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
const FontPreset = {
|
|
243
|
-
of: (id) => FONT_PRESETS[id],
|
|
244
|
-
all: Object.values(FONT_PRESETS)
|
|
245
|
-
};
|
|
246
|
-
//#endregion
|
|
247
|
-
//#region src/appearance/length.ts
|
|
248
|
-
const PT_PER_MM = 72 / 25.4;
|
|
249
|
-
function ptToMm(pt) {
|
|
250
|
-
return pt / PT_PER_MM;
|
|
251
|
-
}
|
|
252
|
-
function mmToPt(mm) {
|
|
253
|
-
return mm * PT_PER_MM;
|
|
254
|
-
}
|
|
255
|
-
//#endregion
|
|
256
|
-
//#region src/appearance/paper-size.ts
|
|
257
|
-
const PAPER_SIZES = {
|
|
258
|
-
a4: {
|
|
259
|
-
id: "a4",
|
|
260
|
-
label: "A4",
|
|
261
|
-
widthMm: 210,
|
|
262
|
-
heightMm: 297
|
|
263
|
-
},
|
|
264
|
-
a5: {
|
|
265
|
-
id: "a5",
|
|
266
|
-
label: "A5",
|
|
267
|
-
widthMm: 148,
|
|
268
|
-
heightMm: 210
|
|
269
|
-
},
|
|
270
|
-
a6: {
|
|
271
|
-
id: "a6",
|
|
272
|
-
label: "A6(文庫)",
|
|
273
|
-
widthMm: 105,
|
|
274
|
-
heightMm: 148
|
|
275
|
-
},
|
|
276
|
-
"jis-b5": {
|
|
277
|
-
id: "jis-b5",
|
|
278
|
-
label: "B5(JIS)",
|
|
279
|
-
widthMm: 182,
|
|
280
|
-
heightMm: 257
|
|
281
|
-
},
|
|
282
|
-
"jis-b6": {
|
|
283
|
-
id: "jis-b6",
|
|
284
|
-
label: "B6(JIS)",
|
|
285
|
-
widthMm: 128,
|
|
286
|
-
heightMm: 182
|
|
287
|
-
},
|
|
288
|
-
shinsho: {
|
|
289
|
-
id: "shinsho",
|
|
290
|
-
label: "新書",
|
|
291
|
-
widthMm: 106,
|
|
292
|
-
heightMm: 173
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
const PaperSize = {
|
|
296
|
-
of: (id) => PAPER_SIZES[id],
|
|
297
|
-
all: Object.values(PAPER_SIZES)
|
|
298
|
-
};
|
|
299
|
-
const BoldAnnotation = { schema: readonlyObject({
|
|
300
|
-
kind: v.literal("bold"),
|
|
301
|
-
range: ManuscriptRange.schema
|
|
302
|
-
}) };
|
|
303
|
-
const EmphasisAnnotation = { schema: readonlyObject({
|
|
304
|
-
kind: v.literal("emphasis"),
|
|
305
|
-
range: ManuscriptRange.schema,
|
|
306
|
-
mark: v.string()
|
|
307
|
-
}) };
|
|
308
|
-
const ItalicAnnotation = { schema: readonlyObject({
|
|
309
|
-
kind: v.literal("italic"),
|
|
310
|
-
range: ManuscriptRange.schema
|
|
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({
|
|
337
|
-
kind: v.literal("ruby"),
|
|
338
|
-
range: ManuscriptRange.schema,
|
|
339
|
-
reading: RubyReadingSchema
|
|
340
|
-
});
|
|
341
|
-
const RubyKind = { schema: RubyKindSchema };
|
|
342
|
-
const RubyReading = { schema: RubyReadingSchema };
|
|
343
|
-
const RubyAnnotation = { schema: RubyAnnotationSchema };
|
|
344
|
-
const ManuscriptAnnotation = {
|
|
345
|
-
schema: v.variant("kind", [
|
|
346
|
-
RubyAnnotation.schema,
|
|
347
|
-
BoldAnnotation.schema,
|
|
348
|
-
ItalicAnnotation.schema,
|
|
349
|
-
EmphasisAnnotation.schema
|
|
350
|
-
]),
|
|
351
|
-
overlapping: (annotations, range) => annotations.filter((annotation) => ManuscriptRange.overlaps(annotation.range, range)),
|
|
352
|
-
/**
|
|
353
|
-
* Stable order for rendering: outermost span first, then by variant name.
|
|
354
|
-
*/
|
|
355
|
-
compare: (left, right) => left.range.source.start - right.range.source.start || left.range.source.end - right.range.source.end || left.kind.localeCompare(right.kind)
|
|
356
|
-
};
|
|
357
|
-
//#endregion
|
|
358
|
-
//#region src/internal/describe-cause.ts
|
|
359
|
-
/**
|
|
360
|
-
* Renders an unknown throwable for display, without assuming it is an Error.
|
|
361
|
-
*/
|
|
362
|
-
function describeCause(cause) {
|
|
363
|
-
return cause instanceof Error ? cause.message : String(cause);
|
|
364
|
-
}
|
|
365
|
-
//#endregion
|
|
366
|
-
//#region src/parser/parse-error.ts
|
|
367
|
-
const ParseError = {
|
|
368
|
-
/**
|
|
369
|
-
* Human-readable rendering. Branch on `kind` instead when the caller needs to react.
|
|
370
|
-
*/
|
|
371
|
-
describe: (error) => {
|
|
372
|
-
switch (error.kind) {
|
|
373
|
-
case "InvalidParserId": return `パーサー ID "${error.parserId}" は namespace/name の形式ではありません`;
|
|
374
|
-
case "InvalidParserOutput": return `パーサー ${error.parserId} の出力が契約を満たしていません: ${error.issues.map(({ message }) => message).join(", ")}`;
|
|
375
|
-
case "ParserRejected": return `パーサー ${error.parserId} が解析を中止しました: ${error.reason}`;
|
|
376
|
-
case "ParserThrew": return `パーサー ${error.parserId} が例外を投げました: ${describeCause(error.cause)}`;
|
|
377
|
-
default: return assertNever(error);
|
|
378
|
-
}
|
|
379
|
-
} };
|
|
380
|
-
const ParsedGrapheme = { schema: readonlyObject({
|
|
381
|
-
value: v.string(),
|
|
382
|
-
range: ManuscriptRange.schema
|
|
383
|
-
}) };
|
|
384
|
-
//#endregion
|
|
385
|
-
//#region src/parser/parsed-manuscript.ts
|
|
386
|
-
const ParsedManuscriptSchema = readonlyObject({
|
|
387
|
-
source: v.string(),
|
|
388
|
-
displayText: v.string(),
|
|
389
|
-
graphemes: readonlyArray(ParsedGrapheme.schema),
|
|
390
|
-
annotations: readonlyArray(ManuscriptAnnotation.schema)
|
|
391
|
-
});
|
|
392
|
-
function rangeFits(range, manuscript) {
|
|
393
|
-
return range.source.end <= manuscript.source.length && range.display.end <= manuscript.displayText.length && range.graphemes.end <= manuscript.graphemes.length;
|
|
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
|
-
}
|
|
404
|
-
function isConsistent(manuscript, source) {
|
|
405
|
-
const joined = manuscript.graphemes.map(({ value }) => value).join("");
|
|
406
|
-
const sequential = manuscript.graphemes.every(({ range }, index) => range.graphemes.start === index && range.graphemes.end === index + 1 && rangeFits(range, manuscript));
|
|
407
|
-
return manuscript.source === source && manuscript.displayText === joined && sequential && manuscript.annotations.every(({ range }) => rangeFits(range, manuscript)) && hasValidRubyAssociations(manuscript);
|
|
408
|
-
}
|
|
409
|
-
const CONTRACT_MESSAGE = "parser output must contain consistent source, display, grapheme, and annotation ranges and valid ruby associations";
|
|
410
|
-
const ParsedManuscript = {
|
|
411
|
-
schema: ParsedManuscriptSchema,
|
|
412
|
-
/**
|
|
413
|
-
* The parser contract as a schema: shape, plus the cross-field invariants that make the three
|
|
414
|
-
* coordinate systems line up. Annotations come out in a stable order regardless of parser.
|
|
415
|
-
*/
|
|
416
|
-
contractFor: (source) => v.pipe(ParsedManuscriptSchema, v.check((manuscript) => isConsistent(manuscript, source), CONTRACT_MESSAGE), v.transform((manuscript) => ({
|
|
417
|
-
...manuscript,
|
|
418
|
-
annotations: [...manuscript.annotations].sort(ManuscriptAnnotation.compare)
|
|
419
|
-
})))
|
|
420
|
-
};
|
|
421
|
-
//#endregion
|
|
422
|
-
//#region src/internal/segmenter.ts
|
|
423
|
-
/**
|
|
424
|
-
* Shared across parsing and proofreading so both agree on what counts as one grapheme.
|
|
425
|
-
*/
|
|
426
|
-
const graphemeSegmenter = new Intl.Segmenter("ja", { granularity: "grapheme" });
|
|
5
|
+
const Rejection = { schema: readonlyObject({ reason: v.string() }) };
|
|
427
6
|
//#endregion
|
|
428
7
|
//#region src/parser/internal/manuscript-draft.ts
|
|
429
8
|
/**
|
|
@@ -550,6 +129,21 @@ function parseManuscript(source, options = {}) {
|
|
|
550
129
|
});
|
|
551
130
|
}
|
|
552
131
|
//#endregion
|
|
132
|
+
//#region src/parser/parse-error.ts
|
|
133
|
+
const ParseError = {
|
|
134
|
+
/**
|
|
135
|
+
* Human-readable rendering. Branch on `kind` instead when the caller needs to react.
|
|
136
|
+
*/
|
|
137
|
+
describe: (error) => {
|
|
138
|
+
switch (error.kind) {
|
|
139
|
+
case "InvalidParserId": return `パーサー ID "${error.parserId}" は namespace/name の形式ではありません`;
|
|
140
|
+
case "InvalidParserOutput": return `パーサー ${error.parserId} の出力が契約を満たしていません: ${error.issues.map(({ message }) => message).join(", ")}`;
|
|
141
|
+
case "ParserRejected": return `パーサー ${error.parserId} が解析を中止しました: ${error.reason}`;
|
|
142
|
+
case "ParserThrew": return `パーサー ${error.parserId} が例外を投げました: ${describeCause(error.cause)}`;
|
|
143
|
+
default: return assertNever(error);
|
|
144
|
+
}
|
|
145
|
+
} };
|
|
146
|
+
//#endregion
|
|
553
147
|
//#region src/parser/internal/line-end.ts
|
|
554
148
|
/**
|
|
555
149
|
* Returns the first line terminator at or after start. Both halves of CRLF remain outside the line.
|
|
@@ -934,26 +528,6 @@ const pixivParser = {
|
|
|
934
528
|
}
|
|
935
529
|
};
|
|
936
530
|
//#endregion
|
|
937
|
-
//#region src/composer/compose-error.ts
|
|
938
|
-
const ComposeError = {
|
|
939
|
-
/**
|
|
940
|
-
* Human-readable rendering. Branch on `kind` instead when the caller needs to react.
|
|
941
|
-
*/
|
|
942
|
-
describe: (error) => {
|
|
943
|
-
switch (error.kind) {
|
|
944
|
-
case "InvalidComposerId": return `組版エンジン ID "${error.composerId}" は namespace/name の形式ではありません`;
|
|
945
|
-
case "InvalidManuscript": return `解析済み原稿が不正です: ${joinIssues$1(error.issues)}`;
|
|
946
|
-
case "InvalidSettings": return `組版設定が ${error.composerId} の要求を満たしていません: ${joinIssues$1(error.issues)}`;
|
|
947
|
-
case "InvalidComposerOutput": return `組版エンジン ${error.composerId} の出力が契約を満たしていません: ${joinIssues$1(error.issues)}`;
|
|
948
|
-
case "ComposerRejected": return `組版エンジン ${error.composerId} が組版を中止しました: ${error.reason}`;
|
|
949
|
-
case "ComposerThrew": return `組版エンジン ${error.composerId} が例外を投げました: ${describeCause(error.cause)}`;
|
|
950
|
-
default: return assertNever(error);
|
|
951
|
-
}
|
|
952
|
-
} };
|
|
953
|
-
function joinIssues$1(issues) {
|
|
954
|
-
return issues.map(({ message }) => message).join(", ");
|
|
955
|
-
}
|
|
956
|
-
//#endregion
|
|
957
531
|
//#region src/composer/compose-manuscript.ts
|
|
958
532
|
function composeManuscript(parsed, options) {
|
|
959
533
|
const { composer } = options;
|
|
@@ -1003,2041 +577,42 @@ function composeManuscript(parsed, options) {
|
|
|
1003
577
|
}, result.warnings);
|
|
1004
578
|
}
|
|
1005
579
|
//#endregion
|
|
1006
|
-
//#region src/composer/
|
|
1007
|
-
const
|
|
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
|
|
1023
|
-
};
|
|
1024
|
-
const ComposedAnnotationFragment = { schema: v.variant("kind", [
|
|
1025
|
-
readonlyObject({
|
|
1026
|
-
kind: v.literal("bold"),
|
|
1027
|
-
...common$1
|
|
1028
|
-
}),
|
|
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
|
-
]) };
|
|
1048
|
-
const LineOffset = {
|
|
1049
|
-
schema: readonlyObject({
|
|
1050
|
-
leading: nonNegativeInteger(),
|
|
1051
|
-
trailing: nonNegativeInteger()
|
|
1052
|
-
}),
|
|
1053
|
-
total: (offset) => offset.leading + offset.trailing
|
|
1054
|
-
};
|
|
1055
|
-
//#endregion
|
|
1056
|
-
//#region src/composer/manuscript-offsets.ts
|
|
1057
|
-
const MAX_DOCUMENT_OFFSET = 1e4;
|
|
1058
|
-
const ManuscriptOffsets = {
|
|
1059
|
-
schema: readonlyObject({
|
|
1060
|
-
document: readonlyObject({
|
|
1061
|
-
leading: nonNegativeInteger(MAX_DOCUMENT_OFFSET),
|
|
1062
|
-
trailing: nonNegativeInteger(MAX_DOCUMENT_OFFSET)
|
|
1063
|
-
}),
|
|
1064
|
-
page: LineOffset.schema,
|
|
1065
|
-
stage: LineOffset.schema
|
|
1066
|
-
}),
|
|
1067
|
-
maxDocumentOffset: MAX_DOCUMENT_OFFSET,
|
|
1068
|
-
defaults: {
|
|
1069
|
-
document: {
|
|
1070
|
-
leading: 0,
|
|
1071
|
-
trailing: 0
|
|
1072
|
-
},
|
|
1073
|
-
page: {
|
|
1074
|
-
leading: 0,
|
|
1075
|
-
trailing: 0
|
|
1076
|
-
},
|
|
1077
|
-
stage: {
|
|
1078
|
-
leading: 0,
|
|
1079
|
-
trailing: 0
|
|
1080
|
-
}
|
|
1081
|
-
},
|
|
1082
|
-
/**
|
|
1083
|
-
* Stage offsets must leave at least one usable line in the stage.
|
|
1084
|
-
*/
|
|
1085
|
-
maxStageTotal: (flow) => Math.max(0, flow.linesPerStage - 1),
|
|
1086
|
-
/**
|
|
1087
|
-
* Page offsets must leave at least one usable line on the page, after stage offsets apply.
|
|
1088
|
-
*/
|
|
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
|
-
}
|
|
1120
|
-
};
|
|
1121
|
-
//#endregion
|
|
1122
|
-
//#region src/composer/composition-settings.ts
|
|
1123
|
-
const NovelCompositionSettingsSchema = v.pipe(readonlyObject({
|
|
1124
|
-
flow: NovelFlowSettings.schema,
|
|
1125
|
-
offsets: ManuscriptOffsets.schema,
|
|
1126
|
-
appearance: ManuscriptAppearanceSettings.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,
|
|
1130
|
-
defaults: {
|
|
1131
|
-
flow: NovelFlowSettings.defaults,
|
|
1132
|
-
offsets: ManuscriptOffsets.defaults,
|
|
1133
|
-
appearance: ManuscriptAppearanceSettings.defaults
|
|
1134
|
-
},
|
|
1135
|
-
is: (value) => v.is(NovelCompositionSettingsSchema, value)
|
|
1136
|
-
};
|
|
1137
|
-
const CompositionStatistics = { schema: readonlyObject({
|
|
1138
|
-
chars: nonNegativeInteger(),
|
|
1139
|
-
sourceLines: nonNegativeInteger(),
|
|
1140
|
-
pages: nonNegativeInteger()
|
|
1141
|
-
}) };
|
|
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
|
|
1153
|
-
}),
|
|
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
|
|
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
|
-
}
|
|
580
|
+
//#region src/composer/compose-error.ts
|
|
581
|
+
const ComposeError = {
|
|
1272
582
|
/**
|
|
1273
|
-
*
|
|
583
|
+
* Human-readable rendering. Branch on `kind` instead when the caller needs to react.
|
|
1274
584
|
*/
|
|
1275
|
-
|
|
1276
|
-
|
|
585
|
+
describe: (error) => {
|
|
586
|
+
switch (error.kind) {
|
|
587
|
+
case "InvalidComposerId": return `組版エンジン ID "${error.composerId}" は namespace/name の形式ではありません`;
|
|
588
|
+
case "InvalidManuscript": return `解析済み原稿が不正です: ${joinIssues(error.issues)}`;
|
|
589
|
+
case "InvalidSettings": return `組版設定が ${error.composerId} の要求を満たしていません: ${joinIssues(error.issues)}`;
|
|
590
|
+
case "InvalidComposerOutput": return `組版エンジン ${error.composerId} の出力が契約を満たしていません: ${joinIssues(error.issues)}`;
|
|
591
|
+
case "ComposerRejected": return `組版エンジン ${error.composerId} が組版を中止しました: ${error.reason}`;
|
|
592
|
+
case "ComposerThrew": return `組版エンジン ${error.composerId} が例外を投げました: ${describeCause(error.cause)}`;
|
|
593
|
+
default: return assertNever(error);
|
|
594
|
+
}
|
|
595
|
+
} };
|
|
596
|
+
function joinIssues(issues) {
|
|
597
|
+
return issues.map(({ message }) => message).join(", ");
|
|
1277
598
|
}
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
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
|
-
};
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/appearance/font-preset.ts
|
|
601
|
+
const FONT_PRESETS = {
|
|
602
|
+
mincho: {
|
|
603
|
+
id: "mincho",
|
|
604
|
+
label: "明朝",
|
|
605
|
+
family: "\"Yu Mincho\", \"Hiragino Mincho ProN\", \"Hiragino Mincho Pro\", serif"
|
|
1299
606
|
},
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
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));
|
|
607
|
+
gothic: {
|
|
608
|
+
id: "gothic",
|
|
609
|
+
label: "ゴシック",
|
|
610
|
+
family: "\"Yu Gothic\", \"Hiragino Kaku Gothic ProN\", system-ui, sans-serif"
|
|
1309
611
|
}
|
|
1310
612
|
};
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
* Brackets and quotes a Japanese paragraph may open with. Halfwidth forms are included because a
|
|
1315
|
-
* manuscript that uses them is still opening a paragraph, not writing prose.
|
|
1316
|
-
*/
|
|
1317
|
-
const OPENING_BRACKETS = "「『〖〈《((“\"‘'[[〔{{<<";
|
|
1318
|
-
/**
|
|
1319
|
-
* The closing counterparts, in the same order.
|
|
1320
|
-
*/
|
|
1321
|
-
const CLOSING_BRACKETS = "」』〗〉》))”\"’']]〕}}>>";
|
|
1322
|
-
/**
|
|
1323
|
-
* Escapes the characters a regular expression character class would otherwise read as syntax, so a
|
|
1324
|
-
* set declared above can be dropped into a pattern.
|
|
1325
|
-
*/
|
|
1326
|
-
function characterClass(characters) {
|
|
1327
|
-
return characters.replaceAll(/[\\\]^-]/gu, "\\$&");
|
|
1328
|
-
}
|
|
1329
|
-
//#endregion
|
|
1330
|
-
//#region src/internal/japanese-spaces.ts
|
|
1331
|
-
const LEADING_SPACES = /^[ \t]+/u;
|
|
1332
|
-
/**
|
|
1333
|
-
* The run of spaces the text begins with, or an empty string. Halfwidth spaces and tabs count: a
|
|
1334
|
-
* rule has to see them to say they are the wrong space.
|
|
1335
|
-
*/
|
|
1336
|
-
function leadingSpaces(text) {
|
|
1337
|
-
return LEADING_SPACES.exec(text)?.[0] ?? "";
|
|
1338
|
-
}
|
|
1339
|
-
//#endregion
|
|
1340
|
-
//#region src/internal/question-or-exclamation-spacing.ts
|
|
1341
|
-
/**
|
|
1342
|
-
* Runs, not single marks: `!?` is one mark and takes one gap, so the gap belongs after the run.
|
|
1343
|
-
*/
|
|
1344
|
-
const MARKS = /[?!]+/gu;
|
|
1345
|
-
function analyzeMatch(line, marks) {
|
|
1346
|
-
const start = marks.index;
|
|
1347
|
-
if (start === void 0) return void 0;
|
|
1348
|
-
const marksRange = {
|
|
1349
|
-
start,
|
|
1350
|
-
end: start + marks[0].length
|
|
1351
|
-
};
|
|
1352
|
-
const next = line[marksRange.end];
|
|
1353
|
-
if (next === void 0 || "」』〗〉》))”\"’']]〕}}>>".includes(next)) return void 0;
|
|
1354
|
-
const spaces = leadingSpaces(line.slice(marksRange.end));
|
|
1355
|
-
if (spaces === "") return {
|
|
1356
|
-
kind: "missing",
|
|
1357
|
-
marks: marksRange
|
|
1358
|
-
};
|
|
1359
|
-
const gap = {
|
|
1360
|
-
start: marksRange.end,
|
|
1361
|
-
end: marksRange.end + spaces.length
|
|
1362
|
-
};
|
|
1363
|
-
const following = line[gap.end];
|
|
1364
|
-
if (following !== void 0 && "」』〗〉》))”\"’']]〕}}>>".includes(following)) return {
|
|
1365
|
-
kind: "before-closing-bracket",
|
|
1366
|
-
marks: marksRange,
|
|
1367
|
-
gap
|
|
1368
|
-
};
|
|
1369
|
-
if (spaces !== " ") return {
|
|
1370
|
-
kind: "invalid-space",
|
|
1371
|
-
marks: marksRange,
|
|
1372
|
-
gap
|
|
1373
|
-
};
|
|
1374
|
-
return {
|
|
1375
|
-
kind: "valid",
|
|
1376
|
-
marks: marksRange,
|
|
1377
|
-
gap
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
/**
|
|
1381
|
-
* Classifies the spacing after each run of fullwidth question or exclamation marks in one display
|
|
1382
|
-
* line. Runs at the end of the line or immediately before a closing bracket need no gap and are
|
|
1383
|
-
* omitted from the result.
|
|
1384
|
-
*/
|
|
1385
|
-
function questionOrExclamationSpacings(line) {
|
|
1386
|
-
return [...line.matchAll(MARKS)].map((marks) => analyzeMatch(line, marks)).filter((spacing) => spacing !== void 0);
|
|
1387
|
-
}
|
|
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: 0,
|
|
1489
|
-
amountEm: .5
|
|
1490
|
-
}
|
|
1491
|
-
} : null,
|
|
1492
|
-
breakPenalty,
|
|
1493
|
-
canHang: (characterClass) => characterClass === "cl-07" || characterClass === "cl-06"
|
|
613
|
+
const FontPreset = {
|
|
614
|
+
of: (id) => FONT_PRESETS[id],
|
|
615
|
+
all: Object.values(FONT_PRESETS)
|
|
1494
616
|
};
|
|
1495
617
|
//#endregion
|
|
1496
|
-
|
|
1497
|
-
const EPSILON = 1e-9;
|
|
1498
|
-
/**
|
|
1499
|
-
* Spending capacity at this priority changes nothing the reader can see.
|
|
1500
|
-
*/
|
|
1501
|
-
const FREE_SHRINK_PRIORITY = 0;
|
|
1502
|
-
function skipSourceGaps(atoms, start) {
|
|
1503
|
-
let cursor = start;
|
|
1504
|
-
while (atoms[cursor]?.sourceGap === true) cursor += 1;
|
|
1505
|
-
return cursor;
|
|
1506
|
-
}
|
|
1507
|
-
function previousVisible(atoms, end) {
|
|
1508
|
-
for (let index = end - 1; index >= 0; index -= 1) if (atoms[index]?.sourceGap === false) return index;
|
|
1509
|
-
}
|
|
1510
|
-
function opportunities(atoms, classes, start, end, profile) {
|
|
1511
|
-
const result = [];
|
|
1512
|
-
const firstClass = classes[start];
|
|
1513
|
-
const startSpacing = firstClass === void 0 ? null : profile.lineStartSpacing(firstClass);
|
|
1514
|
-
if (startSpacing !== null) result.push({
|
|
1515
|
-
boundary: start,
|
|
1516
|
-
spacing: startSpacing
|
|
1517
|
-
});
|
|
1518
|
-
for (let right = start + 1; right < end; right += 1) {
|
|
1519
|
-
const leftAtom = atoms[right - 1];
|
|
1520
|
-
const rightAtom = atoms[right];
|
|
1521
|
-
const leftClass = classes[right - 1];
|
|
1522
|
-
const rightClass = classes[right];
|
|
1523
|
-
if (leftAtom === void 0 || rightAtom === void 0 || leftClass === void 0 || rightClass === void 0 || leftAtom.sourceGap || rightAtom.sourceGap || !leftAtom.pairSpacingAfter) continue;
|
|
1524
|
-
result.push({
|
|
1525
|
-
boundary: right,
|
|
1526
|
-
spacing: profile.pairSpacing(leftClass, rightClass)
|
|
1527
|
-
});
|
|
1528
|
-
}
|
|
1529
|
-
const lastClass = classes[end - 1];
|
|
1530
|
-
const endSpacing = lastClass === void 0 ? null : profile.lineEndSpacing(lastClass);
|
|
1531
|
-
if (endSpacing !== null) result.push({
|
|
1532
|
-
boundary: end,
|
|
1533
|
-
spacing: endSpacing
|
|
1534
|
-
});
|
|
1535
|
-
return result;
|
|
1536
|
-
}
|
|
1537
|
-
function capacity(values, select) {
|
|
1538
|
-
return values.reduce((total, opportunity) => total + (select(opportunity.spacing)?.amountEm ?? 0), 0);
|
|
1539
|
-
}
|
|
1540
|
-
function resolveSpacings(values, adjustment, amountEm) {
|
|
1541
|
-
const remainingByBoundary = /* @__PURE__ */ new Map();
|
|
1542
|
-
let remaining = amountEm;
|
|
1543
|
-
let priorityCost = 0;
|
|
1544
|
-
const capacities = values.flatMap((opportunity) => {
|
|
1545
|
-
const selected = adjustment === "shrink" ? opportunity.spacing.shrink : opportunity.spacing.stretch;
|
|
1546
|
-
return selected === void 0 ? [] : [{
|
|
1547
|
-
...opportunity,
|
|
1548
|
-
capacity: selected
|
|
1549
|
-
}];
|
|
1550
|
-
}).sort((left, right) => left.capacity.priority - right.capacity.priority || left.boundary - right.boundary);
|
|
1551
|
-
for (const opportunity of capacities) {
|
|
1552
|
-
if (remaining <= EPSILON) break;
|
|
1553
|
-
const used = Math.min(remaining, opportunity.capacity.amountEm);
|
|
1554
|
-
remainingByBoundary.set(opportunity.boundary, used);
|
|
1555
|
-
priorityCost += used * opportunity.capacity.priority;
|
|
1556
|
-
remaining -= used;
|
|
1557
|
-
}
|
|
1558
|
-
return {
|
|
1559
|
-
spacings: values.map(({ boundary, spacing }) => {
|
|
1560
|
-
const used = remainingByBoundary.get(boundary) ?? 0;
|
|
1561
|
-
return {
|
|
1562
|
-
boundary,
|
|
1563
|
-
kind: spacing.kind,
|
|
1564
|
-
naturalWidthEm: spacing.naturalWidthEm,
|
|
1565
|
-
widthEm: adjustment === "shrink" ? spacing.naturalWidthEm - used : spacing.naturalWidthEm + used
|
|
1566
|
-
};
|
|
1567
|
-
}),
|
|
1568
|
-
priorityCost
|
|
1569
|
-
};
|
|
1570
|
-
}
|
|
1571
|
-
function candidate(atoms, classes, start, end, lineLengthEm, profile) {
|
|
1572
|
-
const contentStart = skipSourceGaps(atoms, start);
|
|
1573
|
-
const suppressedIndexes = Array.from({ length: contentStart - start }, (_, index) => start + index);
|
|
1574
|
-
const pairValues = opportunities(atoms, classes, contentStart, end, profile);
|
|
1575
|
-
const naturalSizeEm = atoms.slice(contentStart, end).reduce((total, atom) => total + atom.boxAdvanceEm, 0) + pairValues.reduce((total, value) => total + value.spacing.naturalWidthEm, 0);
|
|
1576
|
-
const terminal = skipSourceGaps(atoms, end) === atoms.length;
|
|
1577
|
-
const shrinkCapacity = capacity(pairValues, ({ shrink }) => shrink);
|
|
1578
|
-
const freeShrinkCapacity = capacity(pairValues, ({ shrink }) => shrink?.priority === FREE_SHRINK_PRIORITY ? shrink : void 0);
|
|
1579
|
-
const stretchCapacity = capacity(pairValues, ({ stretch }) => stretch);
|
|
1580
|
-
const overflow = naturalSizeEm - lineLengthEm;
|
|
1581
|
-
const underflow = lineLengthEm - naturalSizeEm;
|
|
1582
|
-
const lastVisible = previousVisible(atoms, end);
|
|
1583
|
-
const lastClass = lastVisible === void 0 ? void 0 : classes[lastVisible];
|
|
1584
|
-
const lastAdvance = lastVisible === void 0 ? 0 : atoms[lastVisible]?.boxAdvanceEm ?? 0;
|
|
1585
|
-
const trailingSpacing = pairValues.find(({ boundary }) => boundary === end)?.spacing.naturalWidthEm ?? 0;
|
|
1586
|
-
if (terminal && overflow <= EPSILON) return {
|
|
1587
|
-
start,
|
|
1588
|
-
contentStart,
|
|
1589
|
-
end,
|
|
1590
|
-
suppressedIndexes,
|
|
1591
|
-
pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings,
|
|
1592
|
-
inlineSizeEm: naturalSizeEm,
|
|
1593
|
-
break: { kind: "paragraph-end" },
|
|
1594
|
-
hangingIndex: null,
|
|
1595
|
-
deformationRatio: 0,
|
|
1596
|
-
priorityCost: 0
|
|
1597
|
-
};
|
|
1598
|
-
if (Math.abs(overflow) <= EPSILON) return {
|
|
1599
|
-
start,
|
|
1600
|
-
contentStart,
|
|
1601
|
-
end,
|
|
1602
|
-
suppressedIndexes,
|
|
1603
|
-
pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings,
|
|
1604
|
-
inlineSizeEm: naturalSizeEm,
|
|
1605
|
-
break: { kind: "natural" },
|
|
1606
|
-
hangingIndex: null,
|
|
1607
|
-
deformationRatio: 0,
|
|
1608
|
-
priorityCost: 0
|
|
1609
|
-
};
|
|
1610
|
-
if (overflow > 0 && overflow <= shrinkCapacity + EPSILON) {
|
|
1611
|
-
const resolved = resolveSpacings(pairValues, "shrink", overflow);
|
|
1612
|
-
const chargeableCapacity = shrinkCapacity - freeShrinkCapacity;
|
|
1613
|
-
const chargeableShrink = Math.max(0, overflow - freeShrinkCapacity);
|
|
1614
|
-
return {
|
|
1615
|
-
start,
|
|
1616
|
-
contentStart,
|
|
1617
|
-
end,
|
|
1618
|
-
suppressedIndexes,
|
|
1619
|
-
pairSpacings: resolved.spacings,
|
|
1620
|
-
inlineSizeEm: lineLengthEm,
|
|
1621
|
-
break: { kind: "shrunk" },
|
|
1622
|
-
hangingIndex: null,
|
|
1623
|
-
deformationRatio: chargeableCapacity <= EPSILON ? 0 : chargeableShrink / chargeableCapacity,
|
|
1624
|
-
priorityCost: resolved.priorityCost
|
|
1625
|
-
};
|
|
1626
|
-
}
|
|
1627
|
-
if (!terminal && overflow > 0 && lastVisible !== void 0 && lastClass !== void 0 && profile.canHang(lastClass) && naturalSizeEm - lastAdvance - trailingSpacing <= lineLengthEm + EPSILON) return {
|
|
1628
|
-
start,
|
|
1629
|
-
contentStart,
|
|
1630
|
-
end,
|
|
1631
|
-
suppressedIndexes,
|
|
1632
|
-
pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings.filter(({ boundary }) => boundary !== end),
|
|
1633
|
-
inlineSizeEm: naturalSizeEm - lastAdvance - trailingSpacing,
|
|
1634
|
-
break: { kind: "hanging" },
|
|
1635
|
-
hangingIndex: lastVisible,
|
|
1636
|
-
deformationRatio: overflow / Math.max(lastAdvance, EPSILON),
|
|
1637
|
-
priorityCost: 0
|
|
1638
|
-
};
|
|
1639
|
-
if (!terminal && underflow > EPSILON && underflow <= stretchCapacity + EPSILON) {
|
|
1640
|
-
const resolved = resolveSpacings(pairValues, "stretch", underflow);
|
|
1641
|
-
return {
|
|
1642
|
-
start,
|
|
1643
|
-
contentStart,
|
|
1644
|
-
end,
|
|
1645
|
-
suppressedIndexes,
|
|
1646
|
-
pairSpacings: resolved.spacings,
|
|
1647
|
-
inlineSizeEm: lineLengthEm,
|
|
1648
|
-
break: { kind: "stretched" },
|
|
1649
|
-
hangingIndex: null,
|
|
1650
|
-
deformationRatio: stretchCapacity === 0 ? 0 : underflow / stretchCapacity,
|
|
1651
|
-
priorityCost: resolved.priorityCost
|
|
1652
|
-
};
|
|
1653
|
-
}
|
|
1654
|
-
return {
|
|
1655
|
-
start,
|
|
1656
|
-
contentStart,
|
|
1657
|
-
end,
|
|
1658
|
-
suppressedIndexes,
|
|
1659
|
-
pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings,
|
|
1660
|
-
inlineSizeEm: naturalSizeEm,
|
|
1661
|
-
break: { kind: "forced" },
|
|
1662
|
-
hangingIndex: null,
|
|
1663
|
-
deformationRatio: Math.abs(naturalSizeEm - lineLengthEm) / Math.max(lineLengthEm, EPSILON),
|
|
1664
|
-
priorityCost: 0
|
|
1665
|
-
};
|
|
1666
|
-
}
|
|
1667
|
-
function scoreFor(line, previousFitness) {
|
|
1668
|
-
const mode = line.break.kind;
|
|
1669
|
-
const fitness = Math.min(3, Math.floor(line.deformationRatio * 4));
|
|
1670
|
-
const transition = Math.abs(previousFitness - fitness) > 1 ? 1 : 0;
|
|
1671
|
-
const modeCounts = {
|
|
1672
|
-
forced: mode === "forced" ? 1 : 0,
|
|
1673
|
-
stretched: mode === "stretched" ? 1 : 0,
|
|
1674
|
-
hanging: mode === "hanging" ? 1 : 0,
|
|
1675
|
-
shrunk: mode === "shrunk" && line.priorityCost > EPSILON ? 1 : 0
|
|
1676
|
-
};
|
|
1677
|
-
return {
|
|
1678
|
-
score: [
|
|
1679
|
-
modeCounts.forced,
|
|
1680
|
-
modeCounts.stretched,
|
|
1681
|
-
modeCounts.hanging,
|
|
1682
|
-
modeCounts.shrunk,
|
|
1683
|
-
line.priorityCost,
|
|
1684
|
-
line.deformationRatio ** 3,
|
|
1685
|
-
transition
|
|
1686
|
-
],
|
|
1687
|
-
fitness
|
|
1688
|
-
};
|
|
1689
|
-
}
|
|
1690
|
-
function addScore(left, right) {
|
|
1691
|
-
return [
|
|
1692
|
-
left[0] + right[0],
|
|
1693
|
-
left[1] + right[1],
|
|
1694
|
-
left[2] + right[2],
|
|
1695
|
-
left[3] + right[3],
|
|
1696
|
-
left[4] + right[4],
|
|
1697
|
-
left[5] + right[5],
|
|
1698
|
-
left[6] + right[6]
|
|
1699
|
-
];
|
|
1700
|
-
}
|
|
1701
|
-
function compareBreaks(left, right) {
|
|
1702
|
-
const length = Math.min(left.length, right.length);
|
|
1703
|
-
for (let index = 0; index < length; index += 1) {
|
|
1704
|
-
const difference = (right[index] ?? 0) - (left[index] ?? 0);
|
|
1705
|
-
if (difference !== 0) return difference;
|
|
1706
|
-
}
|
|
1707
|
-
return left.length - right.length;
|
|
1708
|
-
}
|
|
1709
|
-
function isBetter(candidateState, current) {
|
|
1710
|
-
if (current === void 0) return true;
|
|
1711
|
-
for (let index = 0; index < candidateState.score.length; index += 1) {
|
|
1712
|
-
const difference = (candidateState.score[index] ?? 0) - (current.score[index] ?? 0);
|
|
1713
|
-
if (Math.abs(difference) > EPSILON) return difference < 0;
|
|
1714
|
-
}
|
|
1715
|
-
return compareBreaks(candidateState.breaks, current.breaks) < 0;
|
|
1716
|
-
}
|
|
1717
|
-
function layoutParagraph(atoms, lineLengthEm, profile, boundaryAllowed) {
|
|
1718
|
-
if (atoms.length === 0) return [];
|
|
1719
|
-
const classes = atoms.map(({ characterClass }) => characterClass);
|
|
1720
|
-
const states = /* @__PURE__ */ new Map([[0, /* @__PURE__ */ new Map([[0, {
|
|
1721
|
-
score: [
|
|
1722
|
-
0,
|
|
1723
|
-
0,
|
|
1724
|
-
0,
|
|
1725
|
-
0,
|
|
1726
|
-
0,
|
|
1727
|
-
0,
|
|
1728
|
-
0
|
|
1729
|
-
],
|
|
1730
|
-
fitness: 0,
|
|
1731
|
-
previous: null,
|
|
1732
|
-
line: null,
|
|
1733
|
-
breaks: []
|
|
1734
|
-
}]])]]);
|
|
1735
|
-
for (let start = 0; start < atoms.length; start += 1) {
|
|
1736
|
-
const activeStates = states.get(start);
|
|
1737
|
-
if (activeStates === void 0) continue;
|
|
1738
|
-
const contentStart = skipSourceGaps(atoms, start);
|
|
1739
|
-
if (contentStart === atoms.length) {
|
|
1740
|
-
const terminalStates = states.get(atoms.length) ?? /* @__PURE__ */ new Map();
|
|
1741
|
-
for (const state of activeStates.values()) if (isBetter(state, terminalStates.get(state.fitness))) terminalStates.set(state.fitness, state);
|
|
1742
|
-
states.set(atoms.length, terminalStates);
|
|
1743
|
-
continue;
|
|
1744
|
-
}
|
|
1745
|
-
for (const state of activeStates.values()) for (let end = contentStart + 1; end <= atoms.length; end += 1) {
|
|
1746
|
-
const right = skipSourceGaps(atoms, end);
|
|
1747
|
-
const left = previousVisible(atoms, end);
|
|
1748
|
-
if (atoms[end - 1]?.sourceGap === true) continue;
|
|
1749
|
-
if (end < atoms.length && right < atoms.length && (left === void 0 || !boundaryAllowed(left, right))) continue;
|
|
1750
|
-
const line = candidate(atoms, classes, start, end, lineLengthEm, profile);
|
|
1751
|
-
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))) {
|
|
1752
|
-
if (line.inlineSizeEm > lineLengthEm * 2 && end > contentStart + 1) break;
|
|
1753
|
-
continue;
|
|
1754
|
-
}
|
|
1755
|
-
const lineScore = scoreFor(line, state.fitness);
|
|
1756
|
-
const nextState = {
|
|
1757
|
-
score: addScore(state.score, lineScore.score),
|
|
1758
|
-
fitness: lineScore.fitness,
|
|
1759
|
-
previous: {
|
|
1760
|
-
index: start,
|
|
1761
|
-
fitness: state.fitness
|
|
1762
|
-
},
|
|
1763
|
-
line,
|
|
1764
|
-
breaks: [...state.breaks, end]
|
|
1765
|
-
};
|
|
1766
|
-
const statesAtEnd = states.get(end) ?? /* @__PURE__ */ new Map();
|
|
1767
|
-
if (isBetter(nextState, statesAtEnd.get(nextState.fitness))) {
|
|
1768
|
-
statesAtEnd.set(nextState.fitness, nextState);
|
|
1769
|
-
states.set(end, statesAtEnd);
|
|
1770
|
-
}
|
|
1771
|
-
if (line.inlineSizeEm > lineLengthEm * 2 && line.break.kind === "forced" && end > contentStart + 1) break;
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
const terminal = [...states.get(atoms.length)?.values() ?? []].reduce((best, state) => isBetter(state, best) ? state : best, void 0);
|
|
1775
|
-
if (terminal === void 0) return [];
|
|
1776
|
-
const lines = [];
|
|
1777
|
-
let cursor = terminal;
|
|
1778
|
-
while (cursor?.line !== null && cursor?.line !== void 0) {
|
|
1779
|
-
lines.push(cursor.line);
|
|
1780
|
-
cursor = cursor.previous === null ? void 0 : states.get(cursor.previous.index)?.get(cursor.previous.fitness);
|
|
1781
|
-
}
|
|
1782
|
-
lines.reverse();
|
|
1783
|
-
const last = lines.at(-1);
|
|
1784
|
-
if (last !== void 0 && last.end < atoms.length) lines[lines.length - 1] = {
|
|
1785
|
-
...last,
|
|
1786
|
-
suppressedIndexes: [...last.suppressedIndexes, ...Array.from({ length: atoms.length - last.end }, (_, index) => last.end + index)]
|
|
1787
|
-
};
|
|
1788
|
-
return lines;
|
|
1789
|
-
}
|
|
1790
|
-
const NovelLine = {
|
|
1791
|
-
schema: readonlyObject({
|
|
1792
|
-
range: v.nullable(ManuscriptRange.schema),
|
|
1793
|
-
inlineSizeEm: v.pipe(v.number(), v.finite(), v.minValue(0)),
|
|
1794
|
-
items: readonlyArray(ComposedInlineItem.schema),
|
|
1795
|
-
break: LineBreakResult.schema,
|
|
1796
|
-
annotations: readonlyArray(ComposedAnnotationFragment.schema)
|
|
1797
|
-
}),
|
|
1798
|
-
empty: () => ({
|
|
1799
|
-
range: null,
|
|
1800
|
-
inlineSizeEm: 0,
|
|
1801
|
-
items: [],
|
|
1802
|
-
break: { kind: "paragraph-end" },
|
|
1803
|
-
annotations: []
|
|
1804
|
-
})
|
|
1805
|
-
};
|
|
1806
|
-
const NovelStage = {
|
|
1807
|
-
schema: readonlyObject({
|
|
1808
|
-
range: v.nullable(ManuscriptRange.schema),
|
|
1809
|
-
lines: readonlyArray(NovelLine.schema)
|
|
1810
|
-
}),
|
|
1811
|
-
of: (lines) => ({
|
|
1812
|
-
lines,
|
|
1813
|
-
range: ManuscriptRange.merge(lines.map(({ range }) => range))
|
|
1814
|
-
})
|
|
1815
|
-
};
|
|
1816
|
-
const NovelPage = {
|
|
1817
|
-
schema: readonlyObject({
|
|
1818
|
-
range: v.nullable(ManuscriptRange.schema),
|
|
1819
|
-
stages: readonlyArray(NovelStage.schema)
|
|
1820
|
-
}),
|
|
1821
|
-
of: (stages) => ({
|
|
1822
|
-
stages,
|
|
1823
|
-
range: ManuscriptRange.merge(stages.map(({ range }) => range))
|
|
1824
|
-
})
|
|
1825
|
-
};
|
|
1826
|
-
const NovelLayout = { schema: readonlyObject({
|
|
1827
|
-
pages: readonlyArray(NovelPage.schema),
|
|
1828
|
-
geometry: ManuscriptGeometry.schema,
|
|
1829
|
-
stats: CompositionStatistics.schema
|
|
1830
|
-
}) };
|
|
1831
|
-
//#endregion
|
|
1832
|
-
//#region src/composer/novel-composer.ts
|
|
1833
|
-
const COMPOSER_ID = NamespacedId.of("kg/novel");
|
|
1834
|
-
const TYPESETTING_PROFILE = defaultJapaneseTypesettingProfile;
|
|
1835
|
-
const ASCII_ALPHANUMERIC = /^[A-Za-z0-9]$/u;
|
|
1836
|
-
const ASCII_TWO_DIGITS = /^[0-9]{2}$/u;
|
|
1837
|
-
const UPRIGHT_LATIN_ABBREVIATION = /^(?:[A-Z]+|[A-Z][a-z]{1,2})$/u;
|
|
1838
|
-
const FULLWIDTH_ALPHANUMERIC = /^[A-Za-z0-9]$/u;
|
|
1839
|
-
function presentation(kind, graphemes) {
|
|
1840
|
-
const groupRange = ManuscriptRange.merge(graphemes.map(({ range }) => range));
|
|
1841
|
-
return groupRange === null ? void 0 : {
|
|
1842
|
-
kind,
|
|
1843
|
-
groupRange
|
|
1844
|
-
};
|
|
1845
|
-
}
|
|
1846
|
-
function presentedSourceLine(sourceLine) {
|
|
1847
|
-
const presented = [];
|
|
1848
|
-
let cursor = 0;
|
|
1849
|
-
while (cursor < sourceLine.length) {
|
|
1850
|
-
const first = sourceLine[cursor];
|
|
1851
|
-
if (first === void 0) break;
|
|
1852
|
-
if (!ASCII_ALPHANUMERIC.test(first.value)) {
|
|
1853
|
-
const itemPresentation = presentation(FULLWIDTH_ALPHANUMERIC.test(first.value) ? "upright" : "mixed", [first]);
|
|
1854
|
-
if (itemPresentation !== void 0) presented.push({
|
|
1855
|
-
grapheme: first,
|
|
1856
|
-
presentation: itemPresentation
|
|
1857
|
-
});
|
|
1858
|
-
cursor += 1;
|
|
1859
|
-
continue;
|
|
1860
|
-
}
|
|
1861
|
-
const start = cursor;
|
|
1862
|
-
while (ASCII_ALPHANUMERIC.test(sourceLine[cursor]?.value ?? "")) cursor += 1;
|
|
1863
|
-
const run = sourceLine.slice(start, cursor);
|
|
1864
|
-
const text = run.map(({ value }) => value).join("");
|
|
1865
|
-
const runPresentation = presentation(ASCII_TWO_DIGITS.test(text) ? "tate-chu-yoko" : text.length === 1 || UPRIGHT_LATIN_ABBREVIATION.test(text) ? "upright" : "sideways", run);
|
|
1866
|
-
if (runPresentation !== void 0) presented.push(...run.map((grapheme) => ({
|
|
1867
|
-
grapheme,
|
|
1868
|
-
presentation: runPresentation
|
|
1869
|
-
})));
|
|
1870
|
-
}
|
|
1871
|
-
return presented;
|
|
1872
|
-
}
|
|
1873
|
-
function displayedLines(graphemes) {
|
|
1874
|
-
const lines = [[]];
|
|
1875
|
-
let endedWithLineBreak = false;
|
|
1876
|
-
for (const grapheme of graphemes) {
|
|
1877
|
-
if (grapheme.value === "\n" || grapheme.value === "\r" || grapheme.value === "\r\n") {
|
|
1878
|
-
lines.push([]);
|
|
1879
|
-
endedWithLineBreak = true;
|
|
1880
|
-
continue;
|
|
1881
|
-
}
|
|
1882
|
-
lines.at(-1)?.push(grapheme);
|
|
1883
|
-
endedWithLineBreak = false;
|
|
1884
|
-
}
|
|
1885
|
-
if (lines.length > 1 && endedWithLineBreak) lines.pop();
|
|
1886
|
-
return lines;
|
|
1887
|
-
}
|
|
1888
|
-
function validGapIndexes(sourceLine) {
|
|
1889
|
-
const line = sourceLine.map(({ value }) => value).join("");
|
|
1890
|
-
const gapStarts = new Set(questionOrExclamationSpacings(line).filter((spacing) => spacing.kind === "valid").map(({ gap }) => gap.start));
|
|
1891
|
-
const indexes = /* @__PURE__ */ new Set();
|
|
1892
|
-
let offset = 0;
|
|
1893
|
-
for (const [index, grapheme] of sourceLine.entries()) {
|
|
1894
|
-
if (gapStarts.has(offset)) indexes.add(index);
|
|
1895
|
-
offset += grapheme.value.length;
|
|
1896
|
-
}
|
|
1897
|
-
return indexes;
|
|
1898
|
-
}
|
|
1899
|
-
function indicesInside(atoms, annotation) {
|
|
1900
|
-
return atoms.flatMap(({ grapheme }, index) => ManuscriptRange.overlaps(grapheme.range, annotation.range) ? [index] : []);
|
|
1901
|
-
}
|
|
1902
|
-
function measure(measurer, text, role, settings, presentationKind) {
|
|
1903
|
-
const context = {
|
|
1904
|
-
text,
|
|
1905
|
-
fontPreset: settings.appearance.fontPreset,
|
|
1906
|
-
writingMode: "vertical-rl"
|
|
1907
|
-
};
|
|
1908
|
-
const measurement = role === "base" ? measurer({
|
|
1909
|
-
...context,
|
|
1910
|
-
role,
|
|
1911
|
-
presentation: presentationKind ?? "mixed"
|
|
1912
|
-
}) : measurer({
|
|
1913
|
-
...context,
|
|
1914
|
-
role
|
|
1915
|
-
});
|
|
1916
|
-
const parsed = v.safeParse(InlineMeasurement.schema, measurement);
|
|
1917
|
-
if (!parsed.success) return void 0;
|
|
1918
|
-
const { advanceEm } = parsed.output;
|
|
1919
|
-
return advanceEm;
|
|
1920
|
-
}
|
|
1921
|
-
function measureSourceLine(sourceLine, annotations, settings, measurer) {
|
|
1922
|
-
const mutable = presentedSourceLine(sourceLine).map(({ grapheme, presentation }) => {
|
|
1923
|
-
const renderAdvanceEm = measure(measurer, grapheme.value, "base", settings, presentation.kind);
|
|
1924
|
-
if (renderAdvanceEm === void 0) return void 0;
|
|
1925
|
-
const characterClass = TYPESETTING_PROFILE.classify({
|
|
1926
|
-
value: grapheme.value,
|
|
1927
|
-
presentation: presentation.kind
|
|
1928
|
-
});
|
|
1929
|
-
const metrics = TYPESETTING_PROFILE.boxMetrics(characterClass, renderAdvanceEm);
|
|
1930
|
-
return {
|
|
1931
|
-
grapheme,
|
|
1932
|
-
boxAdvanceEm: metrics.advanceEm,
|
|
1933
|
-
renderAdvanceEm,
|
|
1934
|
-
renderOffsetEm: metrics.renderOffsetEm,
|
|
1935
|
-
presentation
|
|
1936
|
-
};
|
|
1937
|
-
});
|
|
1938
|
-
if (mutable.some((atom) => atom === void 0)) return void 0;
|
|
1939
|
-
const atoms = mutable.flatMap((atom) => atom === void 0 ? [] : [atom]);
|
|
1940
|
-
for (const annotation of annotations) {
|
|
1941
|
-
if (annotation.kind !== "ruby") continue;
|
|
1942
|
-
const indexes = indicesInside(atoms, annotation);
|
|
1943
|
-
if (indexes.length === 0) continue;
|
|
1944
|
-
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;
|
|
1945
|
-
if (annotation.reading.kind === "group") {
|
|
1946
|
-
const readingAdvance = measure(measurer, annotation.reading.text, "ruby", settings);
|
|
1947
|
-
if (readingAdvance === void 0) return void 0;
|
|
1948
|
-
const baseAdvance = indexes.reduce((total, index) => total + (atoms[index]?.boxAdvanceEm ?? 0), 0);
|
|
1949
|
-
const extra = Math.max(0, readingAdvance - baseAdvance) / indexes.length;
|
|
1950
|
-
for (const index of indexes) {
|
|
1951
|
-
const atom = atoms[index];
|
|
1952
|
-
if (atom !== void 0) atom.boxAdvanceEm += extra;
|
|
1953
|
-
}
|
|
1954
|
-
continue;
|
|
1955
|
-
}
|
|
1956
|
-
for (const [segmentIndex, index] of indexes.entries()) {
|
|
1957
|
-
const segment = annotation.reading.segments[segmentIndex];
|
|
1958
|
-
const atom = atoms[index];
|
|
1959
|
-
if (segment === void 0 || atom === void 0) continue;
|
|
1960
|
-
const readingAdvance = measure(measurer, segment, "ruby", settings);
|
|
1961
|
-
if (readingAdvance === void 0) return void 0;
|
|
1962
|
-
atom.boxAdvanceEm = Math.max(atom.boxAdvanceEm, readingAdvance);
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
return {
|
|
1966
|
-
atoms,
|
|
1967
|
-
suppressedIndexes: validGapIndexes(sourceLine)
|
|
1968
|
-
};
|
|
1969
|
-
}
|
|
1970
|
-
function breakInsideFittableGroupRuby(atoms, annotations, boundary, lineLengthEm) {
|
|
1971
|
-
const right = atoms[boundary];
|
|
1972
|
-
if (right === void 0) return false;
|
|
1973
|
-
const graphemeIndex = right.grapheme.range.graphemes.start;
|
|
1974
|
-
return annotations.some((annotation) => {
|
|
1975
|
-
if (annotation.kind !== "ruby" || annotation.reading.kind !== "group") return false;
|
|
1976
|
-
if (graphemeIndex <= annotation.range.graphemes.start || graphemeIndex >= annotation.range.graphemes.end) return false;
|
|
1977
|
-
return indicesInside(atoms, annotation).reduce((total, index) => total + (atoms[index]?.boxAdvanceEm ?? 0), 0) <= lineLengthEm;
|
|
1978
|
-
});
|
|
1979
|
-
}
|
|
1980
|
-
function samePresentationGroup(left, right) {
|
|
1981
|
-
return left.presentation.groupRange.graphemes.start === right.presentation.groupRange.graphemes.start && left.presentation.groupRange.graphemes.end === right.presentation.groupRange.graphemes.end;
|
|
1982
|
-
}
|
|
1983
|
-
function legalBoundary(atoms, annotations, leftIndex, rightIndex, lineLengthEm, profile) {
|
|
1984
|
-
const left = atoms[leftIndex];
|
|
1985
|
-
const right = atoms[rightIndex];
|
|
1986
|
-
if (left === void 0 || right === void 0) return true;
|
|
1987
|
-
return !samePresentationGroup(left, right) && profile.breakPenalty(profile.classify({
|
|
1988
|
-
value: left.grapheme.value,
|
|
1989
|
-
presentation: left.presentation.kind
|
|
1990
|
-
}), profile.classify({
|
|
1991
|
-
value: right.grapheme.value,
|
|
1992
|
-
presentation: right.presentation.kind
|
|
1993
|
-
})) !== null && !breakInsideFittableGroupRuby(atoms, annotations, rightIndex, lineLengthEm);
|
|
1994
|
-
}
|
|
1995
|
-
function continuationFor(annotation, range) {
|
|
1996
|
-
const starts = range.graphemes.start === annotation.range.graphemes.start;
|
|
1997
|
-
const ends = range.graphemes.end === annotation.range.graphemes.end;
|
|
1998
|
-
if (starts && ends) return "whole";
|
|
1999
|
-
if (starts) return "start";
|
|
2000
|
-
if (ends) return "end";
|
|
2001
|
-
return "middle";
|
|
2002
|
-
}
|
|
2003
|
-
function groupReadingsForFragments(annotation, fragmentRanges, baseAdvances) {
|
|
2004
|
-
const baseLength = annotation.range.graphemes.end - annotation.range.graphemes.start;
|
|
2005
|
-
if (annotation.reading.kind !== "group") return [];
|
|
2006
|
-
const reading = [...graphemeSegmenter.segment(annotation.reading.text)].map(({ segment }) => segment);
|
|
2007
|
-
const advances = Array.from({ length: baseLength }, (_, index) => baseAdvances.get(annotation.range.graphemes.start + index));
|
|
2008
|
-
const measured = advances.every((advance) => advance !== void 0);
|
|
2009
|
-
const advanceFor = (fragment) => {
|
|
2010
|
-
const fragmentStart = fragment.graphemes.start - annotation.range.graphemes.start;
|
|
2011
|
-
const fragmentLength = fragment.graphemes.end - fragment.graphemes.start;
|
|
2012
|
-
return measured ? advances.slice(fragmentStart, fragmentStart + fragmentLength).reduce((total, advance) => total + advance, 0) : fragmentLength;
|
|
2013
|
-
};
|
|
2014
|
-
const weights = fragmentRanges.map(advanceFor);
|
|
2015
|
-
const effectiveWeights = weights.reduce((total, weight) => total + weight, 0) > 0 ? weights : fragmentRanges.map(({ graphemes }) => graphemes.end - graphemes.start);
|
|
2016
|
-
const totalWeight = effectiveWeights.reduce((total, weight) => total + weight, 0);
|
|
2017
|
-
const reserveEveryFragment = reading.length >= fragmentRanges.length;
|
|
2018
|
-
const fragments = [];
|
|
2019
|
-
let readingStart = 0;
|
|
2020
|
-
let accumulatedWeight = 0;
|
|
2021
|
-
for (const [index, weight] of effectiveWeights.entries()) {
|
|
2022
|
-
accumulatedWeight += weight;
|
|
2023
|
-
const remainingFragments = effectiveWeights.length - index - 1;
|
|
2024
|
-
const proportionalEnd = index === effectiveWeights.length - 1 ? reading.length : Math.round(accumulatedWeight * reading.length / totalWeight);
|
|
2025
|
-
const minimumEnd = reserveEveryFragment ? readingStart + 1 : readingStart;
|
|
2026
|
-
const maximumEnd = reserveEveryFragment ? reading.length - remainingFragments : reading.length;
|
|
2027
|
-
const readingEnd = Math.min(maximumEnd, Math.max(minimumEnd, proportionalEnd));
|
|
2028
|
-
fragments.push(reading.slice(readingStart, readingEnd).join(""));
|
|
2029
|
-
readingStart = readingEnd;
|
|
2030
|
-
}
|
|
2031
|
-
return fragments;
|
|
2032
|
-
}
|
|
2033
|
-
function readingForFragment(annotation, range, fragmentPlan) {
|
|
2034
|
-
const start = range.graphemes.start - annotation.range.graphemes.start;
|
|
2035
|
-
const length = range.graphemes.end - range.graphemes.start;
|
|
2036
|
-
if (annotation.reading.kind !== "group") return {
|
|
2037
|
-
kind: annotation.reading.kind,
|
|
2038
|
-
text: annotation.reading.segments.slice(start, start + length).join("")
|
|
2039
|
-
};
|
|
2040
|
-
const fragmentIndex = fragmentPlan.ranges.findIndex(({ graphemes }) => graphemes.start === range.graphemes.start && graphemes.end === range.graphemes.end);
|
|
2041
|
-
return {
|
|
2042
|
-
kind: "group",
|
|
2043
|
-
text: fragmentPlan.groupReadings[fragmentIndex] ?? ""
|
|
2044
|
-
};
|
|
2045
|
-
}
|
|
2046
|
-
function positionReading(text, baseOffsetEm, baseAdvanceEm, settings, measurer) {
|
|
2047
|
-
const measured = [...graphemeSegmenter.segment(text)].map(({ segment }) => ({
|
|
2048
|
-
value: segment,
|
|
2049
|
-
advanceEm: measure(measurer, segment, "ruby", settings) ?? 0
|
|
2050
|
-
}));
|
|
2051
|
-
const total = measured.reduce((sum, grapheme) => sum + grapheme.advanceEm, 0);
|
|
2052
|
-
const edgeGap = measured.length > 0 && total < baseAdvanceEm ? (baseAdvanceEm - total) / (2 * measured.length) : 0;
|
|
2053
|
-
let offsetEm = baseOffsetEm + (total > baseAdvanceEm ? (baseAdvanceEm - total) / 2 : edgeGap);
|
|
2054
|
-
return measured.map((grapheme, index) => {
|
|
2055
|
-
const positioned = {
|
|
2056
|
-
value: grapheme.value,
|
|
2057
|
-
advanceEm: grapheme.advanceEm,
|
|
2058
|
-
offsetEm
|
|
2059
|
-
};
|
|
2060
|
-
offsetEm += grapheme.advanceEm;
|
|
2061
|
-
if (index < measured.length - 1) offsetEm += edgeGap * 2;
|
|
2062
|
-
return positioned;
|
|
2063
|
-
});
|
|
2064
|
-
}
|
|
2065
|
-
function annotationFragmentRange(graphemes, suppressed, sourceGlues, annotation) {
|
|
2066
|
-
return ManuscriptRange.merge([
|
|
2067
|
-
...graphemes.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range),
|
|
2068
|
-
...suppressed.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range),
|
|
2069
|
-
...sourceGlues.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range)
|
|
2070
|
-
]);
|
|
2071
|
-
}
|
|
2072
|
-
function annotationFragments(graphemes, suppressed, sourceGlues, annotations, settings, measurer, fragmentPlansByAnnotation) {
|
|
2073
|
-
const fragments = [];
|
|
2074
|
-
for (const annotation of annotations) {
|
|
2075
|
-
const covered = graphemes.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range));
|
|
2076
|
-
const fragmentRange = annotationFragmentRange(graphemes, suppressed, sourceGlues, annotation);
|
|
2077
|
-
const first = covered[0];
|
|
2078
|
-
const last = covered.at(-1);
|
|
2079
|
-
if (fragmentRange === null || first === void 0 || last === void 0) continue;
|
|
2080
|
-
const common = {
|
|
2081
|
-
annotationRange: annotation.range,
|
|
2082
|
-
fragmentRange,
|
|
2083
|
-
continuation: continuationFor(annotation, fragmentRange)
|
|
2084
|
-
};
|
|
2085
|
-
switch (annotation.kind) {
|
|
2086
|
-
case "bold":
|
|
2087
|
-
case "italic":
|
|
2088
|
-
fragments.push({
|
|
2089
|
-
kind: annotation.kind,
|
|
2090
|
-
...common
|
|
2091
|
-
});
|
|
2092
|
-
break;
|
|
2093
|
-
case "emphasis":
|
|
2094
|
-
fragments.push({
|
|
2095
|
-
kind: "emphasis",
|
|
2096
|
-
mark: annotation.mark,
|
|
2097
|
-
...common
|
|
2098
|
-
});
|
|
2099
|
-
break;
|
|
2100
|
-
case "ruby": {
|
|
2101
|
-
const fragmentPlan = fragmentPlansByAnnotation.get(annotation);
|
|
2102
|
-
if (fragmentPlan === void 0) break;
|
|
2103
|
-
const baseOffsetEm = first.layoutSpan.offsetEm;
|
|
2104
|
-
const baseAdvanceEm = last.layoutSpan.offsetEm + last.layoutSpan.advanceEm - baseOffsetEm;
|
|
2105
|
-
const reading = readingForFragment(annotation, fragmentRange, fragmentPlan);
|
|
2106
|
-
fragments.push({
|
|
2107
|
-
kind: "ruby",
|
|
2108
|
-
rubyKind: reading.kind,
|
|
2109
|
-
reading: reading.text,
|
|
2110
|
-
baseOffsetEm,
|
|
2111
|
-
baseAdvanceEm,
|
|
2112
|
-
readingGraphemes: positionReading(reading.text, baseOffsetEm, baseAdvanceEm, settings, measurer),
|
|
2113
|
-
...common
|
|
2114
|
-
});
|
|
2115
|
-
break;
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
return fragments;
|
|
2120
|
-
}
|
|
2121
|
-
function positionedLine(atoms, sourceGapIndexes, plan) {
|
|
2122
|
-
const items = plan.suppressedIndexes.flatMap((index) => {
|
|
2123
|
-
const atom = atoms[index];
|
|
2124
|
-
return atom === void 0 ? [] : [{
|
|
2125
|
-
kind: "suppressed",
|
|
2126
|
-
value: atom.grapheme.value,
|
|
2127
|
-
range: atom.grapheme.range,
|
|
2128
|
-
reason: "question-or-exclamation-gap"
|
|
2129
|
-
}];
|
|
2130
|
-
});
|
|
2131
|
-
const spacings = new Map(plan.pairSpacings.map((spacing) => [spacing.boundary, spacing]));
|
|
2132
|
-
let offsetEm = 0;
|
|
2133
|
-
const positionSpacing = (spacing) => {
|
|
2134
|
-
if (spacing.kind === "kern") items.push({
|
|
2135
|
-
kind: "kern",
|
|
2136
|
-
offsetEm,
|
|
2137
|
-
widthEm: spacing.widthEm
|
|
2138
|
-
});
|
|
2139
|
-
else items.push({
|
|
2140
|
-
kind: "glue",
|
|
2141
|
-
origin: "generated",
|
|
2142
|
-
offsetEm,
|
|
2143
|
-
widthEm: spacing.widthEm,
|
|
2144
|
-
naturalWidthEm: spacing.naturalWidthEm,
|
|
2145
|
-
adjustment: spacing.widthEm < spacing.naturalWidthEm ? "shrunk" : spacing.widthEm > spacing.naturalWidthEm ? "stretched" : "natural"
|
|
2146
|
-
});
|
|
2147
|
-
offsetEm += spacing.widthEm;
|
|
2148
|
-
};
|
|
2149
|
-
for (let index = plan.contentStart; index < plan.end; index += 1) {
|
|
2150
|
-
const spacing = spacings.get(index);
|
|
2151
|
-
if (spacing !== void 0) positionSpacing(spacing);
|
|
2152
|
-
const atom = atoms[index];
|
|
2153
|
-
if (atom === void 0) continue;
|
|
2154
|
-
if (sourceGapIndexes.has(index)) {
|
|
2155
|
-
items.push({
|
|
2156
|
-
kind: "glue",
|
|
2157
|
-
origin: "source",
|
|
2158
|
-
value: atom.grapheme.value,
|
|
2159
|
-
range: atom.grapheme.range,
|
|
2160
|
-
offsetEm,
|
|
2161
|
-
widthEm: atom.boxAdvanceEm,
|
|
2162
|
-
naturalWidthEm: atom.boxAdvanceEm,
|
|
2163
|
-
adjustment: "natural"
|
|
2164
|
-
});
|
|
2165
|
-
offsetEm += atom.boxAdvanceEm;
|
|
2166
|
-
continue;
|
|
2167
|
-
}
|
|
2168
|
-
const hanging = plan.hangingIndex === index;
|
|
2169
|
-
const layoutAdvanceEm = hanging ? 0 : atom.boxAdvanceEm;
|
|
2170
|
-
items.push({
|
|
2171
|
-
kind: "glyph",
|
|
2172
|
-
value: atom.grapheme.value,
|
|
2173
|
-
range: atom.grapheme.range,
|
|
2174
|
-
layoutSpan: {
|
|
2175
|
-
offsetEm,
|
|
2176
|
-
advanceEm: layoutAdvanceEm
|
|
2177
|
-
},
|
|
2178
|
-
renderSpan: {
|
|
2179
|
-
offsetEm: offsetEm + atom.renderOffsetEm,
|
|
2180
|
-
advanceEm: atom.renderAdvanceEm
|
|
2181
|
-
},
|
|
2182
|
-
disposition: hanging ? "hanging" : "placed",
|
|
2183
|
-
presentation: atom.presentation
|
|
2184
|
-
});
|
|
2185
|
-
offsetEm += layoutAdvanceEm;
|
|
2186
|
-
}
|
|
2187
|
-
const trailingSpacing = spacings.get(plan.end);
|
|
2188
|
-
if (trailingSpacing !== void 0) positionSpacing(trailingSpacing);
|
|
2189
|
-
const ranges = items.flatMap((item) => item.kind === "glyph" || item.kind === "suppressed" || item.kind === "glue" && item.origin === "source" ? [item.range] : []);
|
|
2190
|
-
return {
|
|
2191
|
-
range: ManuscriptRange.merge(ranges),
|
|
2192
|
-
inlineSizeEm: Math.max(0, plan.inlineSizeEm),
|
|
2193
|
-
items,
|
|
2194
|
-
break: plan.break,
|
|
2195
|
-
annotations: []
|
|
2196
|
-
};
|
|
2197
|
-
}
|
|
2198
|
-
function wrapSourceLine(sourceLine, annotations, settings, measurer, baseAdvances) {
|
|
2199
|
-
if (sourceLine.atoms.length === 0) return [NovelLine.empty()];
|
|
2200
|
-
const profile = TYPESETTING_PROFILE;
|
|
2201
|
-
const lines = layoutParagraph(sourceLine.atoms.map(({ grapheme, boxAdvanceEm, presentation }, index) => {
|
|
2202
|
-
const current = sourceLine.atoms[index];
|
|
2203
|
-
const next = sourceLine.atoms[index + 1];
|
|
2204
|
-
return {
|
|
2205
|
-
value: grapheme.value,
|
|
2206
|
-
boxAdvanceEm,
|
|
2207
|
-
sourceGap: sourceLine.suppressedIndexes.has(index),
|
|
2208
|
-
characterClass: profile.classify({
|
|
2209
|
-
value: grapheme.value,
|
|
2210
|
-
presentation: presentation.kind
|
|
2211
|
-
}),
|
|
2212
|
-
pairSpacingAfter: current === void 0 || next === void 0 || !samePresentationGroup(current, next) && !breakInsideFittableGroupRuby(sourceLine.atoms, annotations, index + 1, settings.flow.lineLengthEm)
|
|
2213
|
-
};
|
|
2214
|
-
}), settings.flow.lineLengthEm, profile, (leftIndex, rightIndex) => legalBoundary(sourceLine.atoms, annotations, leftIndex, rightIndex, settings.flow.lineLengthEm, profile)).map((plan) => positionedLine(sourceLine.atoms, sourceLine.suppressedIndexes, plan));
|
|
2215
|
-
const fragmentPlansByAnnotation = new Map(annotations.map((annotation) => {
|
|
2216
|
-
const ranges = lines.flatMap((line) => {
|
|
2217
|
-
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);
|
|
2218
|
-
return range === null ? [] : [range];
|
|
2219
|
-
});
|
|
2220
|
-
return [annotation, {
|
|
2221
|
-
ranges,
|
|
2222
|
-
groupReadings: annotation.kind === "ruby" ? groupReadingsForFragments(annotation, ranges, baseAdvances) : []
|
|
2223
|
-
}];
|
|
2224
|
-
}));
|
|
2225
|
-
return lines.map((line) => ({
|
|
2226
|
-
range: line.range,
|
|
2227
|
-
inlineSizeEm: line.inlineSizeEm,
|
|
2228
|
-
items: line.items,
|
|
2229
|
-
break: line.break,
|
|
2230
|
-
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)
|
|
2231
|
-
}));
|
|
2232
|
-
}
|
|
2233
|
-
function blankLines(count) {
|
|
2234
|
-
return Array.from({ length: count }, NovelLine.empty);
|
|
2235
|
-
}
|
|
2236
|
-
function buildPages(contentLines, settings) {
|
|
2237
|
-
const { linesPerStage, stagesPerPage } = settings.flow;
|
|
2238
|
-
const { stage: stageOffset, page: pageOffset } = settings.offsets;
|
|
2239
|
-
const usablePerStage = linesPerStage - LineOffset.total(stageOffset);
|
|
2240
|
-
const usablePerPage = usablePerStage * stagesPerPage;
|
|
2241
|
-
const pages = [];
|
|
2242
|
-
let contentIndex = 0;
|
|
2243
|
-
while (contentIndex < contentLines.length || pages.length === 0) {
|
|
2244
|
-
const stages = [];
|
|
2245
|
-
for (let stageIndex = 0; stageIndex < stagesPerPage; stageIndex += 1) {
|
|
2246
|
-
const lines = [];
|
|
2247
|
-
for (let position = 0; position < linesPerStage; position += 1) {
|
|
2248
|
-
const usableIndex = stageIndex * usablePerStage + position - stageOffset.leading;
|
|
2249
|
-
const withinStage = position >= stageOffset.leading && position < linesPerStage - stageOffset.trailing;
|
|
2250
|
-
const withinPage = usableIndex >= pageOffset.leading && usableIndex < usablePerPage - pageOffset.trailing;
|
|
2251
|
-
const next = contentLines[contentIndex];
|
|
2252
|
-
if (!withinStage || !withinPage || next === void 0) {
|
|
2253
|
-
lines.push(NovelLine.empty());
|
|
2254
|
-
continue;
|
|
2255
|
-
}
|
|
2256
|
-
lines.push(next);
|
|
2257
|
-
contentIndex += 1;
|
|
2258
|
-
}
|
|
2259
|
-
stages.push(NovelStage.of(lines));
|
|
2260
|
-
}
|
|
2261
|
-
pages.push(NovelPage.of(stages));
|
|
2262
|
-
}
|
|
2263
|
-
return pages;
|
|
2264
|
-
}
|
|
2265
|
-
function createCompose(measurer) {
|
|
2266
|
-
return (manuscript, settings) => {
|
|
2267
|
-
const sourceLines = displayedLines(manuscript.graphemes);
|
|
2268
|
-
const measured = sourceLines.map((line) => measureSourceLine(line, manuscript.annotations, settings, measurer));
|
|
2269
|
-
if (measured.some((line) => line === void 0)) return ManuscriptResult.fail({ reason: "inline measurer returned a negative or non-finite value" });
|
|
2270
|
-
const baseAdvances = /* @__PURE__ */ new Map();
|
|
2271
|
-
for (const line of measured) for (const atom of line?.atoms ?? []) baseAdvances.set(atom.grapheme.range.graphemes.start, atom.boxAdvanceEm);
|
|
2272
|
-
const manuscriptLines = measured.flatMap((line) => line === void 0 ? [] : wrapSourceLine(line, manuscript.annotations, settings, measurer, baseAdvances));
|
|
2273
|
-
const pages = buildPages([
|
|
2274
|
-
...blankLines(settings.offsets.document.leading),
|
|
2275
|
-
...manuscriptLines,
|
|
2276
|
-
...blankLines(settings.offsets.document.trailing)
|
|
2277
|
-
], settings);
|
|
2278
|
-
return ManuscriptResult.succeed({
|
|
2279
|
-
pages,
|
|
2280
|
-
geometry: ManuscriptGeometry.of(settings.flow, settings.appearance),
|
|
2281
|
-
stats: {
|
|
2282
|
-
chars: sourceLines.reduce((total, line) => total + line.length, 0),
|
|
2283
|
-
sourceLines: sourceLines.length,
|
|
2284
|
-
pages: pages.length
|
|
2285
|
-
}
|
|
2286
|
-
});
|
|
2287
|
-
};
|
|
2288
|
-
}
|
|
2289
|
-
function createNovelComposer(options) {
|
|
2290
|
-
return {
|
|
2291
|
-
id: COMPOSER_ID,
|
|
2292
|
-
settingsSchema: NovelCompositionSettings.schema,
|
|
2293
|
-
layoutSchema: NovelLayout.schema,
|
|
2294
|
-
compose: createCompose(options.measurer)
|
|
2295
|
-
};
|
|
2296
|
-
}
|
|
2297
|
-
const novelComposer = createNovelComposer({ measurer: logicalInlineMeasurer });
|
|
2298
|
-
//#endregion
|
|
2299
|
-
//#region src/proofreading/invalid-rule-options.ts
|
|
2300
|
-
const InvalidRuleOptions = { describe: (error) => `ルール ${error.ruleId} のオプション "${error.option}" が不正です: ${error.issues.map(({ message }) => message).join(", ")}` };
|
|
2301
|
-
//#endregion
|
|
2302
|
-
//#region src/proofreading/proofread-error.ts
|
|
2303
|
-
const ProofreadError = {
|
|
2304
|
-
/**
|
|
2305
|
-
* Human-readable rendering. Branch on `kind` instead when the caller needs to react.
|
|
2306
|
-
*/
|
|
2307
|
-
describe: (error) => {
|
|
2308
|
-
switch (error.kind) {
|
|
2309
|
-
case "InvalidRuleId": return `ルール ID "${error.ruleId}" は namespace/name の形式ではありません`;
|
|
2310
|
-
case "DuplicateRuleId": return `ルール ID ${error.ruleId} が重複しています`;
|
|
2311
|
-
case "InvalidRuleMetadata": return `ルール ${error.ruleId} の metadata が不正です: ${joinIssues(error.issues)}`;
|
|
2312
|
-
case "InvalidReport": return `ルール ${error.ruleId} の報告が契約を満たしていません: ${joinIssues(error.issues)}`;
|
|
2313
|
-
case "UnknownMessageId": return `ルール ${error.ruleId} が未宣言のメッセージ ID "${error.messageId}" を報告しました`;
|
|
2314
|
-
case "RuleThrew": return `ルール ${error.ruleId} が例外を投げました: ${describeCause(error.cause)}`;
|
|
2315
|
-
default: return assertNever(error);
|
|
2316
|
-
}
|
|
2317
|
-
} };
|
|
2318
|
-
function joinIssues(issues) {
|
|
2319
|
-
return issues.map(({ message }) => message).join(", ");
|
|
2320
|
-
}
|
|
2321
|
-
//#endregion
|
|
2322
|
-
//#region src/proofreading/internal/interpolate.ts
|
|
2323
|
-
/**
|
|
2324
|
-
* Fills `{{ name }}` placeholders from the report's data; an absent key renders as empty.
|
|
2325
|
-
*/
|
|
2326
|
-
function interpolate(template, data) {
|
|
2327
|
-
return template.replaceAll(/\{\{\s*([^}\s]+)\s*\}\}/gu, (_match, key) => String(data?.[key] ?? ""));
|
|
2328
|
-
}
|
|
2329
|
-
const ProofreadingReport = { schema: readonlyObject({
|
|
2330
|
-
range: ManuscriptRange.schema,
|
|
2331
|
-
messageId: v.string(),
|
|
2332
|
-
data: v.optional(v.pipe(v.record(v.string(), v.union([v.string(), v.number()])), v.readonly())),
|
|
2333
|
-
/**
|
|
2334
|
-
* Omitted means `error`: a rule that says nothing is claiming it found a real defect.
|
|
2335
|
-
*/
|
|
2336
|
-
severity: v.optional(DiagnosticSeverity.schema)
|
|
2337
|
-
}) };
|
|
2338
|
-
//#endregion
|
|
2339
|
-
//#region src/proofreading/proofreading-rule.ts
|
|
2340
|
-
const ProofreadingRuleMessagesSchema = v.pipe(v.record(v.string(), v.string()), v.readonly());
|
|
2341
|
-
const ProofreadingRuleMeta = {
|
|
2342
|
-
schema: readonlyObject({
|
|
2343
|
-
id: v.string(),
|
|
2344
|
-
messages: ProofreadingRuleMessagesSchema
|
|
2345
|
-
}),
|
|
2346
|
-
messagesSchema: ProofreadingRuleMessagesSchema
|
|
2347
|
-
};
|
|
2348
|
-
//#endregion
|
|
2349
|
-
//#region src/proofreading/proofread-manuscript.ts
|
|
2350
|
-
const reportsSchema = readonlyArray(ProofreadingReport.schema);
|
|
2351
|
-
function prepareRules(rules) {
|
|
2352
|
-
const prepared = [];
|
|
2353
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2354
|
-
for (const rule of rules) {
|
|
2355
|
-
const id = NamespacedId.parse(rule.meta.id);
|
|
2356
|
-
if (id === void 0) return ManuscriptResult.fail({
|
|
2357
|
-
kind: "InvalidRuleId",
|
|
2358
|
-
ruleId: rule.meta.id
|
|
2359
|
-
});
|
|
2360
|
-
if (seen.has(id)) return ManuscriptResult.fail({
|
|
2361
|
-
kind: "DuplicateRuleId",
|
|
2362
|
-
ruleId: id
|
|
2363
|
-
});
|
|
2364
|
-
const messages = v.safeParse(ProofreadingRuleMeta.messagesSchema, rule.meta.messages);
|
|
2365
|
-
if (!messages.success) return ManuscriptResult.fail({
|
|
2366
|
-
kind: "InvalidRuleMetadata",
|
|
2367
|
-
ruleId: id,
|
|
2368
|
-
issues: ValidationIssue.from(messages.issues)
|
|
2369
|
-
});
|
|
2370
|
-
seen.add(id);
|
|
2371
|
-
prepared.push({
|
|
2372
|
-
id,
|
|
2373
|
-
messages: messages.output,
|
|
2374
|
-
rule
|
|
2375
|
-
});
|
|
2376
|
-
}
|
|
2377
|
-
return ManuscriptResult.succeed(prepared);
|
|
2378
|
-
}
|
|
2379
|
-
/**
|
|
2380
|
-
* Runs one rule and returns what it reported. A composed-only rule against a parsed manuscript is
|
|
2381
|
-
* simply skipped: the caller asked for a stage this rule cannot see.
|
|
2382
|
-
*/
|
|
2383
|
-
function collectReports({ id, rule }, parsed, composed) {
|
|
2384
|
-
const reports = [];
|
|
2385
|
-
const context = { report: (report) => {
|
|
2386
|
-
reports.push(report);
|
|
2387
|
-
} };
|
|
2388
|
-
try {
|
|
2389
|
-
switch (rule.kind) {
|
|
2390
|
-
case "parsed":
|
|
2391
|
-
rule.check(parsed, context);
|
|
2392
|
-
break;
|
|
2393
|
-
case "composed":
|
|
2394
|
-
if (composed !== null) rule.check(composed, context);
|
|
2395
|
-
break;
|
|
2396
|
-
default: assertNever(rule);
|
|
2397
|
-
}
|
|
2398
|
-
} catch (cause) {
|
|
2399
|
-
return ManuscriptResult.fail({
|
|
2400
|
-
kind: "RuleThrew",
|
|
2401
|
-
ruleId: id,
|
|
2402
|
-
cause
|
|
2403
|
-
});
|
|
2404
|
-
}
|
|
2405
|
-
const validated = v.safeParse(reportsSchema, reports);
|
|
2406
|
-
return validated.success ? ManuscriptResult.succeed(validated.output) : ManuscriptResult.fail({
|
|
2407
|
-
kind: "InvalidReport",
|
|
2408
|
-
ruleId: id,
|
|
2409
|
-
issues: ValidationIssue.from(validated.issues)
|
|
2410
|
-
});
|
|
2411
|
-
}
|
|
2412
|
-
function toDiagnostics({ id, messages }, reports, source) {
|
|
2413
|
-
const diagnostics = [];
|
|
2414
|
-
for (const report of reports) {
|
|
2415
|
-
const template = messages[report.messageId];
|
|
2416
|
-
if (template === void 0) return ManuscriptResult.fail({
|
|
2417
|
-
kind: "UnknownMessageId",
|
|
2418
|
-
ruleId: id,
|
|
2419
|
-
messageId: report.messageId
|
|
2420
|
-
});
|
|
2421
|
-
diagnostics.push(ManuscriptDiagnostic.of({
|
|
2422
|
-
source,
|
|
2423
|
-
origin: {
|
|
2424
|
-
kind: "rule",
|
|
2425
|
-
id
|
|
2426
|
-
},
|
|
2427
|
-
severity: report.severity ?? "error",
|
|
2428
|
-
code: report.messageId,
|
|
2429
|
-
message: interpolate(template, report.data),
|
|
2430
|
-
range: report.range
|
|
2431
|
-
}));
|
|
2432
|
-
}
|
|
2433
|
-
return ManuscriptResult.succeed(diagnostics);
|
|
2434
|
-
}
|
|
2435
|
-
function run(parsed, composed, rules) {
|
|
2436
|
-
const prepared = prepareRules(rules);
|
|
2437
|
-
if (!prepared.ok) return prepared;
|
|
2438
|
-
const diagnostics = [];
|
|
2439
|
-
for (const rule of prepared.value) {
|
|
2440
|
-
const reports = collectReports(rule, parsed, composed);
|
|
2441
|
-
if (!reports.ok) return reports;
|
|
2442
|
-
const produced = toDiagnostics(rule, reports.value, parsed.source);
|
|
2443
|
-
if (!produced.ok) return produced;
|
|
2444
|
-
diagnostics.push(...produced.value);
|
|
2445
|
-
}
|
|
2446
|
-
return ManuscriptResult.succeed([...diagnostics].sort(ManuscriptDiagnostic.compare));
|
|
2447
|
-
}
|
|
2448
|
-
function proofreadManuscript(manuscript, options) {
|
|
2449
|
-
return "composerId" in manuscript ? run(manuscript.parsed, manuscript, options.rules) : run(manuscript, null, options.rules);
|
|
2450
|
-
}
|
|
2451
|
-
//#endregion
|
|
2452
|
-
//#region src/proofreading/rules/internal/display-line.ts
|
|
2453
|
-
/**
|
|
2454
|
-
* Splits on CRLF, CR and LF. The text after the last break is always emitted, even when empty.
|
|
2455
|
-
*/
|
|
2456
|
-
function splitDisplayLines(text) {
|
|
2457
|
-
const lines = [];
|
|
2458
|
-
let start = 0;
|
|
2459
|
-
let index = 0;
|
|
2460
|
-
while (index < text.length) {
|
|
2461
|
-
const character = text[index];
|
|
2462
|
-
if (character !== "\n" && character !== "\r") {
|
|
2463
|
-
index += 1;
|
|
2464
|
-
continue;
|
|
2465
|
-
}
|
|
2466
|
-
lines.push({
|
|
2467
|
-
text: text.slice(start, index),
|
|
2468
|
-
start
|
|
2469
|
-
});
|
|
2470
|
-
index += character === "\r" && text[index + 1] === "\n" ? 2 : 1;
|
|
2471
|
-
start = index;
|
|
2472
|
-
}
|
|
2473
|
-
lines.push({
|
|
2474
|
-
text: text.slice(start),
|
|
2475
|
-
start
|
|
2476
|
-
});
|
|
2477
|
-
return lines;
|
|
2478
|
-
}
|
|
2479
|
-
//#endregion
|
|
2480
|
-
//#region src/proofreading/rules/internal/rule-range.ts
|
|
2481
|
-
const EMPTY_BOUNDS = {
|
|
2482
|
-
start: 0,
|
|
2483
|
-
end: 0
|
|
2484
|
-
};
|
|
2485
|
-
/**
|
|
2486
|
-
* Turns a span of display text into a full range. The source span is the smallest contiguous run
|
|
2487
|
-
* that covers the display span, so it includes any notation the parser removed in between.
|
|
2488
|
-
*/
|
|
2489
|
-
function displayRange(manuscript, start, end) {
|
|
2490
|
-
const covered = manuscript.graphemes.filter(({ range }) => range.display.start < end && range.display.end > start);
|
|
2491
|
-
const first = covered[0];
|
|
2492
|
-
const last = covered.at(-1);
|
|
2493
|
-
if (first === void 0 || last === void 0) return ManuscriptRange.of({
|
|
2494
|
-
source: EMPTY_BOUNDS,
|
|
2495
|
-
display: {
|
|
2496
|
-
start,
|
|
2497
|
-
end
|
|
2498
|
-
},
|
|
2499
|
-
graphemes: EMPTY_BOUNDS
|
|
2500
|
-
});
|
|
2501
|
-
const merged = ManuscriptRange.merge(covered.map(({ range }) => range));
|
|
2502
|
-
return ManuscriptRange.of({
|
|
2503
|
-
source: {
|
|
2504
|
-
start: first.range.source.start + clampWithin(start - first.range.display.start, first.range),
|
|
2505
|
-
end: end === last.range.display.end ? last.range.source.end : last.range.source.start + clampWithin(end - last.range.display.start, last.range)
|
|
2506
|
-
},
|
|
2507
|
-
display: {
|
|
2508
|
-
start,
|
|
2509
|
-
end
|
|
2510
|
-
},
|
|
2511
|
-
graphemes: merged?.graphemes ?? EMPTY_BOUNDS
|
|
2512
|
-
});
|
|
2513
|
-
}
|
|
2514
|
-
/**
|
|
2515
|
-
* A display offset inside a grapheme cannot map past that grapheme's own source span.
|
|
2516
|
-
*/
|
|
2517
|
-
function clampWithin(offset, range) {
|
|
2518
|
-
return Math.min(offset, range.source.end - range.source.start);
|
|
2519
|
-
}
|
|
2520
|
-
/**
|
|
2521
|
-
* Turns a span of source text into a full range, for rules that inspect the raw source.
|
|
2522
|
-
*/
|
|
2523
|
-
function sourceRange(manuscript, source) {
|
|
2524
|
-
const covered = manuscript.graphemes.filter(({ range }) => range.source.start < source.end && range.source.end > source.start);
|
|
2525
|
-
const enclosing = manuscript.annotations.find(({ range }) => range.source.start <= source.start && range.source.end >= source.end);
|
|
2526
|
-
const merged = ManuscriptRange.merge(covered.map(({ range }) => range)) ?? enclosing?.range ?? null;
|
|
2527
|
-
return ManuscriptRange.of({
|
|
2528
|
-
source,
|
|
2529
|
-
display: merged?.display ?? EMPTY_BOUNDS,
|
|
2530
|
-
graphemes: merged?.graphemes ?? EMPTY_BOUNDS
|
|
2531
|
-
});
|
|
2532
|
-
}
|
|
2533
|
-
//#endregion
|
|
2534
|
-
//#region src/proofreading/rules/internal/define-rule.ts
|
|
2535
|
-
const DEFAULT_MESSAGE_ID = "default";
|
|
2536
|
-
/**
|
|
2537
|
-
* Builds a single-message rule; the body reports ranges and the wrapper names the message.
|
|
2538
|
-
*/
|
|
2539
|
-
function defineParsedRule(id, message, check) {
|
|
2540
|
-
return {
|
|
2541
|
-
kind: "parsed",
|
|
2542
|
-
meta: {
|
|
2543
|
-
id,
|
|
2544
|
-
messages: { [DEFAULT_MESSAGE_ID]: message }
|
|
2545
|
-
},
|
|
2546
|
-
check: (manuscript, context) => {
|
|
2547
|
-
check(manuscript, (range, options) => {
|
|
2548
|
-
context.report({
|
|
2549
|
-
...options,
|
|
2550
|
-
range,
|
|
2551
|
-
messageId: DEFAULT_MESSAGE_ID
|
|
2552
|
-
});
|
|
2553
|
-
});
|
|
2554
|
-
}
|
|
2555
|
-
};
|
|
2556
|
-
}
|
|
2557
|
-
/**
|
|
2558
|
-
* Builds a rule that scans each display line with a sticky pattern. Matching per line rather than
|
|
2559
|
-
* across the whole text keeps findings from spanning a line break.
|
|
2560
|
-
*/
|
|
2561
|
-
function defineMatchRule(options) {
|
|
2562
|
-
return defineParsedRule(options.id, options.message, (manuscript, report) => {
|
|
2563
|
-
for (const line of splitDisplayLines(manuscript.displayText)) {
|
|
2564
|
-
options.pattern.lastIndex = 0;
|
|
2565
|
-
let match = options.pattern.exec(line.text);
|
|
2566
|
-
while (match !== null) {
|
|
2567
|
-
if (options.accept?.(match) ?? true) {
|
|
2568
|
-
const start = line.start + match.index;
|
|
2569
|
-
report(displayRange(manuscript, start, start + match[0].length));
|
|
2570
|
-
}
|
|
2571
|
-
match = options.pattern.exec(line.text);
|
|
2572
|
-
}
|
|
2573
|
-
}
|
|
2574
|
-
});
|
|
2575
|
-
}
|
|
2576
|
-
//#endregion
|
|
2577
|
-
//#region src/proofreading/rules/consistent-kanji-opening.ts
|
|
2578
|
-
const RULE_ID$5 = "kg/consistent-kanji-opening";
|
|
2579
|
-
const MESSAGE = "「{{ closed }}」と「{{ opened }}」が混在しています。文脈上の使い分けか表記ゆれかを確認してください";
|
|
2580
|
-
const KanjiOpeningPairSchema = v.pipe(readonlyObject({
|
|
2581
|
-
closed: v.pipe(v.string(), v.nonEmpty()),
|
|
2582
|
-
opened: v.pipe(v.string(), v.nonEmpty())
|
|
2583
|
-
}), v.check(({ closed, opened }) => closed !== opened, "a pair must hold two different spellings"));
|
|
2584
|
-
const PairsSchema = v.pipe(v.array(KanjiOpeningPairSchema), v.nonEmpty(), v.readonly());
|
|
2585
|
-
const DEFAULT_PAIRS = [
|
|
2586
|
-
{
|
|
2587
|
-
closed: "出来る",
|
|
2588
|
-
opened: "できる"
|
|
2589
|
-
},
|
|
2590
|
-
{
|
|
2591
|
-
closed: "出来ない",
|
|
2592
|
-
opened: "できない"
|
|
2593
|
-
},
|
|
2594
|
-
{
|
|
2595
|
-
closed: "下さい",
|
|
2596
|
-
opened: "ください"
|
|
2597
|
-
},
|
|
2598
|
-
{
|
|
2599
|
-
closed: "頂く",
|
|
2600
|
-
opened: "いただく"
|
|
2601
|
-
},
|
|
2602
|
-
{
|
|
2603
|
-
closed: "致す",
|
|
2604
|
-
opened: "いたす"
|
|
2605
|
-
},
|
|
2606
|
-
{
|
|
2607
|
-
closed: "何故",
|
|
2608
|
-
opened: "なぜ"
|
|
2609
|
-
},
|
|
2610
|
-
{
|
|
2611
|
-
closed: "為に",
|
|
2612
|
-
opened: "ために"
|
|
2613
|
-
},
|
|
2614
|
-
{
|
|
2615
|
-
closed: "時に",
|
|
2616
|
-
opened: "ときに"
|
|
2617
|
-
},
|
|
2618
|
-
{
|
|
2619
|
-
closed: "様に",
|
|
2620
|
-
opened: "ように"
|
|
2621
|
-
},
|
|
2622
|
-
{
|
|
2623
|
-
closed: "方が",
|
|
2624
|
-
opened: "ほうが"
|
|
2625
|
-
}
|
|
2626
|
-
];
|
|
2627
|
-
/**
|
|
2628
|
-
* Reports the first kanji occurrence of every pair the manuscript also writes in kana. Whether the
|
|
2629
|
-
* two spellings are a deliberate distinction is the author's call, so the finding is a warning.
|
|
2630
|
-
*/
|
|
2631
|
-
function build$3(pairs) {
|
|
2632
|
-
return defineParsedRule(RULE_ID$5, MESSAGE, (manuscript, report) => {
|
|
2633
|
-
for (const { closed, opened } of pairs) {
|
|
2634
|
-
if (!manuscript.displayText.includes(opened)) continue;
|
|
2635
|
-
const index = manuscript.displayText.indexOf(closed);
|
|
2636
|
-
if (index === -1) continue;
|
|
2637
|
-
report(displayRange(manuscript, index, index + closed.length), {
|
|
2638
|
-
data: {
|
|
2639
|
-
closed,
|
|
2640
|
-
opened
|
|
2641
|
-
},
|
|
2642
|
-
severity: "warning"
|
|
2643
|
-
});
|
|
2644
|
-
}
|
|
2645
|
-
});
|
|
2646
|
-
}
|
|
2647
|
-
const consistentKanjiOpeningRule = () => build$3(DEFAULT_PAIRS);
|
|
2648
|
-
function createConsistentKanjiOpeningRule(options = {}) {
|
|
2649
|
-
if (options.pairs === void 0) return ManuscriptResult.succeed(consistentKanjiOpeningRule());
|
|
2650
|
-
const pairs = v.safeParse(PairsSchema, options.pairs);
|
|
2651
|
-
if (!pairs.success) return ManuscriptResult.fail({
|
|
2652
|
-
kind: "InvalidRuleOptions",
|
|
2653
|
-
ruleId: RULE_ID$5,
|
|
2654
|
-
option: "pairs",
|
|
2655
|
-
issues: ValidationIssue.from(pairs.issues)
|
|
2656
|
-
});
|
|
2657
|
-
return ManuscriptResult.succeed(build$3(pairs.output));
|
|
2658
|
-
}
|
|
2659
|
-
//#endregion
|
|
2660
|
-
//#region src/proofreading/rules/internal/mixed-width.ts
|
|
2661
|
-
/**
|
|
2662
|
-
* Builds a rule that fires when a manuscript uses both widths of one character class. Which width
|
|
2663
|
-
* is right is the author's decision, so the finding is a warning, and one finding at the first
|
|
2664
|
-
* halfwidth occurrence says everything a list of every mixed character would.
|
|
2665
|
-
*/
|
|
2666
|
-
function defineMixedWidthRule({ id, message, halfwidth, fullwidth }) {
|
|
2667
|
-
return defineParsedRule(id, message, (manuscript, report) => {
|
|
2668
|
-
const found = halfwidth.exec(manuscript.displayText);
|
|
2669
|
-
const counterpart = fullwidth.exec(manuscript.displayText);
|
|
2670
|
-
if (found === null || counterpart === null) return;
|
|
2671
|
-
report(displayRange(manuscript, found.index, found.index + found[0].length), {
|
|
2672
|
-
data: {
|
|
2673
|
-
halfwidth: found[0],
|
|
2674
|
-
fullwidth: counterpart[0]
|
|
2675
|
-
},
|
|
2676
|
-
severity: "warning"
|
|
2677
|
-
});
|
|
2678
|
-
});
|
|
2679
|
-
}
|
|
2680
|
-
//#endregion
|
|
2681
|
-
//#region src/proofreading/rules/consistent-latin-width.ts
|
|
2682
|
-
const consistentLatinWidthRule = () => defineMixedWidthRule({
|
|
2683
|
-
id: "kg/consistent-latin-width",
|
|
2684
|
-
message: "半角英字「{{ halfwidth }}」と全角英字「{{ fullwidth }}」が混在しています。作品内の方針を確認してください",
|
|
2685
|
-
halfwidth: /[A-Za-z]/u,
|
|
2686
|
-
fullwidth: /[A-Za-z]/u
|
|
2687
|
-
});
|
|
2688
|
-
//#endregion
|
|
2689
|
-
//#region src/proofreading/rules/consistent-numeral-width.ts
|
|
2690
|
-
const consistentNumeralWidthRule = () => defineMixedWidthRule({
|
|
2691
|
-
id: "kg/consistent-numeral-width",
|
|
2692
|
-
message: "半角数字「{{ halfwidth }}」と全角数字「{{ fullwidth }}」が混在しています。作品内の方針を確認してください",
|
|
2693
|
-
halfwidth: /[0-9]/u,
|
|
2694
|
-
fullwidth: /[0-9]/u
|
|
2695
|
-
});
|
|
2696
|
-
//#endregion
|
|
2697
|
-
//#region src/proofreading/rules/dash.ts
|
|
2698
|
-
const RULE_ID$4 = "kg/dash";
|
|
2699
|
-
const DEFAULT_PREFERRED = "―";
|
|
2700
|
-
const DashCharacterSchema = v.picklist([
|
|
2701
|
-
"―",
|
|
2702
|
-
"—",
|
|
2703
|
-
"─"
|
|
2704
|
-
]);
|
|
2705
|
-
const DASH_LIKE_RUN = /[—–―─━﹣--]+/gu;
|
|
2706
|
-
const EVEN_COUNT_MESSAGE = "連続するダッシュの数は偶数にしてください";
|
|
2707
|
-
function messageIdFor(run, preferred) {
|
|
2708
|
-
if (run === "-") return void 0;
|
|
2709
|
-
if (run !== preferred.repeat(run.length)) return "character";
|
|
2710
|
-
return run.length % 2 === 1 ? "even-count" : void 0;
|
|
2711
|
-
}
|
|
2712
|
-
function build$2(preferred) {
|
|
2713
|
-
const messages = {
|
|
2714
|
-
character: `ダッシュには「${preferred}」を使ってください`,
|
|
2715
|
-
"even-count": EVEN_COUNT_MESSAGE
|
|
2716
|
-
};
|
|
2717
|
-
return {
|
|
2718
|
-
kind: "parsed",
|
|
2719
|
-
meta: {
|
|
2720
|
-
id: RULE_ID$4,
|
|
2721
|
-
messages
|
|
2722
|
-
},
|
|
2723
|
-
check: (manuscript, context) => {
|
|
2724
|
-
for (const line of splitDisplayLines(manuscript.displayText)) {
|
|
2725
|
-
DASH_LIKE_RUN.lastIndex = 0;
|
|
2726
|
-
let match = DASH_LIKE_RUN.exec(line.text);
|
|
2727
|
-
while (match !== null) {
|
|
2728
|
-
const run = match[0];
|
|
2729
|
-
const messageId = messageIdFor(run, preferred);
|
|
2730
|
-
if (messageId !== void 0) {
|
|
2731
|
-
const start = line.start + match.index;
|
|
2732
|
-
context.report({
|
|
2733
|
-
range: displayRange(manuscript, start, start + run.length),
|
|
2734
|
-
messageId
|
|
2735
|
-
});
|
|
2736
|
-
}
|
|
2737
|
-
match = DASH_LIKE_RUN.exec(line.text);
|
|
2738
|
-
}
|
|
2739
|
-
}
|
|
2740
|
-
}
|
|
2741
|
-
};
|
|
2742
|
-
}
|
|
2743
|
-
const dashRule = () => build$2(DEFAULT_PREFERRED);
|
|
2744
|
-
function createDashRule(options = {}) {
|
|
2745
|
-
if (options.preferred === void 0) return ManuscriptResult.succeed(dashRule());
|
|
2746
|
-
const preferred = v.safeParse(DashCharacterSchema, options.preferred);
|
|
2747
|
-
if (!preferred.success) return ManuscriptResult.fail({
|
|
2748
|
-
kind: "InvalidRuleOptions",
|
|
2749
|
-
ruleId: RULE_ID$4,
|
|
2750
|
-
option: "preferred",
|
|
2751
|
-
issues: ValidationIssue.from(preferred.issues)
|
|
2752
|
-
});
|
|
2753
|
-
return ManuscriptResult.succeed(build$2(preferred.output));
|
|
2754
|
-
}
|
|
2755
|
-
//#endregion
|
|
2756
|
-
//#region src/proofreading/rules/ellipsis-character.ts
|
|
2757
|
-
/**
|
|
2758
|
-
* Stand-ins for `…`. Runs of `。` and `・` are left to the rules that already count them, so one
|
|
2759
|
-
* substitute is never reported twice.
|
|
2760
|
-
*/
|
|
2761
|
-
const ellipsisCharacterRule = () => defineMatchRule({
|
|
2762
|
-
id: "kg/ellipsis-character",
|
|
2763
|
-
pattern: /\.{3,}|⋯+/gu,
|
|
2764
|
-
message: "三点リーダーには「…」を使ってください"
|
|
2765
|
-
});
|
|
2766
|
-
//#endregion
|
|
2767
|
-
//#region src/proofreading/rules/even-ellipsis.ts
|
|
2768
|
-
const evenEllipsisRule = () => defineMatchRule({
|
|
2769
|
-
id: "kg/even-ellipsis",
|
|
2770
|
-
pattern: /…+/gu,
|
|
2771
|
-
accept: (match) => match[0].length % 2 === 1,
|
|
2772
|
-
message: "連続する三点リーダーの数は偶数にしてください"
|
|
2773
|
-
});
|
|
2774
|
-
//#endregion
|
|
2775
|
-
//#region src/proofreading/rules/fullwidth-japanese-punctuation.ts
|
|
2776
|
-
const RULE_ID$3 = "kg/fullwidth-japanese-punctuation";
|
|
2777
|
-
const MESSAGES$2 = {
|
|
2778
|
-
"halfwidth-japanese-punctuation": "半角の「{{ character }}」があります。日本語の句読点・括弧・感嘆符・疑問符は全角にしてください",
|
|
2779
|
-
"halfwidth-near-japanese": "日本語の前後に半角の「{{ character }}」があります。全角にすべきか確認してください"
|
|
2780
|
-
};
|
|
2781
|
-
const JAPANESE_CHARACTER = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}々〆ヵヶ]/u;
|
|
2782
|
-
const CHECKS = [
|
|
2783
|
-
{
|
|
2784
|
-
pattern: /[。、「」]/gu,
|
|
2785
|
-
messageId: "halfwidth-japanese-punctuation",
|
|
2786
|
-
severity: "error",
|
|
2787
|
-
requiresJapanese: false
|
|
2788
|
-
},
|
|
2789
|
-
{
|
|
2790
|
-
pattern: /[!?]/gu,
|
|
2791
|
-
messageId: "halfwidth-japanese-punctuation",
|
|
2792
|
-
severity: "error",
|
|
2793
|
-
requiresJapanese: true
|
|
2794
|
-
},
|
|
2795
|
-
{
|
|
2796
|
-
pattern: /[,.()[\]{}]/gu,
|
|
2797
|
-
messageId: "halfwidth-near-japanese",
|
|
2798
|
-
severity: "warning",
|
|
2799
|
-
requiresJapanese: true
|
|
2800
|
-
}
|
|
2801
|
-
];
|
|
2802
|
-
function isBesideJapanese(text, index) {
|
|
2803
|
-
const before = text[index - 1];
|
|
2804
|
-
const after = text[index + 1];
|
|
2805
|
-
return before !== void 0 && JAPANESE_CHARACTER.test(before) || after !== void 0 && JAPANESE_CHARACTER.test(after);
|
|
2806
|
-
}
|
|
2807
|
-
/**
|
|
2808
|
-
* A period inside a run of three or more is a stand-in for `…`, which `kg/ellipsis-character`
|
|
2809
|
-
* already reports.
|
|
2810
|
-
*/
|
|
2811
|
-
function isEllipsisSubstitute(text, index) {
|
|
2812
|
-
if (text[index] !== ".") return false;
|
|
2813
|
-
let start = index;
|
|
2814
|
-
while (text[start - 1] === ".") start -= 1;
|
|
2815
|
-
let end = index;
|
|
2816
|
-
while (text[end + 1] === ".") end += 1;
|
|
2817
|
-
return end - start + 1 >= 3;
|
|
2818
|
-
}
|
|
2819
|
-
const fullwidthJapanesePunctuationRule = () => ({
|
|
2820
|
-
kind: "parsed",
|
|
2821
|
-
meta: {
|
|
2822
|
-
id: RULE_ID$3,
|
|
2823
|
-
messages: MESSAGES$2
|
|
2824
|
-
},
|
|
2825
|
-
check: (manuscript, context) => {
|
|
2826
|
-
for (const line of splitDisplayLines(manuscript.displayText)) for (const { pattern, messageId, severity, requiresJapanese } of CHECKS) for (const match of line.text.matchAll(pattern)) {
|
|
2827
|
-
if (requiresJapanese && !isBesideJapanese(line.text, match.index)) continue;
|
|
2828
|
-
if (isEllipsisSubstitute(line.text, match.index)) continue;
|
|
2829
|
-
const start = line.start + match.index;
|
|
2830
|
-
context.report({
|
|
2831
|
-
range: displayRange(manuscript, start, start + match[0].length),
|
|
2832
|
-
messageId,
|
|
2833
|
-
data: { character: match[0] },
|
|
2834
|
-
severity
|
|
2835
|
-
});
|
|
2836
|
-
}
|
|
2837
|
-
}
|
|
2838
|
-
});
|
|
2839
|
-
//#endregion
|
|
2840
|
-
//#region src/proofreading/rules/max-arabic-numeral-digits.ts
|
|
2841
|
-
const RULE_ID$2 = "kg/max-arabic-numeral-digits";
|
|
2842
|
-
const DEFAULT_MAX_DIGITS = 2;
|
|
2843
|
-
const MaxDigitsSchema = v.pipe(v.number(), v.finite(), v.integer(), v.minValue(1));
|
|
2844
|
-
function build$1(maxDigits) {
|
|
2845
|
-
return defineMatchRule({
|
|
2846
|
-
id: RULE_ID$2,
|
|
2847
|
-
pattern: /([0-90-9]+)(?:[..]([0-90-9]+))?/gu,
|
|
2848
|
-
accept: (match) => (match[1]?.length ?? 0) > maxDigits || (match[2]?.length ?? 0) > maxDigits,
|
|
2849
|
-
message: `${maxDigits}桁を超えるアラビア数字が使われています`
|
|
2850
|
-
});
|
|
2851
|
-
}
|
|
2852
|
-
const maxArabicNumeralDigitsRule = () => build$1(DEFAULT_MAX_DIGITS);
|
|
2853
|
-
function createMaxArabicNumeralDigitsRule(options = {}) {
|
|
2854
|
-
if (options.maxDigits === void 0) return ManuscriptResult.succeed(maxArabicNumeralDigitsRule());
|
|
2855
|
-
const maxDigits = v.safeParse(MaxDigitsSchema, options.maxDigits);
|
|
2856
|
-
if (!maxDigits.success) return ManuscriptResult.fail({
|
|
2857
|
-
kind: "InvalidRuleOptions",
|
|
2858
|
-
ruleId: RULE_ID$2,
|
|
2859
|
-
option: "maxDigits",
|
|
2860
|
-
issues: ValidationIssue.from(maxDigits.issues)
|
|
2861
|
-
});
|
|
2862
|
-
return ManuscriptResult.succeed(build$1(maxDigits.output));
|
|
2863
|
-
}
|
|
2864
|
-
//#endregion
|
|
2865
|
-
//#region src/proofreading/rules/minus-before-number.ts
|
|
2866
|
-
const minusBeforeNumberRule = () => defineMatchRule({
|
|
2867
|
-
id: "kg/minus-before-number",
|
|
2868
|
-
pattern: /−(?![0-90-9〇一二三四五六七八九十])/gu,
|
|
2869
|
-
message: "マイナス記号の直後には数字が必要です"
|
|
2870
|
-
});
|
|
2871
|
-
//#endregion
|
|
2872
|
-
//#region src/proofreading/rules/no-consecutive-interpunct.ts
|
|
2873
|
-
const noConsecutiveInterpunctRule = () => defineMatchRule({
|
|
2874
|
-
id: "kg/no-consecutive-interpunct",
|
|
2875
|
-
pattern: /・・+/gu,
|
|
2876
|
-
message: "中黒が連続しています"
|
|
2877
|
-
});
|
|
2878
|
-
//#endregion
|
|
2879
|
-
//#region src/proofreading/rules/no-consecutive-punctuation.ts
|
|
2880
|
-
const noConsecutivePunctuationRule = () => defineMatchRule({
|
|
2881
|
-
id: "kg/no-consecutive-punctuation",
|
|
2882
|
-
pattern: /。。+|、、+/gu,
|
|
2883
|
-
message: "句読点が連続しています"
|
|
2884
|
-
});
|
|
2885
|
-
//#endregion
|
|
2886
|
-
//#region src/proofreading/rules/internal/decoration-line.ts
|
|
2887
|
-
const DECORATION_LINE = /^[**◆◇■□●○▽▼△▲☆★・…―─━ー\-==~~\s]+$/u;
|
|
2888
|
-
/**
|
|
2889
|
-
* A line made only of symbols and spaces: a scene break or a section marker rather than a
|
|
2890
|
-
* paragraph. Indentation rules skip these, because their layout is deliberate.
|
|
2891
|
-
*/
|
|
2892
|
-
function isDecorationLine(text) {
|
|
2893
|
-
return DECORATION_LINE.test(text);
|
|
2894
|
-
}
|
|
2895
|
-
//#endregion
|
|
2896
|
-
//#region src/proofreading/rules/paragraph-opening.ts
|
|
2897
|
-
const RULE_ID$1 = "kg/paragraph-opening";
|
|
2898
|
-
const MESSAGES$1 = {
|
|
2899
|
-
"leading-character": "段落の先頭には全角スペースまたは開き括弧が必要です",
|
|
2900
|
-
"indent-width": "段落冒頭の字下げは全角スペース1字にしてください",
|
|
2901
|
-
"unexpected-indent": "括弧から始まる段落を字下げすることはできません"
|
|
2902
|
-
};
|
|
2903
|
-
const OpeningBracketsSchema = v.pipe(v.string(), v.nonEmpty());
|
|
2904
|
-
/**
|
|
2905
|
-
* How a paragraph opens, judged as one decision: a paragraph that opens with a bracket carries no
|
|
2906
|
-
* indent, and prose carries exactly one ideographic space. The three findings are branches of that
|
|
2907
|
-
* one decision rather than separate rules, so a line can never collect two of them at once.
|
|
2908
|
-
*/
|
|
2909
|
-
function build(openingBrackets) {
|
|
2910
|
-
return {
|
|
2911
|
-
kind: "parsed",
|
|
2912
|
-
meta: {
|
|
2913
|
-
id: RULE_ID$1,
|
|
2914
|
-
messages: MESSAGES$1
|
|
2915
|
-
},
|
|
2916
|
-
check: (manuscript, context) => {
|
|
2917
|
-
const report = (start, end, messageId) => {
|
|
2918
|
-
context.report({
|
|
2919
|
-
range: displayRange(manuscript, start, end),
|
|
2920
|
-
messageId
|
|
2921
|
-
});
|
|
2922
|
-
};
|
|
2923
|
-
for (const line of splitDisplayLines(manuscript.displayText)) {
|
|
2924
|
-
if (isDecorationLine(line.text)) continue;
|
|
2925
|
-
const indent = leadingSpaces(line.text);
|
|
2926
|
-
const opening = line.text[indent.length];
|
|
2927
|
-
if (opening === void 0) continue;
|
|
2928
|
-
if (openingBrackets.includes(opening)) {
|
|
2929
|
-
if (indent !== "") report(line.start, line.start + indent.length, "unexpected-indent");
|
|
2930
|
-
} else if (!line.text.startsWith(" ")) {
|
|
2931
|
-
const length = (line.text.codePointAt(0) ?? 0) > 65535 ? 2 : 1;
|
|
2932
|
-
report(line.start, line.start + length, "leading-character");
|
|
2933
|
-
} else if (indent.length !== 1) report(line.start, line.start + indent.length, "indent-width");
|
|
2934
|
-
}
|
|
2935
|
-
}
|
|
2936
|
-
};
|
|
2937
|
-
}
|
|
2938
|
-
const paragraphOpeningRule = () => build(OPENING_BRACKETS);
|
|
2939
|
-
function createParagraphOpeningRule(options = {}) {
|
|
2940
|
-
if (options.openingBrackets === void 0) return ManuscriptResult.succeed(paragraphOpeningRule());
|
|
2941
|
-
const brackets = v.safeParse(OpeningBracketsSchema, options.openingBrackets);
|
|
2942
|
-
if (!brackets.success) return ManuscriptResult.fail({
|
|
2943
|
-
kind: "InvalidRuleOptions",
|
|
2944
|
-
ruleId: RULE_ID$1,
|
|
2945
|
-
option: "openingBrackets",
|
|
2946
|
-
issues: ValidationIssue.from(brackets.issues)
|
|
2947
|
-
});
|
|
2948
|
-
return ManuscriptResult.succeed(build(brackets.output));
|
|
2949
|
-
}
|
|
2950
|
-
//#endregion
|
|
2951
|
-
//#region src/proofreading/rules/punctuation-before-closing-quote.ts
|
|
2952
|
-
const PATTERN = new RegExp(`[。、]+(?=[${characterClass(CLOSING_BRACKETS)}])`, "gu");
|
|
2953
|
-
const punctuationBeforeClosingQuoteRule = () => defineMatchRule({
|
|
2954
|
-
id: "kg/punctuation-before-closing-quote",
|
|
2955
|
-
pattern: PATTERN,
|
|
2956
|
-
message: "閉じ括弧の直前に句読点を置くことはできません"
|
|
2957
|
-
});
|
|
2958
|
-
//#endregion
|
|
2959
|
-
//#region src/proofreading/rules/space-after-question-or-exclamation.ts
|
|
2960
|
-
const RULE_ID = "kg/space-after-question-or-exclamation";
|
|
2961
|
-
const MESSAGES = {
|
|
2962
|
-
default: "感嘆符または疑問符の直後には全角スペースか閉じ括弧が必要です",
|
|
2963
|
-
"space-width": "感嘆符または疑問符の直後の空白は全角スペース1字にしてください",
|
|
2964
|
-
"before-closing-bracket": "閉じ括弧の直前に空白を置くことはできません"
|
|
2965
|
-
};
|
|
2966
|
-
/**
|
|
2967
|
-
* Checks the gap that follows every run of ! or ?. A run at the end of a line or right before a
|
|
2968
|
-
* closing bracket needs no gap; anywhere else it takes exactly one ideographic space.
|
|
2969
|
-
*/
|
|
2970
|
-
const spaceAfterQuestionOrExclamationRule = () => ({
|
|
2971
|
-
kind: "parsed",
|
|
2972
|
-
meta: {
|
|
2973
|
-
id: RULE_ID,
|
|
2974
|
-
messages: MESSAGES
|
|
2975
|
-
},
|
|
2976
|
-
check: (manuscript, context) => {
|
|
2977
|
-
const report = (start, end, messageId) => {
|
|
2978
|
-
context.report({
|
|
2979
|
-
range: displayRange(manuscript, start, end),
|
|
2980
|
-
messageId
|
|
2981
|
-
});
|
|
2982
|
-
};
|
|
2983
|
-
for (const line of splitDisplayLines(manuscript.displayText)) for (const spacing of questionOrExclamationSpacings(line.text)) switch (spacing.kind) {
|
|
2984
|
-
case "valid": break;
|
|
2985
|
-
case "missing":
|
|
2986
|
-
report(line.start + spacing.marks.start, line.start + spacing.marks.end, "default");
|
|
2987
|
-
break;
|
|
2988
|
-
case "invalid-space":
|
|
2989
|
-
report(line.start + spacing.gap.start, line.start + spacing.gap.end, "space-width");
|
|
2990
|
-
break;
|
|
2991
|
-
case "before-closing-bracket":
|
|
2992
|
-
report(line.start + spacing.gap.start, line.start + spacing.gap.end, "before-closing-bracket");
|
|
2993
|
-
break;
|
|
2994
|
-
default: assertNever(spacing);
|
|
2995
|
-
}
|
|
2996
|
-
}
|
|
2997
|
-
});
|
|
2998
|
-
//#endregion
|
|
2999
|
-
//#region src/proofreading/rules/variant-character.ts
|
|
3000
|
-
/**
|
|
3001
|
-
* Mongolian free variation selectors, variation selectors, CJK compatibility ideographs, IVSes.
|
|
3002
|
-
*/
|
|
3003
|
-
function isVariantCharacter(character) {
|
|
3004
|
-
const codePoint = character.codePointAt(0);
|
|
3005
|
-
if (codePoint === void 0) return false;
|
|
3006
|
-
return codePoint >= 6155 && codePoint <= 6157 || codePoint === 6159 || codePoint >= 65024 && codePoint <= 65039 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 194560 && codePoint <= 195103 || codePoint >= 917760 && codePoint <= 917999;
|
|
3007
|
-
}
|
|
3008
|
-
/**
|
|
3009
|
-
* Scans the source rather than the display text: notation removal must not hide a variant.
|
|
3010
|
-
*/
|
|
3011
|
-
const variantCharacterRule = () => defineParsedRule("kg/variant-character", "異体字または字形選択子が使われています", (manuscript, report) => {
|
|
3012
|
-
for (const { index, segment } of graphemeSegmenter.segment(manuscript.source)) if (Array.from(segment).some(isVariantCharacter)) report(sourceRange(manuscript, {
|
|
3013
|
-
start: index,
|
|
3014
|
-
end: index + segment.length
|
|
3015
|
-
}));
|
|
3016
|
-
});
|
|
3017
|
-
//#endregion
|
|
3018
|
-
//#region src/proofreading/rules/default-rules.ts
|
|
3019
|
-
/**
|
|
3020
|
-
* The built-in rule set at its default configuration. Infallible by construction: every rule is
|
|
3021
|
-
* built from constants, so there is no options error for a caller to handle.
|
|
3022
|
-
*
|
|
3023
|
-
* Rules whose answer depends on the work's own conventions — which width numerals take, whether a
|
|
3024
|
-
* word is written in kanji or kana — are exported individually instead of included here.
|
|
3025
|
-
*/
|
|
3026
|
-
function createDefaultProofreadingRules() {
|
|
3027
|
-
return [
|
|
3028
|
-
paragraphOpeningRule(),
|
|
3029
|
-
punctuationBeforeClosingQuoteRule(),
|
|
3030
|
-
spaceAfterQuestionOrExclamationRule(),
|
|
3031
|
-
evenEllipsisRule(),
|
|
3032
|
-
dashRule(),
|
|
3033
|
-
ellipsisCharacterRule(),
|
|
3034
|
-
noConsecutivePunctuationRule(),
|
|
3035
|
-
noConsecutiveInterpunctRule(),
|
|
3036
|
-
minusBeforeNumberRule(),
|
|
3037
|
-
maxArabicNumeralDigitsRule(),
|
|
3038
|
-
fullwidthJapanesePunctuationRule(),
|
|
3039
|
-
variantCharacterRule()
|
|
3040
|
-
];
|
|
3041
|
-
}
|
|
3042
|
-
//#endregion
|
|
3043
|
-
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 };
|
|
618
|
+
export { ComposeError, FontPreset, FontPresetId, FontSizePt, ManuscriptAppearanceSettings, ManuscriptDiagnostic, ManuscriptGeometry, ManuscriptOffsets, ManuscriptRange, ManuscriptResult, NovelCompositionSettings, NovelFlowSettings, PaperSize, PaperSizeId, ParseError, composeManuscript, kakuyomuParser, novelComposer, parseManuscript, pixivParser, plainTextParser };
|