@sushichan044/kg-core 0.6.4 → 0.7.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.
@@ -0,0 +1,1814 @@
1
+ import { c as ManuscriptResult, d as NamespacedId, f as ManuscriptRange, h as readonlyObject, i as OPENING_BRACKETS, m as readonlyArray, o as graphemeSegmenter, p as nonNegativeInteger, r as CLOSING_BRACKETS, t as questionOrExclamationSpacings } from "./question-or-exclamation-spacing-B5o23ZLf.mjs";
2
+ import * as v from "valibot";
3
+ import { eastAsianWidth } from "get-east-asian-width";
4
+ const BoldAnnotation = { schema: readonlyObject({
5
+ kind: v.literal("bold"),
6
+ range: ManuscriptRange.schema
7
+ }) };
8
+ const EmphasisAnnotation = { schema: readonlyObject({
9
+ kind: v.literal("emphasis"),
10
+ range: ManuscriptRange.schema,
11
+ mark: v.string()
12
+ }) };
13
+ const ItalicAnnotation = { schema: readonlyObject({
14
+ kind: v.literal("italic"),
15
+ range: ManuscriptRange.schema
16
+ }) };
17
+ //#endregion
18
+ //#region src/parser/annotation/ruby-annotation.ts
19
+ const GroupRubyKindSchema = v.literal("group");
20
+ const MonoRubyKindSchema = v.literal("mono");
21
+ const JukugoRubyKindSchema = v.literal("jukugo");
22
+ const RubyKindSchema = v.union([
23
+ GroupRubyKindSchema,
24
+ MonoRubyKindSchema,
25
+ JukugoRubyKindSchema
26
+ ]);
27
+ const RubyReadingSchema = v.variant("kind", [
28
+ readonlyObject({
29
+ kind: GroupRubyKindSchema,
30
+ text: v.pipe(v.string(), v.nonEmpty())
31
+ }),
32
+ readonlyObject({
33
+ kind: MonoRubyKindSchema,
34
+ segments: v.pipe(v.array(v.pipe(v.string(), v.nonEmpty())), v.nonEmpty(), v.readonly())
35
+ }),
36
+ readonlyObject({
37
+ kind: JukugoRubyKindSchema,
38
+ segments: v.pipe(v.array(v.pipe(v.string(), v.nonEmpty())), v.nonEmpty(), v.readonly())
39
+ })
40
+ ]);
41
+ const RubyAnnotationSchema = readonlyObject({
42
+ kind: v.literal("ruby"),
43
+ range: ManuscriptRange.schema,
44
+ reading: RubyReadingSchema
45
+ });
46
+ const RubyKind = { schema: RubyKindSchema };
47
+ const RubyAnnotation = { schema: RubyAnnotationSchema };
48
+ const ManuscriptAnnotation = {
49
+ schema: v.variant("kind", [
50
+ RubyAnnotation.schema,
51
+ BoldAnnotation.schema,
52
+ ItalicAnnotation.schema,
53
+ EmphasisAnnotation.schema
54
+ ]),
55
+ /**
56
+ * Stable order for rendering: outermost span first, then by variant name.
57
+ */
58
+ compare: (left, right) => left.range.source.start - right.range.source.start || left.range.source.end - right.range.source.end || left.kind.localeCompare(right.kind)
59
+ };
60
+ const ParsedGrapheme = { schema: readonlyObject({
61
+ value: v.string(),
62
+ range: ManuscriptRange.schema
63
+ }) };
64
+ //#endregion
65
+ //#region src/parser/parsed-manuscript.ts
66
+ const ParsedManuscriptSchema = readonlyObject({
67
+ source: v.string(),
68
+ displayText: v.string(),
69
+ graphemes: readonlyArray(ParsedGrapheme.schema),
70
+ annotations: readonlyArray(ManuscriptAnnotation.schema)
71
+ });
72
+ function rangeFits(range, manuscript) {
73
+ return range.source.end <= manuscript.source.length && range.display.end <= manuscript.displayText.length && range.graphemes.end <= manuscript.graphemes.length;
74
+ }
75
+ function hasValidRubyAssociations(manuscript) {
76
+ const ruby = manuscript.annotations.filter((annotation) => annotation.kind === "ruby");
77
+ return ruby.every((annotation, index) => {
78
+ const baseLength = annotation.range.graphemes.end - annotation.range.graphemes.start;
79
+ if (baseLength <= 0) return false;
80
+ if (annotation.reading.kind !== "group" && annotation.reading.segments.length !== baseLength) return false;
81
+ return ruby.slice(index + 1).every((other) => !ManuscriptRange.overlaps(annotation.range, other.range));
82
+ });
83
+ }
84
+ function isConsistent(manuscript, source) {
85
+ const joined = manuscript.graphemes.map(({ value }) => value).join("");
86
+ const sequential = manuscript.graphemes.every(({ range }, index) => range.graphemes.start === index && range.graphemes.end === index + 1 && rangeFits(range, manuscript));
87
+ return manuscript.source === source && manuscript.displayText === joined && sequential && manuscript.annotations.every(({ range }) => rangeFits(range, manuscript)) && hasValidRubyAssociations(manuscript);
88
+ }
89
+ const CONTRACT_MESSAGE = "parser output must contain consistent source, display, grapheme, and annotation ranges and valid ruby associations";
90
+ const ParsedManuscript = {
91
+ schema: ParsedManuscriptSchema,
92
+ /**
93
+ * The parser contract as a schema: shape, plus the cross-field invariants that make the three
94
+ * coordinate systems line up. Annotations come out in a stable order regardless of parser.
95
+ */
96
+ contractFor: (source) => v.pipe(ParsedManuscriptSchema, v.check((manuscript) => isConsistent(manuscript, source), CONTRACT_MESSAGE), v.transform((manuscript) => ({
97
+ ...manuscript,
98
+ annotations: [...manuscript.annotations].sort(ManuscriptAnnotation.compare)
99
+ })))
100
+ };
101
+ //#endregion
102
+ //#region src/appearance/font-preset-id.ts
103
+ const FontPresetIdSchema = v.picklist(["mincho", "gothic"]);
104
+ const FontPresetId = {
105
+ schema: FontPresetIdSchema,
106
+ is: (value) => v.is(FontPresetIdSchema, value)
107
+ };
108
+ //#endregion
109
+ //#region src/appearance/font-size-pt.ts
110
+ const RANGE = {
111
+ min: 6,
112
+ max: 24,
113
+ step: .5
114
+ };
115
+ const FontSizePtSchema = v.pipe(v.number(), v.finite(), v.minValue(RANGE.min), v.maxValue(RANGE.max), v.multipleOf(RANGE.step));
116
+ const FontSizePt = {
117
+ schema: FontSizePtSchema,
118
+ range: RANGE,
119
+ is: (value) => v.is(FontSizePtSchema, value)
120
+ };
121
+ //#endregion
122
+ //#region src/appearance/paper-size-id.ts
123
+ const PaperSizeIdSchema = v.picklist([
124
+ "a4",
125
+ "a5",
126
+ "a6",
127
+ "jis-b5",
128
+ "jis-b6",
129
+ "shinsho"
130
+ ]);
131
+ const PaperSizeId = {
132
+ schema: PaperSizeIdSchema,
133
+ is: (value) => v.is(PaperSizeIdSchema, value)
134
+ };
135
+ const ManuscriptAppearanceSettings = {
136
+ schema: readonlyObject({
137
+ paperSize: PaperSizeId.schema,
138
+ fontSizePt: FontSizePt.schema,
139
+ fontPreset: FontPresetId.schema
140
+ }),
141
+ defaults: {
142
+ paperSize: "a5",
143
+ fontSizePt: 9,
144
+ fontPreset: "mincho"
145
+ }
146
+ };
147
+ const LineOffset = {
148
+ schema: readonlyObject({
149
+ leading: nonNegativeInteger(),
150
+ trailing: nonNegativeInteger()
151
+ }),
152
+ total: (offset) => offset.leading + offset.trailing
153
+ };
154
+ //#endregion
155
+ //#region src/composer/manuscript-offsets.ts
156
+ const MAX_DOCUMENT_OFFSET = 1e4;
157
+ const ManuscriptOffsets = {
158
+ schema: readonlyObject({
159
+ document: readonlyObject({
160
+ leading: nonNegativeInteger(MAX_DOCUMENT_OFFSET),
161
+ trailing: nonNegativeInteger(MAX_DOCUMENT_OFFSET)
162
+ }),
163
+ page: LineOffset.schema,
164
+ stage: LineOffset.schema
165
+ }),
166
+ defaults: {
167
+ document: {
168
+ leading: 0,
169
+ trailing: 0
170
+ },
171
+ page: {
172
+ leading: 0,
173
+ trailing: 0
174
+ },
175
+ stage: {
176
+ leading: 0,
177
+ trailing: 0
178
+ }
179
+ },
180
+ /**
181
+ * Stage offsets must leave at least one usable line in the stage.
182
+ */
183
+ maxStageTotal: (flow) => Math.max(0, flow.linesPerStage - 1),
184
+ /**
185
+ * Page offsets must leave at least one usable line on the page, after stage offsets apply.
186
+ */
187
+ maxPageTotal: (flow, stage) => Math.max(0, (flow.linesPerStage - LineOffset.total(stage)) * flow.stagesPerPage - 1)
188
+ };
189
+ //#endregion
190
+ //#region src/composer/novel-flow-settings.ts
191
+ const RANGES = {
192
+ lineLengthEm: {
193
+ min: 10,
194
+ max: 60
195
+ },
196
+ linesPerStage: {
197
+ min: 10,
198
+ max: 60
199
+ },
200
+ stagesPerPage: {
201
+ min: 1,
202
+ max: 3
203
+ }
204
+ };
205
+ const boundedCount = (bounds) => v.pipe(v.number(), v.finite(), v.integer(), v.minValue(bounds.min), v.maxValue(bounds.max));
206
+ const NovelFlowSettings = {
207
+ schema: readonlyObject({
208
+ lineLengthEm: boundedCount(RANGES.lineLengthEm),
209
+ linesPerStage: boundedCount(RANGES.linesPerStage),
210
+ stagesPerPage: boundedCount(RANGES.stagesPerPage)
211
+ }),
212
+ ranges: RANGES,
213
+ defaults: {
214
+ lineLengthEm: 27,
215
+ linesPerStage: 23,
216
+ stagesPerPage: 2
217
+ }
218
+ };
219
+ //#endregion
220
+ //#region src/composer/composition-settings.ts
221
+ const NovelCompositionSettingsSchema = v.pipe(readonlyObject({
222
+ flow: NovelFlowSettings.schema,
223
+ offsets: ManuscriptOffsets.schema,
224
+ appearance: ManuscriptAppearanceSettings.schema
225
+ }), 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"));
226
+ const NovelCompositionSettings = {
227
+ schema: NovelCompositionSettingsSchema,
228
+ defaults: {
229
+ flow: NovelFlowSettings.defaults,
230
+ offsets: ManuscriptOffsets.defaults,
231
+ appearance: ManuscriptAppearanceSettings.defaults
232
+ },
233
+ is: (value) => v.is(NovelCompositionSettingsSchema, value)
234
+ };
235
+ const InlineMeasurement = { schema: readonlyObject({ advanceEm: v.pipe(v.number(), v.finite(), v.minValue(0)) }) };
236
+ function graphemeWidth(grapheme) {
237
+ let width = 1;
238
+ for (const character of grapheme) {
239
+ const codePoint = character.codePointAt(0);
240
+ if (codePoint !== void 0) width = Math.max(width, eastAsianWidth(codePoint, { ambiguousAsWide: true }));
241
+ }
242
+ return width / 2;
243
+ }
244
+ /**
245
+ * Deterministic Japanese-context measurement in logical em units.
246
+ */
247
+ const logicalInlineMeasurer = (request) => {
248
+ const { text } = request;
249
+ const width = [...graphemeSegmenter.segment(text)].reduce((total, { segment }) => total + graphemeWidth(segment), 0);
250
+ if (request.role === "ruby") return { advanceEm: width / 2 };
251
+ return { advanceEm: request.presentation === "upright" ? [...graphemeSegmenter.segment(text)].length : width };
252
+ };
253
+ //#endregion
254
+ //#region src/composer/internal/japanese-typesetting-profile.ts
255
+ /**
256
+ * The JLReq character classes (文字クラス) this profile distinguishes. The bare `cl-NN` identifiers are
257
+ * kept rather than descriptive names so the spacing and penalty tables below can be read side by
258
+ * side with the tables in the specification.
259
+ *
260
+ * JLReq defines thirty classes. Math (cl-17, cl-18) and warichu brackets (cl-28, cl-29) are absent
261
+ * because a novel is not set with them. The base-group classes are absent for a different reason: a
262
+ * novel does carry ruby (cl-22, cl-23), but ruby is an annotation the composer positions
263
+ * separately, and `JapaneseCharacter` cannot describe a base group at all — nor a reference mark
264
+ * (cl-20) or a subscripted base (cl-21). A base character is therefore classified here as whatever
265
+ * it is on its own, which for the kanji that usually carries ruby means cl-19.
266
+ *
267
+ * @see https://www.w3.org/TR/jlreq/#character_classes
268
+ */
269
+ const japaneseCharacterClasses = [
270
+ "cl-01",
271
+ "cl-02",
272
+ "cl-03",
273
+ "cl-04",
274
+ "cl-05",
275
+ "cl-06",
276
+ "cl-07",
277
+ "cl-08",
278
+ "cl-09",
279
+ "cl-10",
280
+ "cl-11",
281
+ "cl-12",
282
+ "cl-13",
283
+ "cl-14",
284
+ "cl-15",
285
+ "cl-16",
286
+ "cl-19",
287
+ "cl-24",
288
+ "cl-25",
289
+ "cl-26",
290
+ "cl-27",
291
+ "cl-30"
292
+ ];
293
+ const OPENING = /* @__PURE__ */ new Set(`${OPENING_BRACKETS}【〘〝⦅«`);
294
+ const CLOSING = /* @__PURE__ */ new Set(`${CLOSING_BRACKETS}】〙〟⦆»`);
295
+ const HYPHENS = /* @__PURE__ */ new Set("‐‑⁃–");
296
+ const DIVIDING_PUNCTUATION = /* @__PURE__ */ new Set("!?‼⁇⁈⁉");
297
+ const MIDDLE_DOTS = /* @__PURE__ */ new Set("・:;");
298
+ const SMALL_KANA = /* @__PURE__ */ new Set("ァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖ");
299
+ const ITERATION_MARKS = /* @__PURE__ */ new Set("ヽヾゝゞ々〻");
300
+ const INSEPARABLE = /* @__PURE__ */ new Set("—―…‥─〳〵");
301
+ const PREFIXED_ABBREVIATIONS = /* @__PURE__ */ new Set("¥$£##");
302
+ const POSTFIXED_ABBREVIATIONS = /* @__PURE__ */ new Set("°′″%‰");
303
+ const UNIT_SYMBOLS = /* @__PURE__ */ new Set("℃℉㎀㎁㎂㎃㎄㎅㎆㎇㎈㎉㎊㎋㎌㎍㎎㎏㎐㎑㎒㎓㎔㎕㎖㎗㎘㎙㎚㎛㎜㎝㎞㎟㎠㎡㎢");
304
+ const GROUPED_NUMERAL = /^[0-90-9]$/u;
305
+ const HIRAGANA = /^\p{Script=Hiragana}$/u;
306
+ const KATAKANA = /^\p{Script=Katakana}$/u;
307
+ const WESTERN = /^[\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}]$/u;
308
+ /**
309
+ * Order matters: a character inside a tate-chu-yoko group is cl-30 whatever it would be on its own,
310
+ * and anything left unmatched is cl-19, the class JLReq reserves for kanji and the like.
311
+ */
312
+ function classify({ value, presentation }) {
313
+ if (presentation === "tate-chu-yoko") return "cl-30";
314
+ if (OPENING.has(value)) return "cl-01";
315
+ if (CLOSING.has(value)) return "cl-02";
316
+ if (HYPHENS.has(value)) return "cl-03";
317
+ if (DIVIDING_PUNCTUATION.has(value)) return "cl-04";
318
+ if (MIDDLE_DOTS.has(value)) return "cl-05";
319
+ if (value === "。" || value === ".") return "cl-06";
320
+ if (value === "、" || value === ",") return "cl-07";
321
+ if (INSEPARABLE.has(value)) return "cl-08";
322
+ if (ITERATION_MARKS.has(value)) return "cl-09";
323
+ if (value === "ー") return "cl-10";
324
+ if (SMALL_KANA.has(value)) return "cl-11";
325
+ if (PREFIXED_ABBREVIATIONS.has(value)) return "cl-12";
326
+ if (POSTFIXED_ABBREVIATIONS.has(value)) return "cl-13";
327
+ if (value === " ") return "cl-14";
328
+ if (HIRAGANA.test(value)) return "cl-15";
329
+ if (KATAKANA.test(value)) return "cl-16";
330
+ if (GROUPED_NUMERAL.test(value)) return "cl-24";
331
+ if (UNIT_SYMBOLS.has(value)) return "cl-25";
332
+ if (value === " ") return "cl-26";
333
+ if (WESTERN.test(value)) return "cl-27";
334
+ return "cl-19";
335
+ }
336
+ /**
337
+ * The stages JLReq 3.8.3 spends inter-character space in, as glue priorities: the lower the number,
338
+ * the earlier the paragraph optimizer takes it.
339
+ *
340
+ * JLReq's own first stage is the western word space (cl-26), which is absent here because reducing
341
+ * it means resizing the space's own advance rather than a space between two characters.
342
+ *
343
+ * The line-end stage is priority 0, which the optimizer also charges nothing for. JLReq 3.1.9 is
344
+ * what allows that: the half em after a closing bracket, full stop or comma — and the quarter em
345
+ * after a middle dot — cannot be seen once that character sits at the line end. JLReq orders it
346
+ * second rather than first, but only after the word space this profile does not adjust at all.
347
+ * Stages two and three are one priority here because a line has a single line end, so the two never
348
+ * compete.
349
+ */
350
+ const REDUCTION_STAGE = {
351
+ /**
352
+ * Stages 2 and 3: the space after a closing bracket, full stop, comma or middle dot at line end.
353
+ */
354
+ lineEnd: 0,
355
+ /**
356
+ * Stage 4: the quarter em on either side of a mid-line middle dot.
357
+ */
358
+ middleDot: 1,
359
+ /**
360
+ * Stage 5: the half em before an opening bracket and after a closing bracket or comma.
361
+ */
362
+ punctuation: 2,
363
+ /**
364
+ * Stage 6: the quarter em between Japanese and a numeral, unit symbol or western character.
365
+ */
366
+ mixedText: 3
367
+ };
368
+ /**
369
+ * 無印 in 表1: the pair is set solid.
370
+ */
371
+ const SOLID = {
372
+ kind: "glue",
373
+ naturalWidthEm: 0,
374
+ stretch: {
375
+ priority: 3,
376
+ amountEm: .25
377
+ }
378
+ };
379
+ /**
380
+ * `1/2` in 表1 with `1/2-0` in 表3: a half em that line adjustment may take down to solid.
381
+ */
382
+ const HALF = {
383
+ kind: "glue",
384
+ naturalWidthEm: .5,
385
+ shrink: {
386
+ priority: REDUCTION_STAGE.punctuation,
387
+ amountEm: .5
388
+ }
389
+ };
390
+ /**
391
+ * `1/2` in both tables: the half em exists but is not a reduction opportunity.
392
+ */
393
+ const FIXED_HALF = {
394
+ kind: "glue",
395
+ naturalWidthEm: .5
396
+ };
397
+ /**
398
+ * `1/4` in 表1 with `1/4-0` in 表3: a quarter em that may go down to solid.
399
+ */
400
+ const QUARTER = {
401
+ kind: "glue",
402
+ naturalWidthEm: .25,
403
+ shrink: {
404
+ priority: REDUCTION_STAGE.middleDot,
405
+ amountEm: .25
406
+ }
407
+ };
408
+ /**
409
+ * `1/4` in both tables: the quarter em exists but is not a reduction opportunity.
410
+ */
411
+ const FIXED_QUARTER = {
412
+ kind: "glue",
413
+ naturalWidthEm: .25
414
+ };
415
+ /**
416
+ * `1/4-1/8` in 表3: the Japanese-to-western quarter em, reducible to an eighth.
417
+ */
418
+ const MIXED_TEXT_QUARTER = {
419
+ kind: "glue",
420
+ naturalWidthEm: .25,
421
+ shrink: {
422
+ priority: REDUCTION_STAGE.mixedText,
423
+ amountEm: .125
424
+ }
425
+ };
426
+ /**
427
+ * 表1 注3 with 表3 注1: two middle dots meet, and their quarters go solid together.
428
+ */
429
+ const TWO_QUARTERS = {
430
+ kind: "glue",
431
+ naturalWidthEm: .5,
432
+ shrink: {
433
+ priority: REDUCTION_STAGE.middleDot,
434
+ amountEm: .5
435
+ }
436
+ };
437
+ /**
438
+ * 表1 注5 with 表3 注2: a full stop's fixed half em plus the following middle dot's quarter.
439
+ */
440
+ const FIXED_HALF_AND_QUARTER = {
441
+ kind: "glue",
442
+ naturalWidthEm: .75,
443
+ shrink: {
444
+ priority: REDUCTION_STAGE.middleDot,
445
+ amountEm: .25
446
+ }
447
+ };
448
+ /**
449
+ * 表1 注5 with 表3 注3: a comma's half em plus the following middle dot's quarter.
450
+ *
451
+ * 表3 spends the quarter at stage 4 and the half at stage 5, which one capacity cannot say. Both are
452
+ * reducible to solid, so the total is right and only the order collapses — and `、・` is rare enough
453
+ * that the order never shows.
454
+ */
455
+ const HALF_AND_QUARTER = {
456
+ kind: "glue",
457
+ naturalWidthEm: .75,
458
+ shrink: {
459
+ priority: REDUCTION_STAGE.punctuation,
460
+ amountEm: .75
461
+ }
462
+ };
463
+ /**
464
+ * 分離禁止文字 repeated (a 2倍ダッシュ, a 2倍リーダ) must read as one continuous rule (3.1.10).
465
+ */
466
+ const DASH_JOINT = {
467
+ kind: "kern",
468
+ naturalWidthEm: 0
469
+ };
470
+ /**
471
+ * The classes that take a quarter em against a numeral, unit symbol or western character (3.2.6).
472
+ * Dividing punctuation is on the list in that direction only, because the space before a `?` or `!`
473
+ * is solid (3.1.6).
474
+ */
475
+ const JAPANESE_BEFORE_WESTERN = [
476
+ "cl-04",
477
+ "cl-09",
478
+ "cl-10",
479
+ "cl-11",
480
+ "cl-15",
481
+ "cl-16",
482
+ "cl-19",
483
+ "cl-30"
484
+ ];
485
+ const JAPANESE_AFTER_WESTERN = [
486
+ "cl-09",
487
+ "cl-10",
488
+ "cl-11",
489
+ "cl-15",
490
+ "cl-16",
491
+ "cl-19",
492
+ "cl-30"
493
+ ];
494
+ const NUMERAL_UNIT_OR_WESTERN = [
495
+ "cl-24",
496
+ "cl-25",
497
+ "cl-27"
498
+ ];
499
+ const PUNCTUATION_TAKING_SPACE_AFTER = [
500
+ "cl-02",
501
+ "cl-06",
502
+ "cl-07"
503
+ ];
504
+ /**
505
+ * The classes JLReq 3.1.2 sets on a half em rather than a full one.
506
+ */
507
+ const HALF_EM_CLASSES = /* @__PURE__ */ new Set([
508
+ "cl-01",
509
+ "cl-02",
510
+ "cl-05",
511
+ "cl-06",
512
+ "cl-07"
513
+ ]);
514
+ const PAIR_SPACING_TABLE = new Map([
515
+ {
516
+ left: "any",
517
+ right: "any",
518
+ spacing: SOLID
519
+ },
520
+ {
521
+ left: "any",
522
+ right: ["cl-01"],
523
+ spacing: HALF
524
+ },
525
+ {
526
+ left: "any",
527
+ right: ["cl-05"],
528
+ spacing: QUARTER
529
+ },
530
+ {
531
+ left: "any",
532
+ right: PUNCTUATION_TAKING_SPACE_AFTER,
533
+ spacing: SOLID
534
+ },
535
+ {
536
+ left: JAPANESE_BEFORE_WESTERN,
537
+ right: NUMERAL_UNIT_OR_WESTERN,
538
+ spacing: MIXED_TEXT_QUARTER
539
+ },
540
+ {
541
+ left: NUMERAL_UNIT_OR_WESTERN,
542
+ right: JAPANESE_AFTER_WESTERN,
543
+ spacing: MIXED_TEXT_QUARTER
544
+ },
545
+ {
546
+ left: ["cl-25"],
547
+ right: ["cl-24"],
548
+ spacing: MIXED_TEXT_QUARTER
549
+ },
550
+ {
551
+ left: ["cl-27"],
552
+ right: ["cl-25"],
553
+ spacing: MIXED_TEXT_QUARTER
554
+ },
555
+ {
556
+ left: ["cl-24"],
557
+ right: ["cl-25"],
558
+ spacing: FIXED_QUARTER
559
+ },
560
+ {
561
+ left: ["cl-02", "cl-07"],
562
+ right: "any",
563
+ spacing: HALF
564
+ },
565
+ {
566
+ left: ["cl-06"],
567
+ right: "any",
568
+ spacing: FIXED_HALF
569
+ },
570
+ {
571
+ left: ["cl-02"],
572
+ right: ["cl-05"],
573
+ spacing: QUARTER
574
+ },
575
+ {
576
+ left: ["cl-06"],
577
+ right: ["cl-05"],
578
+ spacing: FIXED_HALF_AND_QUARTER
579
+ },
580
+ {
581
+ left: ["cl-07"],
582
+ right: ["cl-05"],
583
+ spacing: HALF_AND_QUARTER
584
+ },
585
+ {
586
+ left: PUNCTUATION_TAKING_SPACE_AFTER,
587
+ right: PUNCTUATION_TAKING_SPACE_AFTER,
588
+ spacing: SOLID
589
+ },
590
+ {
591
+ left: ["cl-02"],
592
+ right: ["cl-14"],
593
+ spacing: SOLID
594
+ },
595
+ {
596
+ left: ["cl-05"],
597
+ right: "any",
598
+ spacing: QUARTER
599
+ },
600
+ {
601
+ left: ["cl-05"],
602
+ right: ["cl-05"],
603
+ spacing: TWO_QUARTERS
604
+ },
605
+ {
606
+ left: ["cl-01"],
607
+ right: "any",
608
+ spacing: SOLID
609
+ },
610
+ {
611
+ left: ["cl-01"],
612
+ right: ["cl-05"],
613
+ spacing: QUARTER
614
+ },
615
+ {
616
+ left: ["cl-14"],
617
+ right: "any",
618
+ spacing: SOLID
619
+ },
620
+ {
621
+ left: ["cl-14"],
622
+ right: ["cl-05"],
623
+ spacing: QUARTER
624
+ },
625
+ {
626
+ left: "any",
627
+ right: ["cl-26"],
628
+ spacing: SOLID
629
+ },
630
+ {
631
+ left: PUNCTUATION_TAKING_SPACE_AFTER,
632
+ right: ["cl-26"],
633
+ spacing: FIXED_HALF
634
+ },
635
+ {
636
+ left: ["cl-05"],
637
+ right: ["cl-26"],
638
+ spacing: FIXED_QUARTER
639
+ },
640
+ {
641
+ left: ["cl-26"],
642
+ right: "any",
643
+ spacing: SOLID
644
+ },
645
+ {
646
+ left: ["cl-26"],
647
+ right: ["cl-01"],
648
+ spacing: FIXED_HALF
649
+ },
650
+ {
651
+ left: ["cl-26"],
652
+ right: ["cl-05"],
653
+ spacing: FIXED_QUARTER
654
+ },
655
+ {
656
+ left: ["cl-08"],
657
+ right: ["cl-08"],
658
+ spacing: DASH_JOINT
659
+ }
660
+ ].flatMap(({ left, right, spacing }) => (left === "any" ? japaneseCharacterClasses : left).flatMap((leftClass) => (right === "any" ? japaneseCharacterClasses : right).map((rightClass) => [`${leftClass}/${rightClass}`, spacing]))));
661
+ function pairSpacing(left, right) {
662
+ return PAIR_SPACING_TABLE.get(`${left}/${right}`) ?? SOLID;
663
+ }
664
+ /**
665
+ * `null` removes the break opportunity outright rather than pricing it, because the three rules
666
+ * below are prohibitions and not preferences.
667
+ *
668
+ * Line-end prohibition (行末禁則, JLReq 3.1.8): an opening bracket or a prefixed abbreviation may not
669
+ * be the last character on a line, since it belongs to what follows it.
670
+ *
671
+ * Line-start prohibition (行頭禁則, JLReq 3.1.7): closing brackets, hyphens, dividing punctuation,
672
+ * middle dots, full stops, commas, iteration marks, the prolonged sound mark, small kana and
673
+ * postfixed abbreviations may not open a line.
674
+ *
675
+ * Unbreakable sequences (分割禁止, JLReq 3.1.10): a 分離禁止文字 followed by another one is a single mark set
676
+ * over two ems and must not be split.
677
+ */
678
+ function breakPenalty(left, right) {
679
+ if (left === "cl-01" || left === "cl-12") return null;
680
+ 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;
681
+ if (left === "cl-08" && right === "cl-08") return null;
682
+ return 0;
683
+ }
684
+ const defaultJapaneseTypesettingProfile = {
685
+ classify,
686
+ /**
687
+ * Opening and closing brackets, full stops, commas and middle dots are set on a half em (JLReq
688
+ * 3.1.2); the space the pair table puts around them is what brings each back to a full em.
689
+ *
690
+ * A fullwidth glyph keeps drawing its ink where the font put it, so halving the advance alone
691
+ * would leave the ink outside the box. The offset pulls it back: an opening bracket draws in the
692
+ * trailing half of its em, a middle dot in the centre, and the rest in the leading half.
693
+ */
694
+ boxMetrics: (characterClass, measuredAdvanceEm) => {
695
+ const advanceEm = HALF_EM_CLASSES.has(characterClass) ? Math.min(.5, measuredAdvanceEm) : measuredAdvanceEm;
696
+ const inkOverhangEm = measuredAdvanceEm - advanceEm;
697
+ return {
698
+ advanceEm,
699
+ renderOffsetEm: characterClass === "cl-01" ? -inkOverhangEm : characterClass === "cl-05" ? -inkOverhangEm / 2 : 0
700
+ };
701
+ },
702
+ pairSpacing,
703
+ /**
704
+ * An opening bracket at the line head keeps a half em of white, so a line of dialogue starts one
705
+ * half em in rather than tight against the edge. JLReq 3.1.5 lists three schemes for that white
706
+ * and 表1 注17 sets solid as the principle; the half em is the variant the publishers of Japanese
707
+ * novels use for a line that begins a paragraph.
708
+ *
709
+ * The white is not shrinkable. 表3 excludes the line head from line adjustment altogether, and
710
+ * shrinking it is exactly what pushed a line-head bracket flush against the edge in #94.
711
+ */
712
+ lineStartSpacing: (first) => first === "cl-01" ? {
713
+ kind: "glue",
714
+ naturalWidthEm: .5
715
+ } : null,
716
+ /**
717
+ * The half em trailing a closing bracket, full stop or comma at the line end, and the quarter em
718
+ * trailing a middle dot there (JLReq 3.1.9). Priority 0: spending it changes nothing the reader
719
+ * can see.
720
+ */
721
+ lineEndSpacing: (last) => last === "cl-02" || last === "cl-06" || last === "cl-07" ? {
722
+ kind: "glue",
723
+ naturalWidthEm: .5,
724
+ shrink: {
725
+ priority: REDUCTION_STAGE.lineEnd,
726
+ amountEm: .5
727
+ }
728
+ } : last === "cl-05" ? {
729
+ kind: "glue",
730
+ naturalWidthEm: .25,
731
+ shrink: {
732
+ priority: REDUCTION_STAGE.lineEnd,
733
+ amountEm: .25
734
+ }
735
+ } : null,
736
+ breakPenalty,
737
+ /**
738
+ * Hanging punctuation (ぶら下げ組, JLReq 3.8.2) lets a full stop or comma sit outside the text area
739
+ * instead of forcing the line to be respaced. JLReq admits no other class for it.
740
+ */
741
+ canHang: (characterClass) => characterClass === "cl-07" || characterClass === "cl-06"
742
+ };
743
+ //#endregion
744
+ //#region src/composer/internal/paragraph-layout.ts
745
+ const EPSILON = 1e-9;
746
+ /**
747
+ * Spending capacity at this priority changes nothing the reader can see.
748
+ */
749
+ const FREE_SHRINK_PRIORITY = 0;
750
+ function skipSourceGaps(atoms, start) {
751
+ let cursor = start;
752
+ while (atoms[cursor]?.sourceGap === true) cursor += 1;
753
+ return cursor;
754
+ }
755
+ function previousVisible(atoms, end) {
756
+ for (let index = end - 1; index >= 0; index -= 1) if (atoms[index]?.sourceGap === false) return index;
757
+ }
758
+ function opportunities(atoms, classes, start, end, profile) {
759
+ const result = [];
760
+ const firstClass = classes[start];
761
+ const startSpacing = firstClass === void 0 ? null : profile.lineStartSpacing(firstClass);
762
+ if (startSpacing !== null) result.push({
763
+ boundary: start,
764
+ spacing: startSpacing
765
+ });
766
+ for (let right = start + 1; right < end; right += 1) {
767
+ const leftAtom = atoms[right - 1];
768
+ const rightAtom = atoms[right];
769
+ const leftClass = classes[right - 1];
770
+ const rightClass = classes[right];
771
+ if (leftAtom === void 0 || rightAtom === void 0 || leftClass === void 0 || rightClass === void 0 || leftAtom.sourceGap || rightAtom.sourceGap || !leftAtom.pairSpacingAfter) continue;
772
+ result.push({
773
+ boundary: right,
774
+ spacing: profile.pairSpacing(leftClass, rightClass)
775
+ });
776
+ }
777
+ const lastClass = classes[end - 1];
778
+ const endSpacing = lastClass === void 0 ? null : profile.lineEndSpacing(lastClass);
779
+ if (endSpacing !== null) result.push({
780
+ boundary: end,
781
+ spacing: endSpacing
782
+ });
783
+ return result;
784
+ }
785
+ function capacity(values, select) {
786
+ return values.reduce((total, opportunity) => total + (select(opportunity.spacing)?.amountEm ?? 0), 0);
787
+ }
788
+ function resolveSpacings(values, adjustment, amountEm) {
789
+ const remainingByBoundary = /* @__PURE__ */ new Map();
790
+ let remaining = amountEm;
791
+ let priorityCost = 0;
792
+ const capacities = values.flatMap((opportunity) => {
793
+ const selected = adjustment === "shrink" ? opportunity.spacing.shrink : opportunity.spacing.stretch;
794
+ return selected === void 0 ? [] : [{
795
+ ...opportunity,
796
+ capacity: selected
797
+ }];
798
+ }).sort((left, right) => left.capacity.priority - right.capacity.priority || left.boundary - right.boundary);
799
+ for (const opportunity of capacities) {
800
+ if (remaining <= EPSILON) break;
801
+ const used = Math.min(remaining, opportunity.capacity.amountEm);
802
+ remainingByBoundary.set(opportunity.boundary, used);
803
+ priorityCost += used * opportunity.capacity.priority;
804
+ remaining -= used;
805
+ }
806
+ return {
807
+ spacings: values.map(({ boundary, spacing }) => {
808
+ const used = remainingByBoundary.get(boundary) ?? 0;
809
+ return {
810
+ boundary,
811
+ kind: spacing.kind,
812
+ naturalWidthEm: spacing.naturalWidthEm,
813
+ widthEm: adjustment === "shrink" ? spacing.naturalWidthEm - used : spacing.naturalWidthEm + used
814
+ };
815
+ }),
816
+ priorityCost
817
+ };
818
+ }
819
+ function candidate(atoms, classes, start, end, lineLengthEm, profile) {
820
+ const contentStart = skipSourceGaps(atoms, start);
821
+ const suppressedIndexes = Array.from({ length: contentStart - start }, (_, index) => start + index);
822
+ const pairValues = opportunities(atoms, classes, contentStart, end, profile);
823
+ const naturalSizeEm = atoms.slice(contentStart, end).reduce((total, atom) => total + atom.boxAdvanceEm, 0) + pairValues.reduce((total, value) => total + value.spacing.naturalWidthEm, 0);
824
+ const terminal = skipSourceGaps(atoms, end) === atoms.length;
825
+ const shrinkCapacity = capacity(pairValues, ({ shrink }) => shrink);
826
+ const freeShrinkCapacity = capacity(pairValues, ({ shrink }) => shrink?.priority === FREE_SHRINK_PRIORITY ? shrink : void 0);
827
+ const stretchCapacity = capacity(pairValues, ({ stretch }) => stretch);
828
+ const overflow = naturalSizeEm - lineLengthEm;
829
+ const underflow = lineLengthEm - naturalSizeEm;
830
+ const lastVisible = previousVisible(atoms, end);
831
+ const lastClass = lastVisible === void 0 ? void 0 : classes[lastVisible];
832
+ const lastAdvance = lastVisible === void 0 ? 0 : atoms[lastVisible]?.boxAdvanceEm ?? 0;
833
+ const trailingSpacing = pairValues.find(({ boundary }) => boundary === end)?.spacing.naturalWidthEm ?? 0;
834
+ if (terminal && overflow <= EPSILON) return {
835
+ start,
836
+ contentStart,
837
+ end,
838
+ suppressedIndexes,
839
+ pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings,
840
+ inlineSizeEm: naturalSizeEm,
841
+ break: { kind: "paragraph-end" },
842
+ hangingIndex: null,
843
+ deformationRatio: 0,
844
+ priorityCost: 0
845
+ };
846
+ if (Math.abs(overflow) <= EPSILON) return {
847
+ start,
848
+ contentStart,
849
+ end,
850
+ suppressedIndexes,
851
+ pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings,
852
+ inlineSizeEm: naturalSizeEm,
853
+ break: { kind: "natural" },
854
+ hangingIndex: null,
855
+ deformationRatio: 0,
856
+ priorityCost: 0
857
+ };
858
+ if (overflow > 0 && overflow <= shrinkCapacity + EPSILON) {
859
+ const resolved = resolveSpacings(pairValues, "shrink", overflow);
860
+ const chargeableCapacity = shrinkCapacity - freeShrinkCapacity;
861
+ const chargeableShrink = Math.max(0, overflow - freeShrinkCapacity);
862
+ return {
863
+ start,
864
+ contentStart,
865
+ end,
866
+ suppressedIndexes,
867
+ pairSpacings: resolved.spacings,
868
+ inlineSizeEm: lineLengthEm,
869
+ break: { kind: "shrunk" },
870
+ hangingIndex: null,
871
+ deformationRatio: chargeableCapacity <= EPSILON ? 0 : chargeableShrink / chargeableCapacity,
872
+ priorityCost: resolved.priorityCost
873
+ };
874
+ }
875
+ if (!terminal && overflow > 0 && lastVisible !== void 0 && lastClass !== void 0 && profile.canHang(lastClass) && naturalSizeEm - lastAdvance - trailingSpacing <= lineLengthEm + EPSILON) return {
876
+ start,
877
+ contentStart,
878
+ end,
879
+ suppressedIndexes,
880
+ pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings.filter(({ boundary }) => boundary !== end),
881
+ inlineSizeEm: naturalSizeEm - lastAdvance - trailingSpacing,
882
+ break: { kind: "hanging" },
883
+ hangingIndex: lastVisible,
884
+ deformationRatio: overflow / Math.max(lastAdvance, EPSILON),
885
+ priorityCost: 0
886
+ };
887
+ if (!terminal && underflow > EPSILON && underflow <= stretchCapacity + EPSILON) {
888
+ const resolved = resolveSpacings(pairValues, "stretch", underflow);
889
+ return {
890
+ start,
891
+ contentStart,
892
+ end,
893
+ suppressedIndexes,
894
+ pairSpacings: resolved.spacings,
895
+ inlineSizeEm: lineLengthEm,
896
+ break: { kind: "stretched" },
897
+ hangingIndex: null,
898
+ deformationRatio: stretchCapacity === 0 ? 0 : underflow / stretchCapacity,
899
+ priorityCost: resolved.priorityCost
900
+ };
901
+ }
902
+ return {
903
+ start,
904
+ contentStart,
905
+ end,
906
+ suppressedIndexes,
907
+ pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings,
908
+ inlineSizeEm: naturalSizeEm,
909
+ break: { kind: "forced" },
910
+ hangingIndex: null,
911
+ deformationRatio: Math.abs(naturalSizeEm - lineLengthEm) / Math.max(lineLengthEm, EPSILON),
912
+ priorityCost: 0
913
+ };
914
+ }
915
+ function scoreFor(line, previousFitness) {
916
+ const mode = line.break.kind;
917
+ const fitness = Math.min(3, Math.floor(line.deformationRatio * 4));
918
+ const transition = Math.abs(previousFitness - fitness) > 1 ? 1 : 0;
919
+ const modeCounts = {
920
+ forced: mode === "forced" ? 1 : 0,
921
+ stretched: mode === "stretched" ? 1 : 0,
922
+ hanging: mode === "hanging" ? 1 : 0,
923
+ shrunk: mode === "shrunk" && line.priorityCost > EPSILON ? 1 : 0
924
+ };
925
+ return {
926
+ score: [
927
+ modeCounts.forced,
928
+ modeCounts.stretched,
929
+ modeCounts.hanging,
930
+ modeCounts.shrunk,
931
+ line.priorityCost,
932
+ line.deformationRatio ** 3,
933
+ transition
934
+ ],
935
+ fitness
936
+ };
937
+ }
938
+ function addScore(left, right) {
939
+ return [
940
+ left[0] + right[0],
941
+ left[1] + right[1],
942
+ left[2] + right[2],
943
+ left[3] + right[3],
944
+ left[4] + right[4],
945
+ left[5] + right[5],
946
+ left[6] + right[6]
947
+ ];
948
+ }
949
+ function compareBreaks(left, right) {
950
+ const length = Math.min(left.length, right.length);
951
+ for (let index = 0; index < length; index += 1) {
952
+ const difference = (right[index] ?? 0) - (left[index] ?? 0);
953
+ if (difference !== 0) return difference;
954
+ }
955
+ return left.length - right.length;
956
+ }
957
+ function isBetter(candidateState, current) {
958
+ if (current === void 0) return true;
959
+ for (let index = 0; index < candidateState.score.length; index += 1) {
960
+ const difference = (candidateState.score[index] ?? 0) - (current.score[index] ?? 0);
961
+ if (Math.abs(difference) > EPSILON) return difference < 0;
962
+ }
963
+ return compareBreaks(candidateState.breaks, current.breaks) < 0;
964
+ }
965
+ function layoutParagraph(atoms, lineLengthEm, profile, boundaryAllowed) {
966
+ if (atoms.length === 0) return [];
967
+ const classes = atoms.map(({ characterClass }) => characterClass);
968
+ const states = /* @__PURE__ */ new Map([[0, /* @__PURE__ */ new Map([[0, {
969
+ score: [
970
+ 0,
971
+ 0,
972
+ 0,
973
+ 0,
974
+ 0,
975
+ 0,
976
+ 0
977
+ ],
978
+ fitness: 0,
979
+ previous: null,
980
+ line: null,
981
+ breaks: []
982
+ }]])]]);
983
+ for (let start = 0; start < atoms.length; start += 1) {
984
+ const activeStates = states.get(start);
985
+ if (activeStates === void 0) continue;
986
+ const contentStart = skipSourceGaps(atoms, start);
987
+ if (contentStart === atoms.length) {
988
+ const terminalStates = states.get(atoms.length) ?? /* @__PURE__ */ new Map();
989
+ for (const state of activeStates.values()) if (isBetter(state, terminalStates.get(state.fitness))) terminalStates.set(state.fitness, state);
990
+ states.set(atoms.length, terminalStates);
991
+ continue;
992
+ }
993
+ for (const state of activeStates.values()) for (let end = contentStart + 1; end <= atoms.length; end += 1) {
994
+ const right = skipSourceGaps(atoms, end);
995
+ const left = previousVisible(atoms, end);
996
+ if (atoms[end - 1]?.sourceGap === true) continue;
997
+ if (end < atoms.length && right < atoms.length && (left === void 0 || !boundaryAllowed(left, right))) continue;
998
+ const line = candidate(atoms, classes, start, end, lineLengthEm, profile);
999
+ 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))) {
1000
+ if (line.inlineSizeEm > lineLengthEm * 2 && end > contentStart + 1) break;
1001
+ continue;
1002
+ }
1003
+ const lineScore = scoreFor(line, state.fitness);
1004
+ const nextState = {
1005
+ score: addScore(state.score, lineScore.score),
1006
+ fitness: lineScore.fitness,
1007
+ previous: {
1008
+ index: start,
1009
+ fitness: state.fitness
1010
+ },
1011
+ line,
1012
+ breaks: [...state.breaks, end]
1013
+ };
1014
+ const statesAtEnd = states.get(end) ?? /* @__PURE__ */ new Map();
1015
+ if (isBetter(nextState, statesAtEnd.get(nextState.fitness))) {
1016
+ statesAtEnd.set(nextState.fitness, nextState);
1017
+ states.set(end, statesAtEnd);
1018
+ }
1019
+ if (line.inlineSizeEm > lineLengthEm * 2 && line.break.kind === "forced" && end > contentStart + 1) break;
1020
+ }
1021
+ }
1022
+ const terminal = [...states.get(atoms.length)?.values() ?? []].reduce((best, state) => isBetter(state, best) ? state : best, void 0);
1023
+ if (terminal === void 0) return [];
1024
+ const lines = [];
1025
+ let cursor = terminal;
1026
+ while (cursor?.line !== null && cursor?.line !== void 0) {
1027
+ lines.push(cursor.line);
1028
+ cursor = cursor.previous === null ? void 0 : states.get(cursor.previous.index)?.get(cursor.previous.fitness);
1029
+ }
1030
+ lines.reverse();
1031
+ const last = lines.at(-1);
1032
+ if (last !== void 0 && last.end < atoms.length) lines[lines.length - 1] = {
1033
+ ...last,
1034
+ suppressedIndexes: [...last.suppressedIndexes, ...Array.from({ length: atoms.length - last.end }, (_, index) => last.end + index)]
1035
+ };
1036
+ return lines;
1037
+ }
1038
+ //#endregion
1039
+ //#region src/appearance/paper-size.ts
1040
+ const PAPER_SIZES = {
1041
+ a4: {
1042
+ id: "a4",
1043
+ label: "A4",
1044
+ widthMm: 210,
1045
+ heightMm: 297
1046
+ },
1047
+ a5: {
1048
+ id: "a5",
1049
+ label: "A5",
1050
+ widthMm: 148,
1051
+ heightMm: 210
1052
+ },
1053
+ a6: {
1054
+ id: "a6",
1055
+ label: "A6(文庫)",
1056
+ widthMm: 105,
1057
+ heightMm: 148
1058
+ },
1059
+ "jis-b5": {
1060
+ id: "jis-b5",
1061
+ label: "B5(JIS)",
1062
+ widthMm: 182,
1063
+ heightMm: 257
1064
+ },
1065
+ "jis-b6": {
1066
+ id: "jis-b6",
1067
+ label: "B6(JIS)",
1068
+ widthMm: 128,
1069
+ heightMm: 182
1070
+ },
1071
+ shinsho: {
1072
+ id: "shinsho",
1073
+ label: "新書",
1074
+ widthMm: 106,
1075
+ heightMm: 173
1076
+ }
1077
+ };
1078
+ const PaperSize = {
1079
+ of: (id) => PAPER_SIZES[id],
1080
+ all: Object.values(PAPER_SIZES)
1081
+ };
1082
+ //#endregion
1083
+ //#region src/internal/length.ts
1084
+ const PT_PER_MM = 72 / 25.4;
1085
+ function ptToMm(pt) {
1086
+ return pt / PT_PER_MM;
1087
+ }
1088
+ function mmToPt(mm) {
1089
+ return mm * PT_PER_MM;
1090
+ }
1091
+ //#endregion
1092
+ //#region src/composer/manuscript-geometry.ts
1093
+ const STAGE_GAP_CELLS = 2;
1094
+ const LINE_GAP_CELLS = .5;
1095
+ const nonNegativeLength = () => v.pipe(v.number(), v.finite(), v.minValue(0));
1096
+ const ManuscriptGeometrySchema = readonlyObject({
1097
+ paperWidthMm: nonNegativeLength(),
1098
+ paperHeightMm: nonNegativeLength(),
1099
+ cellSizeMm: nonNegativeLength(),
1100
+ lineGapMm: nonNegativeLength(),
1101
+ fontSizePt: FontSizePt.schema,
1102
+ contentWidthMm: nonNegativeLength(),
1103
+ contentHeightMm: nonNegativeLength(),
1104
+ marginInlineMm: v.pipe(v.number(), v.finite()),
1105
+ marginBlockMm: v.pipe(v.number(), v.finite()),
1106
+ fitsPaper: v.boolean()
1107
+ });
1108
+ /**
1109
+ * Every stage stacked vertically, plus the gap between adjacent stages.
1110
+ */
1111
+ function contentHeightInEm(flow) {
1112
+ return flow.stagesPerPage * flow.lineLengthEm + STAGE_GAP_CELLS * (flow.stagesPerPage - 1);
1113
+ }
1114
+ /**
1115
+ * Every vertical line side by side, plus the gap between adjacent lines.
1116
+ */
1117
+ function contentWidthInEm(flow) {
1118
+ return flow.linesPerStage + LINE_GAP_CELLS * (flow.linesPerStage - 1);
1119
+ }
1120
+ const ManuscriptGeometry = {
1121
+ schema: ManuscriptGeometrySchema,
1122
+ of: (flow, appearance) => {
1123
+ const paper = PaperSize.of(appearance.paperSize);
1124
+ const cellSizeMm = ptToMm(appearance.fontSizePt);
1125
+ const contentWidthMm = contentWidthInEm(flow) * cellSizeMm;
1126
+ const contentHeightMm = contentHeightInEm(flow) * cellSizeMm;
1127
+ const marginInlineMm = (paper.widthMm - contentWidthMm) / 2;
1128
+ const marginBlockMm = (paper.heightMm - contentHeightMm) / 2;
1129
+ return {
1130
+ paperWidthMm: paper.widthMm,
1131
+ paperHeightMm: paper.heightMm,
1132
+ cellSizeMm,
1133
+ lineGapMm: LINE_GAP_CELLS * cellSizeMm,
1134
+ fontSizePt: appearance.fontSizePt,
1135
+ contentWidthMm,
1136
+ contentHeightMm,
1137
+ marginInlineMm,
1138
+ marginBlockMm,
1139
+ fitsPaper: marginInlineMm >= 0 && marginBlockMm >= 0
1140
+ };
1141
+ },
1142
+ /**
1143
+ * The largest step-aligned point size whose text area still fits the paper.
1144
+ */
1145
+ maxFontSizePt: (flow, paperSizeId) => {
1146
+ const paper = PaperSize.of(paperSizeId);
1147
+ const maxCellSizeMm = Math.min(paper.widthMm / contentWidthInEm(flow), paper.heightMm / contentHeightInEm(flow));
1148
+ const steps = Math.floor(mmToPt(maxCellSizeMm) / FontSizePt.range.step + 1e-9);
1149
+ const aligned = Math.round(steps * FontSizePt.range.step * 1e3) / 1e3;
1150
+ return Math.min(FontSizePt.range.max, Math.max(FontSizePt.range.min, aligned));
1151
+ }
1152
+ };
1153
+ const CompositionStatistics = { schema: readonlyObject({
1154
+ chars: nonNegativeInteger(),
1155
+ sourceLines: nonNegativeInteger(),
1156
+ pages: nonNegativeInteger()
1157
+ }) };
1158
+ //#endregion
1159
+ //#region src/composer/composed-annotation-fragment.ts
1160
+ const emLength = () => v.pipe(v.number(), v.finite(), v.minValue(0));
1161
+ const continuation = v.union([
1162
+ v.literal("whole"),
1163
+ v.literal("start"),
1164
+ v.literal("middle"),
1165
+ v.literal("end")
1166
+ ]);
1167
+ const PositionedReadingGraphemeSchema = readonlyObject({
1168
+ value: v.string(),
1169
+ offsetEm: v.pipe(v.number(), v.finite()),
1170
+ advanceEm: emLength()
1171
+ });
1172
+ const common$1 = {
1173
+ annotationRange: ManuscriptRange.schema,
1174
+ fragmentRange: ManuscriptRange.schema,
1175
+ continuation
1176
+ };
1177
+ const ComposedAnnotationFragment = { schema: v.variant("kind", [
1178
+ readonlyObject({
1179
+ kind: v.literal("bold"),
1180
+ ...common$1
1181
+ }),
1182
+ readonlyObject({
1183
+ kind: v.literal("italic"),
1184
+ ...common$1
1185
+ }),
1186
+ readonlyObject({
1187
+ kind: v.literal("emphasis"),
1188
+ mark: v.string(),
1189
+ ...common$1
1190
+ }),
1191
+ readonlyObject({
1192
+ kind: v.literal("ruby"),
1193
+ rubyKind: RubyKind.schema,
1194
+ reading: v.string(),
1195
+ baseOffsetEm: emLength(),
1196
+ baseAdvanceEm: emLength(),
1197
+ readingGraphemes: readonlyArray(PositionedReadingGraphemeSchema),
1198
+ ...common$1
1199
+ })
1200
+ ]) };
1201
+ const InlineSpan = { schema: readonlyObject({
1202
+ offsetEm: v.pipe(v.number(), v.finite()),
1203
+ advanceEm: v.pipe(v.number(), v.finite(), v.minValue(0))
1204
+ }) };
1205
+ //#endregion
1206
+ //#region src/composer/vertical-text-presentation.ts
1207
+ const common = { groupRange: ManuscriptRange.schema };
1208
+ const VerticalTextPresentation = { schema: v.variant("kind", [
1209
+ readonlyObject({
1210
+ kind: v.literal("mixed"),
1211
+ ...common
1212
+ }),
1213
+ readonlyObject({
1214
+ kind: v.literal("upright"),
1215
+ ...common
1216
+ }),
1217
+ readonlyObject({
1218
+ kind: v.literal("sideways"),
1219
+ ...common
1220
+ }),
1221
+ readonlyObject({
1222
+ kind: v.literal("tate-chu-yoko"),
1223
+ ...common
1224
+ })
1225
+ ]) };
1226
+ //#endregion
1227
+ //#region src/composer/composed-inline-item.ts
1228
+ const signedEm = () => v.pipe(v.number(), v.finite());
1229
+ const GlyphItemSchema = readonlyObject({
1230
+ kind: v.literal("glyph"),
1231
+ value: v.string(),
1232
+ range: ManuscriptRange.schema,
1233
+ layoutSpan: InlineSpan.schema,
1234
+ renderSpan: InlineSpan.schema,
1235
+ disposition: v.union([v.literal("placed"), v.literal("hanging")]),
1236
+ presentation: VerticalTextPresentation.schema
1237
+ });
1238
+ const GeneratedGlueItemSchema = readonlyObject({
1239
+ kind: v.literal("glue"),
1240
+ origin: v.literal("generated"),
1241
+ offsetEm: signedEm(),
1242
+ widthEm: signedEm(),
1243
+ naturalWidthEm: signedEm(),
1244
+ adjustment: v.union([
1245
+ v.literal("natural"),
1246
+ v.literal("shrunk"),
1247
+ v.literal("stretched")
1248
+ ])
1249
+ });
1250
+ const SourceGlueItemSchema = readonlyObject({
1251
+ kind: v.literal("glue"),
1252
+ origin: v.literal("source"),
1253
+ value: v.string(),
1254
+ range: ManuscriptRange.schema,
1255
+ offsetEm: signedEm(),
1256
+ widthEm: signedEm(),
1257
+ naturalWidthEm: signedEm(),
1258
+ adjustment: v.union([
1259
+ v.literal("natural"),
1260
+ v.literal("shrunk"),
1261
+ v.literal("stretched")
1262
+ ])
1263
+ });
1264
+ const KernItemSchema = readonlyObject({
1265
+ kind: v.literal("kern"),
1266
+ offsetEm: signedEm(),
1267
+ widthEm: signedEm()
1268
+ });
1269
+ const SuppressedItemSchema = readonlyObject({
1270
+ kind: v.literal("suppressed"),
1271
+ value: v.string(),
1272
+ range: ManuscriptRange.schema,
1273
+ reason: v.literal("question-or-exclamation-gap")
1274
+ });
1275
+ const ComposedInlineItem = { schema: v.union([
1276
+ GlyphItemSchema,
1277
+ GeneratedGlueItemSchema,
1278
+ SourceGlueItemSchema,
1279
+ KernItemSchema,
1280
+ SuppressedItemSchema
1281
+ ]) };
1282
+ const LineBreakResult = { schema: v.variant("kind", [
1283
+ readonlyObject({ kind: v.literal("natural") }),
1284
+ readonlyObject({ kind: v.literal("shrunk") }),
1285
+ readonlyObject({ kind: v.literal("stretched") }),
1286
+ readonlyObject({ kind: v.literal("hanging") }),
1287
+ readonlyObject({ kind: v.literal("forced") }),
1288
+ readonlyObject({ kind: v.literal("paragraph-end") })
1289
+ ]) };
1290
+ const NovelLine = {
1291
+ schema: readonlyObject({
1292
+ range: v.nullable(ManuscriptRange.schema),
1293
+ inlineSizeEm: v.pipe(v.number(), v.finite(), v.minValue(0)),
1294
+ items: readonlyArray(ComposedInlineItem.schema),
1295
+ break: LineBreakResult.schema,
1296
+ annotations: readonlyArray(ComposedAnnotationFragment.schema)
1297
+ }),
1298
+ empty: () => ({
1299
+ range: null,
1300
+ inlineSizeEm: 0,
1301
+ items: [],
1302
+ break: { kind: "paragraph-end" },
1303
+ annotations: []
1304
+ })
1305
+ };
1306
+ const NovelStage = {
1307
+ schema: readonlyObject({
1308
+ range: v.nullable(ManuscriptRange.schema),
1309
+ lines: readonlyArray(NovelLine.schema)
1310
+ }),
1311
+ of: (lines) => ({
1312
+ lines,
1313
+ range: ManuscriptRange.merge(lines.map(({ range }) => range))
1314
+ })
1315
+ };
1316
+ const NovelPage = {
1317
+ schema: readonlyObject({
1318
+ range: v.nullable(ManuscriptRange.schema),
1319
+ stages: readonlyArray(NovelStage.schema)
1320
+ }),
1321
+ of: (stages) => ({
1322
+ stages,
1323
+ range: ManuscriptRange.merge(stages.map(({ range }) => range))
1324
+ })
1325
+ };
1326
+ const NovelLayout = { schema: readonlyObject({
1327
+ pages: readonlyArray(NovelPage.schema),
1328
+ geometry: ManuscriptGeometry.schema,
1329
+ stats: CompositionStatistics.schema
1330
+ }) };
1331
+ //#endregion
1332
+ //#region src/composer/novel-composer.ts
1333
+ const COMPOSER_ID = NamespacedId.of("kg/novel");
1334
+ const TYPESETTING_PROFILE = defaultJapaneseTypesettingProfile;
1335
+ const ASCII_ALPHANUMERIC = /^[A-Za-z0-9]$/u;
1336
+ const ASCII_TWO_DIGITS = /^[0-9]{2}$/u;
1337
+ const UPRIGHT_LATIN_ABBREVIATION = /^(?:[A-Z]+|[A-Z][a-z]{1,2})$/u;
1338
+ const FULLWIDTH_ALPHANUMERIC = /^[A-Za-z0-9]$/u;
1339
+ function presentation(kind, graphemes) {
1340
+ const groupRange = ManuscriptRange.merge(graphemes.map(({ range }) => range));
1341
+ return groupRange === null ? void 0 : {
1342
+ kind,
1343
+ groupRange
1344
+ };
1345
+ }
1346
+ function presentedSourceLine(sourceLine) {
1347
+ const presented = [];
1348
+ let cursor = 0;
1349
+ while (cursor < sourceLine.length) {
1350
+ const first = sourceLine[cursor];
1351
+ if (first === void 0) break;
1352
+ if (!ASCII_ALPHANUMERIC.test(first.value)) {
1353
+ const itemPresentation = presentation(FULLWIDTH_ALPHANUMERIC.test(first.value) ? "upright" : "mixed", [first]);
1354
+ if (itemPresentation !== void 0) presented.push({
1355
+ grapheme: first,
1356
+ presentation: itemPresentation
1357
+ });
1358
+ cursor += 1;
1359
+ continue;
1360
+ }
1361
+ const start = cursor;
1362
+ while (ASCII_ALPHANUMERIC.test(sourceLine[cursor]?.value ?? "")) cursor += 1;
1363
+ const run = sourceLine.slice(start, cursor);
1364
+ const text = run.map(({ value }) => value).join("");
1365
+ const runPresentation = presentation(ASCII_TWO_DIGITS.test(text) ? "tate-chu-yoko" : text.length === 1 || UPRIGHT_LATIN_ABBREVIATION.test(text) ? "upright" : "sideways", run);
1366
+ if (runPresentation !== void 0) presented.push(...run.map((grapheme) => ({
1367
+ grapheme,
1368
+ presentation: runPresentation
1369
+ })));
1370
+ }
1371
+ return presented;
1372
+ }
1373
+ function displayedLines(graphemes) {
1374
+ const lines = [[]];
1375
+ let endedWithLineBreak = false;
1376
+ for (const grapheme of graphemes) {
1377
+ if (grapheme.value === "\n" || grapheme.value === "\r" || grapheme.value === "\r\n") {
1378
+ lines.push([]);
1379
+ endedWithLineBreak = true;
1380
+ continue;
1381
+ }
1382
+ lines.at(-1)?.push(grapheme);
1383
+ endedWithLineBreak = false;
1384
+ }
1385
+ if (lines.length > 1 && endedWithLineBreak) lines.pop();
1386
+ return lines;
1387
+ }
1388
+ function validGapIndexes(sourceLine) {
1389
+ const line = sourceLine.map(({ value }) => value).join("");
1390
+ const gapStarts = new Set(questionOrExclamationSpacings(line).filter((spacing) => spacing.kind === "valid").map(({ gap }) => gap.start));
1391
+ const indexes = /* @__PURE__ */ new Set();
1392
+ let offset = 0;
1393
+ for (const [index, grapheme] of sourceLine.entries()) {
1394
+ if (gapStarts.has(offset)) indexes.add(index);
1395
+ offset += grapheme.value.length;
1396
+ }
1397
+ return indexes;
1398
+ }
1399
+ function indicesInside(atoms, annotation) {
1400
+ return atoms.flatMap(({ grapheme }, index) => ManuscriptRange.overlaps(grapheme.range, annotation.range) ? [index] : []);
1401
+ }
1402
+ function measure(measurer, text, role, settings, presentationKind) {
1403
+ const context = {
1404
+ text,
1405
+ fontPreset: settings.appearance.fontPreset,
1406
+ writingMode: "vertical-rl"
1407
+ };
1408
+ const measurement = role === "base" ? measurer({
1409
+ ...context,
1410
+ role,
1411
+ presentation: presentationKind ?? "mixed"
1412
+ }) : measurer({
1413
+ ...context,
1414
+ role
1415
+ });
1416
+ const parsed = v.safeParse(InlineMeasurement.schema, measurement);
1417
+ if (!parsed.success) return void 0;
1418
+ const { advanceEm } = parsed.output;
1419
+ return advanceEm;
1420
+ }
1421
+ /**
1422
+ * Widen a base character by `extraEm` so a reading longer than it has room, keeping the character's
1423
+ * ink in the middle of the widened box.
1424
+ *
1425
+ * JLReq 3.3.6 sets such a reading solid and spends the surplus on the base instead: two units
1426
+ * between the base characters for one unit before the first and after the last. Handing every base
1427
+ * character an equal share of the surplus and centring it inside that share produces exactly those
1428
+ * proportions, and it is also the 中付き position JLReq 3.3.5 asks of a mono ruby whose reading
1429
+ * outruns its single base character.
1430
+ */
1431
+ function widenForReading(atom, extraEm) {
1432
+ if (extraEm <= 0) return;
1433
+ atom.boxAdvanceEm += extraEm;
1434
+ atom.renderOffsetEm += extraEm / 2;
1435
+ }
1436
+ function measureSourceLine(sourceLine, annotations, settings, measurer) {
1437
+ const mutable = presentedSourceLine(sourceLine).map(({ grapheme, presentation }) => {
1438
+ const renderAdvanceEm = measure(measurer, grapheme.value, "base", settings, presentation.kind);
1439
+ if (renderAdvanceEm === void 0) return void 0;
1440
+ const characterClass = TYPESETTING_PROFILE.classify({
1441
+ value: grapheme.value,
1442
+ presentation: presentation.kind
1443
+ });
1444
+ const metrics = TYPESETTING_PROFILE.boxMetrics(characterClass, renderAdvanceEm);
1445
+ return {
1446
+ grapheme,
1447
+ boxAdvanceEm: metrics.advanceEm,
1448
+ renderAdvanceEm,
1449
+ renderOffsetEm: metrics.renderOffsetEm,
1450
+ presentation
1451
+ };
1452
+ });
1453
+ if (mutable.some((atom) => atom === void 0)) return void 0;
1454
+ const atoms = mutable.flatMap((atom) => atom === void 0 ? [] : [atom]);
1455
+ for (const annotation of annotations) {
1456
+ if (annotation.kind !== "ruby") continue;
1457
+ const indexes = indicesInside(atoms, annotation);
1458
+ if (indexes.length === 0) continue;
1459
+ 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;
1460
+ if (annotation.reading.kind === "group") {
1461
+ const readingAdvance = measure(measurer, annotation.reading.text, "ruby", settings);
1462
+ if (readingAdvance === void 0) return void 0;
1463
+ const baseAdvance = indexes.reduce((total, index) => total + (atoms[index]?.boxAdvanceEm ?? 0), 0);
1464
+ const extra = Math.max(0, readingAdvance - baseAdvance) / indexes.length;
1465
+ for (const index of indexes) {
1466
+ const atom = atoms[index];
1467
+ if (atom !== void 0) widenForReading(atom, extra);
1468
+ }
1469
+ continue;
1470
+ }
1471
+ for (const [segmentIndex, index] of indexes.entries()) {
1472
+ const segment = annotation.reading.segments[segmentIndex];
1473
+ const atom = atoms[index];
1474
+ if (segment === void 0 || atom === void 0) continue;
1475
+ const readingAdvance = measure(measurer, segment, "ruby", settings);
1476
+ if (readingAdvance === void 0) return void 0;
1477
+ widenForReading(atom, readingAdvance - atom.boxAdvanceEm);
1478
+ }
1479
+ }
1480
+ return {
1481
+ atoms,
1482
+ suppressedIndexes: validGapIndexes(sourceLine)
1483
+ };
1484
+ }
1485
+ function breakInsideFittableGroupRuby(atoms, annotations, boundary, lineLengthEm) {
1486
+ const right = atoms[boundary];
1487
+ if (right === void 0) return false;
1488
+ const graphemeIndex = right.grapheme.range.graphemes.start;
1489
+ return annotations.some((annotation) => {
1490
+ if (annotation.kind !== "ruby" || annotation.reading.kind !== "group") return false;
1491
+ if (graphemeIndex <= annotation.range.graphemes.start || graphemeIndex >= annotation.range.graphemes.end) return false;
1492
+ return indicesInside(atoms, annotation).reduce((total, index) => total + (atoms[index]?.boxAdvanceEm ?? 0), 0) <= lineLengthEm;
1493
+ });
1494
+ }
1495
+ function samePresentationGroup(left, right) {
1496
+ return left.presentation.groupRange.graphemes.start === right.presentation.groupRange.graphemes.start && left.presentation.groupRange.graphemes.end === right.presentation.groupRange.graphemes.end;
1497
+ }
1498
+ function legalBoundary(atoms, annotations, leftIndex, rightIndex, lineLengthEm, profile) {
1499
+ const left = atoms[leftIndex];
1500
+ const right = atoms[rightIndex];
1501
+ if (left === void 0 || right === void 0) return true;
1502
+ return !samePresentationGroup(left, right) && profile.breakPenalty(profile.classify({
1503
+ value: left.grapheme.value,
1504
+ presentation: left.presentation.kind
1505
+ }), profile.classify({
1506
+ value: right.grapheme.value,
1507
+ presentation: right.presentation.kind
1508
+ })) !== null && !breakInsideFittableGroupRuby(atoms, annotations, rightIndex, lineLengthEm);
1509
+ }
1510
+ function continuationFor(annotation, range) {
1511
+ const starts = range.graphemes.start === annotation.range.graphemes.start;
1512
+ const ends = range.graphemes.end === annotation.range.graphemes.end;
1513
+ if (starts && ends) return "whole";
1514
+ if (starts) return "start";
1515
+ if (ends) return "end";
1516
+ return "middle";
1517
+ }
1518
+ function groupReadingsForFragments(annotation, fragmentRanges, baseAdvances) {
1519
+ const baseLength = annotation.range.graphemes.end - annotation.range.graphemes.start;
1520
+ if (annotation.reading.kind !== "group") return [];
1521
+ const reading = [...graphemeSegmenter.segment(annotation.reading.text)].map(({ segment }) => segment);
1522
+ const advances = Array.from({ length: baseLength }, (_, index) => baseAdvances.get(annotation.range.graphemes.start + index));
1523
+ const measured = advances.every((advance) => advance !== void 0);
1524
+ const advanceFor = (fragment) => {
1525
+ const fragmentStart = fragment.graphemes.start - annotation.range.graphemes.start;
1526
+ const fragmentLength = fragment.graphemes.end - fragment.graphemes.start;
1527
+ return measured ? advances.slice(fragmentStart, fragmentStart + fragmentLength).reduce((total, advance) => total + advance, 0) : fragmentLength;
1528
+ };
1529
+ const weights = fragmentRanges.map(advanceFor);
1530
+ const effectiveWeights = weights.reduce((total, weight) => total + weight, 0) > 0 ? weights : fragmentRanges.map(({ graphemes }) => graphemes.end - graphemes.start);
1531
+ const totalWeight = effectiveWeights.reduce((total, weight) => total + weight, 0);
1532
+ const reserveEveryFragment = reading.length >= fragmentRanges.length;
1533
+ const fragments = [];
1534
+ let readingStart = 0;
1535
+ let accumulatedWeight = 0;
1536
+ for (const [index, weight] of effectiveWeights.entries()) {
1537
+ accumulatedWeight += weight;
1538
+ const remainingFragments = effectiveWeights.length - index - 1;
1539
+ const proportionalEnd = index === effectiveWeights.length - 1 ? reading.length : Math.round(accumulatedWeight * reading.length / totalWeight);
1540
+ const minimumEnd = reserveEveryFragment ? readingStart + 1 : readingStart;
1541
+ const maximumEnd = reserveEveryFragment ? reading.length - remainingFragments : reading.length;
1542
+ const readingEnd = Math.min(maximumEnd, Math.max(minimumEnd, proportionalEnd));
1543
+ fragments.push(reading.slice(readingStart, readingEnd).join(""));
1544
+ readingStart = readingEnd;
1545
+ }
1546
+ return fragments;
1547
+ }
1548
+ function readingForFragment(annotation, range, fragmentPlan) {
1549
+ const start = range.graphemes.start - annotation.range.graphemes.start;
1550
+ const length = range.graphemes.end - range.graphemes.start;
1551
+ if (annotation.reading.kind !== "group") return {
1552
+ kind: annotation.reading.kind,
1553
+ text: annotation.reading.segments.slice(start, start + length).join("")
1554
+ };
1555
+ const fragmentIndex = fragmentPlan.ranges.findIndex(({ graphemes }) => graphemes.start === range.graphemes.start && graphemes.end === range.graphemes.end);
1556
+ return {
1557
+ kind: "group",
1558
+ text: fragmentPlan.groupReadings[fragmentIndex] ?? ""
1559
+ };
1560
+ }
1561
+ function positionReading(text, baseOffsetEm, baseAdvanceEm, settings, measurer) {
1562
+ const measured = [...graphemeSegmenter.segment(text)].map(({ segment }) => ({
1563
+ value: segment,
1564
+ advanceEm: measure(measurer, segment, "ruby", settings) ?? 0
1565
+ }));
1566
+ const total = measured.reduce((sum, grapheme) => sum + grapheme.advanceEm, 0);
1567
+ const edgeGap = measured.length > 0 && total < baseAdvanceEm ? (baseAdvanceEm - total) / (2 * measured.length) : 0;
1568
+ let offsetEm = baseOffsetEm + (total > baseAdvanceEm ? (baseAdvanceEm - total) / 2 : edgeGap);
1569
+ return measured.map((grapheme, index) => {
1570
+ const positioned = {
1571
+ value: grapheme.value,
1572
+ advanceEm: grapheme.advanceEm,
1573
+ offsetEm
1574
+ };
1575
+ offsetEm += grapheme.advanceEm;
1576
+ if (index < measured.length - 1) offsetEm += edgeGap * 2;
1577
+ return positioned;
1578
+ });
1579
+ }
1580
+ function annotationFragmentRange(graphemes, suppressed, sourceGlues, annotation) {
1581
+ return ManuscriptRange.merge([
1582
+ ...graphemes.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range),
1583
+ ...suppressed.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range),
1584
+ ...sourceGlues.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range)
1585
+ ]);
1586
+ }
1587
+ function annotationFragments(graphemes, suppressed, sourceGlues, annotations, settings, measurer, fragmentPlansByAnnotation) {
1588
+ const fragments = [];
1589
+ for (const annotation of annotations) {
1590
+ const covered = graphemes.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range));
1591
+ const fragmentRange = annotationFragmentRange(graphemes, suppressed, sourceGlues, annotation);
1592
+ const first = covered[0];
1593
+ const last = covered.at(-1);
1594
+ if (fragmentRange === null || first === void 0 || last === void 0) continue;
1595
+ const common = {
1596
+ annotationRange: annotation.range,
1597
+ fragmentRange,
1598
+ continuation: continuationFor(annotation, fragmentRange)
1599
+ };
1600
+ switch (annotation.kind) {
1601
+ case "bold":
1602
+ case "italic":
1603
+ fragments.push({
1604
+ kind: annotation.kind,
1605
+ ...common
1606
+ });
1607
+ break;
1608
+ case "emphasis":
1609
+ fragments.push({
1610
+ kind: "emphasis",
1611
+ mark: annotation.mark,
1612
+ ...common
1613
+ });
1614
+ break;
1615
+ case "ruby": {
1616
+ const fragmentPlan = fragmentPlansByAnnotation.get(annotation);
1617
+ if (fragmentPlan === void 0) break;
1618
+ const baseOffsetEm = first.layoutSpan.offsetEm;
1619
+ const baseAdvanceEm = last.layoutSpan.offsetEm + last.layoutSpan.advanceEm - baseOffsetEm;
1620
+ const reading = readingForFragment(annotation, fragmentRange, fragmentPlan);
1621
+ fragments.push({
1622
+ kind: "ruby",
1623
+ rubyKind: reading.kind,
1624
+ reading: reading.text,
1625
+ baseOffsetEm,
1626
+ baseAdvanceEm,
1627
+ readingGraphemes: positionReading(reading.text, baseOffsetEm, baseAdvanceEm, settings, measurer),
1628
+ ...common
1629
+ });
1630
+ break;
1631
+ }
1632
+ }
1633
+ }
1634
+ return fragments;
1635
+ }
1636
+ function positionedLine(atoms, sourceGapIndexes, plan) {
1637
+ const items = plan.suppressedIndexes.flatMap((index) => {
1638
+ const atom = atoms[index];
1639
+ return atom === void 0 ? [] : [{
1640
+ kind: "suppressed",
1641
+ value: atom.grapheme.value,
1642
+ range: atom.grapheme.range,
1643
+ reason: "question-or-exclamation-gap"
1644
+ }];
1645
+ });
1646
+ const spacings = new Map(plan.pairSpacings.map((spacing) => [spacing.boundary, spacing]));
1647
+ let offsetEm = 0;
1648
+ const positionSpacing = (spacing) => {
1649
+ if (spacing.kind === "kern") items.push({
1650
+ kind: "kern",
1651
+ offsetEm,
1652
+ widthEm: spacing.widthEm
1653
+ });
1654
+ else items.push({
1655
+ kind: "glue",
1656
+ origin: "generated",
1657
+ offsetEm,
1658
+ widthEm: spacing.widthEm,
1659
+ naturalWidthEm: spacing.naturalWidthEm,
1660
+ adjustment: spacing.widthEm < spacing.naturalWidthEm ? "shrunk" : spacing.widthEm > spacing.naturalWidthEm ? "stretched" : "natural"
1661
+ });
1662
+ offsetEm += spacing.widthEm;
1663
+ };
1664
+ for (let index = plan.contentStart; index < plan.end; index += 1) {
1665
+ const spacing = spacings.get(index);
1666
+ if (spacing !== void 0) positionSpacing(spacing);
1667
+ const atom = atoms[index];
1668
+ if (atom === void 0) continue;
1669
+ if (sourceGapIndexes.has(index)) {
1670
+ items.push({
1671
+ kind: "glue",
1672
+ origin: "source",
1673
+ value: atom.grapheme.value,
1674
+ range: atom.grapheme.range,
1675
+ offsetEm,
1676
+ widthEm: atom.boxAdvanceEm,
1677
+ naturalWidthEm: atom.boxAdvanceEm,
1678
+ adjustment: "natural"
1679
+ });
1680
+ offsetEm += atom.boxAdvanceEm;
1681
+ continue;
1682
+ }
1683
+ const hanging = plan.hangingIndex === index;
1684
+ const layoutAdvanceEm = hanging ? 0 : atom.boxAdvanceEm;
1685
+ items.push({
1686
+ kind: "glyph",
1687
+ value: atom.grapheme.value,
1688
+ range: atom.grapheme.range,
1689
+ layoutSpan: {
1690
+ offsetEm,
1691
+ advanceEm: layoutAdvanceEm
1692
+ },
1693
+ renderSpan: {
1694
+ offsetEm: offsetEm + atom.renderOffsetEm,
1695
+ advanceEm: atom.renderAdvanceEm
1696
+ },
1697
+ disposition: hanging ? "hanging" : "placed",
1698
+ presentation: atom.presentation
1699
+ });
1700
+ offsetEm += layoutAdvanceEm;
1701
+ }
1702
+ const trailingSpacing = spacings.get(plan.end);
1703
+ if (trailingSpacing !== void 0) positionSpacing(trailingSpacing);
1704
+ const ranges = items.flatMap((item) => item.kind === "glyph" || item.kind === "suppressed" || item.kind === "glue" && item.origin === "source" ? [item.range] : []);
1705
+ return {
1706
+ range: ManuscriptRange.merge(ranges),
1707
+ inlineSizeEm: Math.max(0, plan.inlineSizeEm),
1708
+ items,
1709
+ break: plan.break,
1710
+ annotations: []
1711
+ };
1712
+ }
1713
+ function wrapSourceLine(sourceLine, annotations, settings, measurer, baseAdvances) {
1714
+ if (sourceLine.atoms.length === 0) return [NovelLine.empty()];
1715
+ const profile = TYPESETTING_PROFILE;
1716
+ const lines = layoutParagraph(sourceLine.atoms.map(({ grapheme, boxAdvanceEm, presentation }, index) => {
1717
+ const current = sourceLine.atoms[index];
1718
+ const next = sourceLine.atoms[index + 1];
1719
+ return {
1720
+ value: grapheme.value,
1721
+ boxAdvanceEm,
1722
+ sourceGap: sourceLine.suppressedIndexes.has(index),
1723
+ characterClass: profile.classify({
1724
+ value: grapheme.value,
1725
+ presentation: presentation.kind
1726
+ }),
1727
+ pairSpacingAfter: current === void 0 || next === void 0 || !samePresentationGroup(current, next) && !breakInsideFittableGroupRuby(sourceLine.atoms, annotations, index + 1, settings.flow.lineLengthEm)
1728
+ };
1729
+ }), settings.flow.lineLengthEm, profile, (leftIndex, rightIndex) => legalBoundary(sourceLine.atoms, annotations, leftIndex, rightIndex, settings.flow.lineLengthEm, profile)).map((plan) => positionedLine(sourceLine.atoms, sourceLine.suppressedIndexes, plan));
1730
+ const fragmentPlansByAnnotation = new Map(annotations.map((annotation) => {
1731
+ const ranges = lines.flatMap((line) => {
1732
+ 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);
1733
+ return range === null ? [] : [range];
1734
+ });
1735
+ return [annotation, {
1736
+ ranges,
1737
+ groupReadings: annotation.kind === "ruby" ? groupReadingsForFragments(annotation, ranges, baseAdvances) : []
1738
+ }];
1739
+ }));
1740
+ return lines.map((line) => ({
1741
+ range: line.range,
1742
+ inlineSizeEm: line.inlineSizeEm,
1743
+ items: line.items,
1744
+ break: line.break,
1745
+ 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)
1746
+ }));
1747
+ }
1748
+ function blankLines(count) {
1749
+ return Array.from({ length: count }, NovelLine.empty);
1750
+ }
1751
+ function buildPages(contentLines, settings) {
1752
+ const { linesPerStage, stagesPerPage } = settings.flow;
1753
+ const { stage: stageOffset, page: pageOffset } = settings.offsets;
1754
+ const usablePerStage = linesPerStage - LineOffset.total(stageOffset);
1755
+ const usablePerPage = usablePerStage * stagesPerPage;
1756
+ const pages = [];
1757
+ let contentIndex = 0;
1758
+ while (contentIndex < contentLines.length || pages.length === 0) {
1759
+ const stages = [];
1760
+ for (let stageIndex = 0; stageIndex < stagesPerPage; stageIndex += 1) {
1761
+ const lines = [];
1762
+ for (let position = 0; position < linesPerStage; position += 1) {
1763
+ const usableIndex = stageIndex * usablePerStage + position - stageOffset.leading;
1764
+ const withinStage = position >= stageOffset.leading && position < linesPerStage - stageOffset.trailing;
1765
+ const withinPage = usableIndex >= pageOffset.leading && usableIndex < usablePerPage - pageOffset.trailing;
1766
+ const next = contentLines[contentIndex];
1767
+ if (!withinStage || !withinPage || next === void 0) {
1768
+ lines.push(NovelLine.empty());
1769
+ continue;
1770
+ }
1771
+ lines.push(next);
1772
+ contentIndex += 1;
1773
+ }
1774
+ stages.push(NovelStage.of(lines));
1775
+ }
1776
+ pages.push(NovelPage.of(stages));
1777
+ }
1778
+ return pages;
1779
+ }
1780
+ function createCompose(measurer) {
1781
+ return (manuscript, settings) => {
1782
+ const sourceLines = displayedLines(manuscript.graphemes);
1783
+ const measured = sourceLines.map((line) => measureSourceLine(line, manuscript.annotations, settings, measurer));
1784
+ if (measured.some((line) => line === void 0)) return ManuscriptResult.fail({ reason: "inline measurer returned a negative or non-finite value" });
1785
+ const baseAdvances = /* @__PURE__ */ new Map();
1786
+ for (const line of measured) for (const atom of line?.atoms ?? []) baseAdvances.set(atom.grapheme.range.graphemes.start, atom.boxAdvanceEm);
1787
+ const manuscriptLines = measured.flatMap((line) => line === void 0 ? [] : wrapSourceLine(line, manuscript.annotations, settings, measurer, baseAdvances));
1788
+ const pages = buildPages([
1789
+ ...blankLines(settings.offsets.document.leading),
1790
+ ...manuscriptLines,
1791
+ ...blankLines(settings.offsets.document.trailing)
1792
+ ], settings);
1793
+ return ManuscriptResult.succeed({
1794
+ pages,
1795
+ geometry: ManuscriptGeometry.of(settings.flow, settings.appearance),
1796
+ stats: {
1797
+ chars: sourceLines.reduce((total, line) => total + line.length, 0),
1798
+ sourceLines: sourceLines.length,
1799
+ pages: pages.length
1800
+ }
1801
+ });
1802
+ };
1803
+ }
1804
+ function createNovelComposer(options) {
1805
+ return {
1806
+ id: COMPOSER_ID,
1807
+ settingsSchema: NovelCompositionSettings.schema,
1808
+ layoutSchema: NovelLayout.schema,
1809
+ compose: createCompose(options.measurer)
1810
+ };
1811
+ }
1812
+ const novelComposer = createNovelComposer({ measurer: logicalInlineMeasurer });
1813
+ //#endregion
1814
+ export { logicalInlineMeasurer as a, ManuscriptOffsets as c, FontSizePt as d, FontPresetId as f, PaperSize as i, ManuscriptAppearanceSettings as l, novelComposer as n, NovelCompositionSettings as o, ParsedManuscript as p, ManuscriptGeometry as r, NovelFlowSettings as s, createNovelComposer as t, PaperSizeId as u };