@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/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
- import { eastAsianWidth } from "get-east-asian-width";
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/composed-annotation-fragment.ts
1007
- const emLength = () => v.pipe(v.number(), v.finite(), v.minValue(0));
1008
- const continuation = v.union([
1009
- v.literal("whole"),
1010
- v.literal("start"),
1011
- v.literal("middle"),
1012
- v.literal("end")
1013
- ]);
1014
- const PositionedReadingGraphemeSchema = readonlyObject({
1015
- value: v.string(),
1016
- offsetEm: v.pipe(v.number(), v.finite()),
1017
- advanceEm: emLength()
1018
- });
1019
- const common$1 = {
1020
- annotationRange: ManuscriptRange.schema,
1021
- fragmentRange: ManuscriptRange.schema,
1022
- continuation
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
- * Every vertical line side by side, plus the gap between adjacent lines.
583
+ * Human-readable rendering. Branch on `kind` instead when the caller needs to react.
1274
584
  */
1275
- function contentWidthInEm(flow) {
1276
- return flow.linesPerStage + LINE_GAP_CELLS * (flow.linesPerStage - 1);
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
- const ManuscriptGeometry = {
1279
- schema: ManuscriptGeometrySchema,
1280
- of: (flow, appearance) => {
1281
- const paper = PaperSize.of(appearance.paperSize);
1282
- const cellSizeMm = ptToMm(appearance.fontSizePt);
1283
- const contentWidthMm = contentWidthInEm(flow) * cellSizeMm;
1284
- const contentHeightMm = contentHeightInEm(flow) * cellSizeMm;
1285
- const marginInlineMm = (paper.widthMm - contentWidthMm) / 2;
1286
- const marginBlockMm = (paper.heightMm - contentHeightMm) / 2;
1287
- return {
1288
- paperWidthMm: paper.widthMm,
1289
- paperHeightMm: paper.heightMm,
1290
- cellSizeMm,
1291
- lineGapMm: LINE_GAP_CELLS * cellSizeMm,
1292
- fontSizePt: appearance.fontSizePt,
1293
- contentWidthMm,
1294
- contentHeightMm,
1295
- marginInlineMm,
1296
- marginBlockMm,
1297
- fitsPaper: marginInlineMm >= 0 && marginBlockMm >= 0
1298
- };
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
- * The largest step-aligned point size whose text area still fits the paper.
1302
- */
1303
- maxFontSizePt: (flow, paperSizeId) => {
1304
- const paper = PaperSize.of(paperSizeId);
1305
- const maxCellSizeMm = Math.min(paper.widthMm / contentWidthInEm(flow), paper.heightMm / contentHeightInEm(flow));
1306
- const steps = Math.floor(mmToPt(maxCellSizeMm) / FontSizePt.range.step + 1e-9);
1307
- const aligned = Math.round(steps * FontSizePt.range.step * 1e3) / 1e3;
1308
- return Math.min(FontSizePt.range.max, Math.max(FontSizePt.range.min, aligned));
607
+ gothic: {
608
+ id: "gothic",
609
+ label: "ゴシック",
610
+ family: "\"Yu Gothic\", \"Hiragino Kaku Gothic ProN\", system-ui, sans-serif"
1309
611
  }
1310
612
  };
1311
- //#endregion
1312
- //#region src/internal/japanese-brackets.ts
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
- //#region src/composer/internal/paragraph-layout.ts
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 };