@sushichan044/kg-core 0.9.5 → 0.10.1

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,3393 @@
1
+ import { _ as readonlyArray, a as CLOSING_BRACKETS, c as graphemeSegmenter, g as nonNegativeInteger, h as TextRange, i as TATE_CHU_YOKO_MARKS, m as ManuscriptRange, o as OPENING_BRACKETS, p as NamespacedId, r as DIVIDING_PUNCTUATION, s as characterClass, t as questionOrExclamationSpacings, u as ManuscriptResult, v as readonlyObject } from "./question-or-exclamation-spacing-Cp3eOGlA.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
+ //#endregion
236
+ //#region src/composer/internal/annotation-style.ts
237
+ const defaultAnnotationStyle = {
238
+ sizeEm: .5,
239
+ areaStartEm: -.5,
240
+ areaEndEm: 0
241
+ };
242
+ //#endregion
243
+ //#region src/composer/internal/book-style.ts
244
+ const shrink = {
245
+ "line-end": {
246
+ stage: 0,
247
+ costPerEm: 0
248
+ },
249
+ "word-space": {
250
+ stage: 1,
251
+ costPerEm: 1
252
+ },
253
+ "middle-dot": {
254
+ stage: 2,
255
+ costPerEm: 2
256
+ },
257
+ punctuation: {
258
+ stage: 3,
259
+ costPerEm: 3
260
+ },
261
+ "mixed-text": {
262
+ stage: 4,
263
+ costPerEm: 4
264
+ },
265
+ solid: {
266
+ stage: 4,
267
+ costPerEm: 4
268
+ }
269
+ };
270
+ const stretch = {
271
+ "word-space": {
272
+ stage: 1,
273
+ costPerEm: 1
274
+ },
275
+ "mixed-text": {
276
+ stage: 2,
277
+ costPerEm: 2
278
+ },
279
+ solid: {
280
+ stage: 3,
281
+ costPerEm: 3
282
+ },
283
+ "line-end": {
284
+ stage: 3,
285
+ costPerEm: 3
286
+ },
287
+ "middle-dot": {
288
+ stage: 3,
289
+ costPerEm: 3
290
+ },
291
+ punctuation: {
292
+ stage: 3,
293
+ costPerEm: 3
294
+ }
295
+ };
296
+ const defaultBookStyle = {
297
+ adjustment: (direction, category) => (direction === "shrink" ? shrink : stretch)[category],
298
+ lineStartSpacing: (first, head) => first === "cl-01" && head === "paragraph-start" ? {
299
+ kind: "glue",
300
+ naturalWidthEm: .5
301
+ } : null,
302
+ finalStretchCostPerEm: 4,
303
+ annotation: defaultAnnotationStyle
304
+ };
305
+ //#endregion
306
+ //#region src/composer/internal/annotation-clearance.ts
307
+ const EPSILON$7 = 1e-9;
308
+ function overlaps(left, right) {
309
+ return left.inlineSpan.offsetEm < right.inlineSpan.offsetEm + right.inlineSpan.advanceEm - EPSILON$7 && right.inlineSpan.offsetEm < left.inlineSpan.offsetEm + left.inlineSpan.advanceEm - EPSILON$7 && left.blockOffsetEm < right.blockOffsetEm + right.blockSizeEm - EPSILON$7 && right.blockOffsetEm < left.blockOffsetEm + left.blockSizeEm - EPSILON$7;
310
+ }
311
+ const AnnotationClearance = { resolve: (areas) => {
312
+ for (const area of areas) if (area.some((placement) => placement.side !== "before" || placement.blockOffsetEm < defaultBookStyle.annotation.areaStartEm - EPSILON$7 || placement.blockOffsetEm + placement.blockSizeEm > defaultBookStyle.annotation.areaEndEm + EPSILON$7)) return {
313
+ kind: "refused",
314
+ reason: "annotation exceeds the fixed half-em right-side area"
315
+ };
316
+ for (const [index, area] of areas.entries()) if (areas.slice(index + 1).some((other) => area.some((left) => other.some((right) => overlaps(left, right))))) return {
317
+ kind: "refused",
318
+ reason: "annotation readings or emphasis marks overlap in the fixed right-side area"
319
+ };
320
+ return { kind: "clear" };
321
+ } };
322
+ //#endregion
323
+ //#region src/composer/internal/binding-sequence.ts
324
+ const BindingSequence = { contains: (left, right) => left === right || (left === "〳" || left === "〴") && right === "〵" };
325
+ //#endregion
326
+ //#region src/composer/internal/boundary-rule.ts
327
+ function breakConstraint(left, right, profile, context) {
328
+ if (context.runInterior) return {
329
+ kind: "prohibited",
330
+ reason: "run-interior"
331
+ };
332
+ if (context.bindingSequence) return {
333
+ kind: "prohibited",
334
+ reason: "binding-sequence"
335
+ };
336
+ const penalty = profile.breakPenalty(left, right);
337
+ if (penalty === null) return {
338
+ kind: "prohibited",
339
+ reason: left === "cl-01" || left === "cl-12" ? "line-end" : "line-head"
340
+ };
341
+ if (context.rubyInterior) return {
342
+ kind: "prohibited",
343
+ reason: "ruby-interior"
344
+ };
345
+ return {
346
+ kind: "allowed",
347
+ penalty
348
+ };
349
+ }
350
+ const BoundaryRule = { resolve: (left, right, profile, context) => {
351
+ const spacingAllowed = !context.runInterior && !context.rubySpacingInterior && !context.sourceGap;
352
+ return {
353
+ break: breakConstraint(left, right, profile, context),
354
+ spacing: !spacingAllowed ? null : context.bindingSequence ? {
355
+ kind: "kern",
356
+ naturalWidthEm: 0
357
+ } : profile.pairSpacing(left, right),
358
+ finalStretch: spacingAllowed && !context.bindingSequence && profile.canExpandAtFinalStage(left, right)
359
+ };
360
+ } };
361
+ //#endregion
362
+ //#region src/composer/internal/composition-run.ts
363
+ const ASCII_ALPHANUMERIC = /^[A-Za-z0-9]$/u;
364
+ const ASCII_TWO_DIGITS = /^[0-9]{2}$/u;
365
+ const UPRIGHT_LATIN_ABBREVIATION = /^(?:[A-Z]+|[A-Z][a-z]{1,2})$/u;
366
+ const TATE_CHU_YOKO_MARK = new RegExp(`^[${characterClass(TATE_CHU_YOKO_MARKS)}]$`, "u");
367
+ const FULLWIDTH_ALPHANUMERIC = /^[A-Za-z0-9]$/u;
368
+ function stretchEnd(graphemes, start, pattern) {
369
+ let end = start + 1;
370
+ while (pattern.test(graphemes[end]?.value ?? "")) end += 1;
371
+ return end;
372
+ }
373
+ function presentationOf(text) {
374
+ if (ASCII_ALPHANUMERIC.test(text[0] ?? "")) {
375
+ if (ASCII_TWO_DIGITS.test(text)) return "tate-chu-yoko";
376
+ return text.length === 1 || UPRIGHT_LATIN_ABBREVIATION.test(text) ? "upright" : "sideways";
377
+ }
378
+ return FULLWIDTH_ALPHANUMERIC.test(text) ? "upright" : "mixed";
379
+ }
380
+ function recognize(graphemes) {
381
+ const runs = [];
382
+ const push = (members, presentation) => {
383
+ const range = ManuscriptRange.merge(members.map((member) => member.range));
384
+ if (range !== null) runs.push({
385
+ range,
386
+ presentation,
387
+ members
388
+ });
389
+ };
390
+ let cursor = 0;
391
+ while (cursor < graphemes.length) {
392
+ const first = graphemes[cursor];
393
+ if (first === void 0) break;
394
+ if (TATE_CHU_YOKO_MARK.test(first.value)) {
395
+ const end = stretchEnd(graphemes, cursor, TATE_CHU_YOKO_MARK);
396
+ const members = graphemes.slice(cursor, end);
397
+ if (members.length === 2 || members.length === 3) push(members, "tate-chu-yoko");
398
+ else for (const member of members) push([member], "mixed");
399
+ cursor = end;
400
+ continue;
401
+ }
402
+ const end = ASCII_ALPHANUMERIC.test(first.value) ? stretchEnd(graphemes, cursor, ASCII_ALPHANUMERIC) : cursor + 1;
403
+ const members = graphemes.slice(cursor, end);
404
+ push(members, presentationOf(members.map(({ value }) => value).join("")));
405
+ cursor = end;
406
+ }
407
+ return runs;
408
+ }
409
+ const CompositionRun = { recognize };
410
+ //#endregion
411
+ //#region src/composer/internal/emphasis-placement.ts
412
+ const { sizeEm } = defaultBookStyle.annotation;
413
+ const EmphasisPlacement = {
414
+ accepts: (characterClass) => characterClass !== void 0 && ![
415
+ "cl-01",
416
+ "cl-02",
417
+ "cl-06",
418
+ "cl-07"
419
+ ].some((value) => value === characterClass),
420
+ of: (base) => ({
421
+ side: "before",
422
+ inlineSpan: {
423
+ offsetEm: base.offsetEm + (base.advanceEm - sizeEm) / 2,
424
+ advanceEm: sizeEm
425
+ },
426
+ blockOffsetEm: -sizeEm,
427
+ blockSizeEm: sizeEm
428
+ })
429
+ };
430
+ //#endregion
431
+ //#region src/composer/measurement-text-range.ts
432
+ const MeasurementTextRangeSchema = v.pipe(TextRange.schema, v.brand("MeasurementTextRange"));
433
+ const MeasurementTextRange = {
434
+ schema: MeasurementTextRangeSchema,
435
+ of: (range) => v.parse(MeasurementTextRangeSchema, range)
436
+ };
437
+ //#endregion
438
+ //#region src/composer/internal/group-ruby-allocation.ts
439
+ function nearestBoundary(prefix, target) {
440
+ let low = 0;
441
+ let high = prefix.length - 1;
442
+ while (low < high) {
443
+ const middle = Math.floor((low + high) / 2);
444
+ if ((prefix[middle] ?? 0) < target) low = middle + 1;
445
+ else high = middle;
446
+ }
447
+ return low > 0 && target - (prefix[low - 1] ?? 0) < (prefix[low] ?? 0) - target ? low - 1 : low;
448
+ }
449
+ const GroupRubyAllocation = {
450
+ prepare: (association, pieces, advancesEm) => {
451
+ const start = association.indexes[0];
452
+ const last = association.indexes.at(-1);
453
+ if (start === void 0 || last === void 0) return void 0;
454
+ const basePrefixEm = [0];
455
+ for (const index of association.indexes) basePrefixEm.push((basePrefixEm.at(-1) ?? 0) + (advancesEm[index] ?? 0));
456
+ const readingPrefixGraphemes = [0];
457
+ for (const piece of pieces) readingPrefixGraphemes.push((readingPrefixGraphemes.at(-1) ?? 0) + [...graphemeSegmenter.segment(piece.value)].length);
458
+ return {
459
+ association,
460
+ start,
461
+ end: last + 1,
462
+ pieces,
463
+ basePrefixEm,
464
+ readingPrefixGraphemes
465
+ };
466
+ },
467
+ assign: (group, start, end, cursor, lineLengthEm) => {
468
+ const fragmentStart = Math.max(start, group.start);
469
+ const fragmentEnd = Math.min(end, group.end);
470
+ const readingStart = start <= group.start ? 0 : cursor;
471
+ const totalBase = group.basePrefixEm.at(-1) ?? 0;
472
+ const consumedBase = group.basePrefixEm[fragmentEnd - group.start] ?? totalBase;
473
+ const baseFraction = totalBase > 0 ? consumedBase / totalBase : (fragmentEnd - group.start) / (group.end - group.start);
474
+ const target = Math.round(baseFraction * (group.readingPrefixGraphemes.at(-1) ?? 0));
475
+ let readingEnd = nearestBoundary(group.readingPrefixGraphemes, target);
476
+ if (fragmentEnd === group.end) readingEnd = group.pieces.length;
477
+ else {
478
+ const remainingFragments = Math.max(1, Math.ceil((totalBase - consumedBase) / lineLengthEm));
479
+ const reserve = group.pieces.length - readingStart >= remainingFragments + 1;
480
+ const minimum = reserve ? readingStart + 1 : readingStart;
481
+ const maximum = reserve ? group.pieces.length - remainingFragments : group.pieces.length;
482
+ readingEnd = Math.max(minimum, Math.min(maximum, readingEnd));
483
+ }
484
+ const selected = group.pieces.slice(readingStart, readingEnd);
485
+ const textStart = selected[0]?.textRange.start ?? 0;
486
+ const layoutStart = selected[0]?.layoutSpan.offsetEm ?? 0;
487
+ const pieces = selected.map((piece) => ({
488
+ value: piece.value,
489
+ textRange: MeasurementTextRange.of({
490
+ start: piece.textRange.start - textStart,
491
+ end: piece.textRange.end - textStart
492
+ }),
493
+ layoutSpan: {
494
+ advanceEm: piece.layoutSpan.advanceEm,
495
+ offsetEm: piece.layoutSpan.offsetEm - layoutStart
496
+ },
497
+ renderSpan: {
498
+ advanceEm: piece.renderSpan.advanceEm,
499
+ offsetEm: piece.renderSpan.offsetEm - layoutStart
500
+ }
501
+ }));
502
+ return {
503
+ segment: {
504
+ association: group.association,
505
+ start: fragmentStart,
506
+ end: fragmentEnd,
507
+ pieces
508
+ },
509
+ nextCursor: fragmentEnd === group.end ? 0 : readingEnd
510
+ };
511
+ }
512
+ };
513
+ //#endregion
514
+ //#region src/composer/internal/adjustment-unit.ts
515
+ const AdjustmentUnit = {
516
+ capacity: (unit) => unit.parts.reduce((total, part) => total + part.capacityEm, 0),
517
+ of: (direction, capacity, parts) => ({
518
+ direction,
519
+ kind: capacity.granularity,
520
+ stage: capacity.stage,
521
+ costPerEm: capacity.costPerEm,
522
+ parts
523
+ })
524
+ };
525
+ //#endregion
526
+ //#region src/composer/internal/spacing-slot.ts
527
+ const SpacingSlot = { key: (slot) => slot.kind === "gap" ? slot.boundary * 2 : slot.index * 2 + 1 };
528
+ //#endregion
529
+ //#region src/composer/internal/spacing-allocation.ts
530
+ const EPSILON$6 = 1e-9;
531
+ function resolveSpacings(values, units, adjustment, amountEm, finalExpansion) {
532
+ const usedBySlot = /* @__PURE__ */ new Map();
533
+ let remaining = amountEm;
534
+ let deformationCost = 0;
535
+ let freeEm = 0;
536
+ let finalStretchPerSlotEm = 0;
537
+ const spend = (unit, fraction) => {
538
+ for (const { slot, capacityEm: partEm } of unit.parts) {
539
+ const key = SpacingSlot.key(slot);
540
+ usedBySlot.set(key, (usedBySlot.get(key) ?? 0) + partEm * fraction);
541
+ }
542
+ const spentEm = AdjustmentUnit.capacity(unit) * fraction;
543
+ deformationCost += spentEm * unit.costPerEm;
544
+ if (unit.costPerEm === 0) freeEm += spentEm;
545
+ remaining -= spentEm;
546
+ };
547
+ const stages = /* @__PURE__ */ new Map();
548
+ for (const unit of units) {
549
+ if (unit.direction !== adjustment) continue;
550
+ const stage = stages.get(unit.stage);
551
+ if (stage === void 0) stages.set(unit.stage, [unit]);
552
+ else stage.push(unit);
553
+ }
554
+ for (const stageIndex of [...stages.keys()].sort((left, right) => left - right)) {
555
+ if (remaining <= EPSILON$6) break;
556
+ const stage = stages.get(stageIndex) ?? [];
557
+ let stageCapacityEm = 0;
558
+ for (const unit of stage) if (unit.kind === "continuous") stageCapacityEm += AdjustmentUnit.capacity(unit);
559
+ else if (AdjustmentUnit.capacity(unit) <= remaining + EPSILON$6) spend(unit, 1);
560
+ if (stageCapacityEm <= EPSILON$6 || remaining <= EPSILON$6) continue;
561
+ const fraction = Math.min(remaining, stageCapacityEm) / stageCapacityEm;
562
+ for (const unit of stage) if (unit.kind === "continuous") spend(unit, fraction);
563
+ }
564
+ if (adjustment === "stretch" && remaining > EPSILON$6) {
565
+ const finalSlotCount = finalExpansion.slots.length;
566
+ if (finalSlotCount > 0) {
567
+ finalStretchPerSlotEm = remaining / finalSlotCount;
568
+ for (const slot of finalExpansion.slots) {
569
+ const key = SpacingSlot.key(slot);
570
+ usedBySlot.set(key, (usedBySlot.get(key) ?? 0) + finalStretchPerSlotEm);
571
+ }
572
+ deformationCost += remaining * finalExpansion.costPerEm;
573
+ remaining = 0;
574
+ }
575
+ }
576
+ return {
577
+ spacings: splitBySlot(values, (slot, spacing) => {
578
+ const used = usedBySlot.get(SpacingSlot.key(slot)) ?? 0;
579
+ return adjustment === "shrink" ? spacing.naturalWidthEm - used : spacing.naturalWidthEm + used;
580
+ }),
581
+ deformationCost,
582
+ freeEm,
583
+ finalStretchPerSlotEm,
584
+ unabsorbedEm: Math.max(0, remaining)
585
+ };
586
+ }
587
+ function splitBySlot(values, widthOf) {
588
+ const pairSpacings = [];
589
+ const characterSpacings = [];
590
+ for (const { slot, spacing } of values) {
591
+ const resolved = {
592
+ kind: spacing.kind,
593
+ naturalWidthEm: spacing.naturalWidthEm,
594
+ widthEm: widthOf(slot, spacing)
595
+ };
596
+ if (slot.kind === "gap") pairSpacings.push({
597
+ boundary: slot.boundary,
598
+ ...resolved
599
+ });
600
+ else characterSpacings.push({
601
+ index: slot.index,
602
+ ...resolved
603
+ });
604
+ }
605
+ return {
606
+ pairSpacings,
607
+ characterSpacings
608
+ };
609
+ }
610
+ /**
611
+ * The same spaces at their resolved natural width, for a line no adjustment reaches.
612
+ */
613
+ function naturalSpacings(values) {
614
+ return splitBySlot(values, (_slot, spacing) => spacing.naturalWidthEm);
615
+ }
616
+ /**
617
+ * The same, minus the アキ the line end would have taken: a hanging character sits outside the text
618
+ * area and the space that would have followed it goes with it.
619
+ */
620
+ function hangingSpacings(values, end) {
621
+ const { pairSpacings, characterSpacings } = naturalSpacings(values);
622
+ return {
623
+ pairSpacings: pairSpacings.filter(({ boundary }) => boundary !== end),
624
+ characterSpacings
625
+ };
626
+ }
627
+ //#endregion
628
+ //#region src/composer/internal/candidate-line.ts
629
+ const EPSILON$5 = 1e-9;
630
+ const CandidateLine = { resolve: (context, lineLengthEm) => {
631
+ const { start, contentStart, end, suppressedIndexes, boxesSizeEm, terminal, lastVisible, lastAdvance, canHang, pairValues, unitsFor, finalExpansion } = context;
632
+ const naturalSizeEm = boxesSizeEm + pairValues.reduce((total, value) => total + value.spacing.naturalWidthEm, 0);
633
+ const overflow = naturalSizeEm - lineLengthEm;
634
+ const underflow = lineLengthEm - naturalSizeEm;
635
+ const units = unitsFor(overflow > 0 ? "shrink" : "stretch");
636
+ const capacity = units.reduce((total, unit) => total + AdjustmentUnit.capacity(unit), 0);
637
+ const freeShrinkCapacity = units.reduce((total, unit) => total + (unit.costPerEm === 0 ? AdjustmentUnit.capacity(unit) : 0), 0);
638
+ const trailingSpacing = pairValues.find(({ slot }) => slot.kind === "gap" && slot.boundary === end)?.spacing.naturalWidthEm ?? 0;
639
+ const hasFinalStretch = finalExpansion.slots.length > 0;
640
+ if (terminal && overflow <= EPSILON$5) return {
641
+ start,
642
+ contentStart,
643
+ end,
644
+ suppressedIndexes,
645
+ ...naturalSpacings(pairValues),
646
+ inlineSizeEm: naturalSizeEm,
647
+ break: { kind: "paragraph-end" },
648
+ hangingIndex: null,
649
+ deformationRatio: 0,
650
+ deformationCost: 0
651
+ };
652
+ if (Math.abs(overflow) <= EPSILON$5) return {
653
+ start,
654
+ contentStart,
655
+ end,
656
+ suppressedIndexes,
657
+ ...naturalSpacings(pairValues),
658
+ inlineSizeEm: naturalSizeEm,
659
+ break: { kind: "natural" },
660
+ hangingIndex: null,
661
+ deformationRatio: 0,
662
+ deformationCost: 0
663
+ };
664
+ const shrunk = overflow > 0 && overflow <= capacity + EPSILON$5 ? resolveSpacings(pairValues, units, "shrink", overflow, finalExpansion) : null;
665
+ if (shrunk !== null && shrunk.unabsorbedEm <= EPSILON$5) {
666
+ const chargeableCapacity = capacity - freeShrinkCapacity;
667
+ const chargeableShrink = Math.max(0, overflow - shrunk.freeEm);
668
+ return {
669
+ start,
670
+ contentStart,
671
+ end,
672
+ suppressedIndexes,
673
+ ...shrunk.spacings,
674
+ inlineSizeEm: lineLengthEm,
675
+ break: { kind: "shrunk" },
676
+ hangingIndex: null,
677
+ deformationRatio: chargeableCapacity <= EPSILON$5 ? 0 : chargeableShrink / chargeableCapacity,
678
+ deformationCost: shrunk.deformationCost
679
+ };
680
+ }
681
+ if (!terminal && overflow > 0 && lastVisible !== void 0 && canHang && naturalSizeEm - lastAdvance - trailingSpacing <= lineLengthEm + EPSILON$5) return {
682
+ start,
683
+ contentStart,
684
+ end,
685
+ suppressedIndexes,
686
+ ...hangingSpacings(pairValues, end),
687
+ inlineSizeEm: naturalSizeEm - lastAdvance - trailingSpacing,
688
+ break: { kind: "hanging" },
689
+ hangingIndex: lastVisible,
690
+ deformationRatio: overflow / Math.max(lastAdvance, EPSILON$5),
691
+ deformationCost: 0
692
+ };
693
+ const stretched = !terminal && underflow > EPSILON$5 && (underflow <= capacity + EPSILON$5 || hasFinalStretch) ? resolveSpacings(pairValues, units, "stretch", underflow, finalExpansion) : null;
694
+ if (stretched !== null && stretched.unabsorbedEm <= EPSILON$5) return {
695
+ start,
696
+ contentStart,
697
+ end,
698
+ suppressedIndexes,
699
+ ...stretched.spacings,
700
+ inlineSizeEm: lineLengthEm,
701
+ break: { kind: "stretched" },
702
+ hangingIndex: null,
703
+ deformationRatio: stretched.finalStretchPerSlotEm > 0 ? 1 + stretched.finalStretchPerSlotEm : capacity === 0 ? 0 : underflow / capacity,
704
+ deformationCost: stretched.deformationCost
705
+ };
706
+ return {
707
+ start,
708
+ contentStart,
709
+ end,
710
+ suppressedIndexes,
711
+ ...naturalSpacings(pairValues),
712
+ inlineSizeEm: naturalSizeEm,
713
+ break: { kind: "forced" },
714
+ hangingIndex: null,
715
+ deformationRatio: Math.abs(naturalSizeEm - lineLengthEm) / Math.max(lineLengthEm, EPSILON$5),
716
+ deformationCost: 0
717
+ };
718
+ } };
719
+ //#endregion
720
+ //#region src/composer/internal/japanese-adjustment-units.ts
721
+ const EPSILON$4 = 1e-9;
722
+ function adjustmentUnits(values, adjustment) {
723
+ const select = (spacing) => adjustment === "shrink" ? spacing.shrink : spacing.stretch;
724
+ const units = [];
725
+ const straddle = values.find(({ absorbsPrecedingEm }) => absorbsPrecedingEm > 0);
726
+ let absorbedBoundary;
727
+ let absorbedEm = 0;
728
+ let absorbingBoundary;
729
+ const straddleSlot = straddle?.slot.kind === "gap" ? straddle.slot : void 0;
730
+ const straddleOwn = straddle === void 0 ? void 0 : select(straddle.spacing);
731
+ if (straddle !== void 0 && straddleSlot !== void 0 && straddleOwn !== void 0) {
732
+ const precedingBoundary = straddleSlot.boundary - 1;
733
+ const preceding = values.find(({ slot }) => slot.kind === "gap" && slot.boundary === precedingBoundary);
734
+ const availableEm = preceding === void 0 ? 0 : select(preceding.spacing)?.amountEm ?? 0;
735
+ absorbingBoundary = straddleSlot.boundary;
736
+ if (availableEm + EPSILON$4 >= straddle.absorbsPrecedingEm) {
737
+ absorbedBoundary = precedingBoundary;
738
+ absorbedEm = straddle.absorbsPrecedingEm;
739
+ units.push(AdjustmentUnit.of(adjustment, straddleOwn, [{
740
+ slot: {
741
+ kind: "gap",
742
+ boundary: precedingBoundary
743
+ },
744
+ capacityEm: absorbedEm
745
+ }, {
746
+ slot: straddleSlot,
747
+ capacityEm: straddleOwn.amountEm
748
+ }]));
749
+ }
750
+ }
751
+ for (const { slot, spacing } of values) {
752
+ if (slot.kind === "gap" && slot.boundary === absorbingBoundary) continue;
753
+ const own = select(spacing);
754
+ if (own === void 0) continue;
755
+ const claimedEm = slot.kind === "gap" && slot.boundary === absorbedBoundary ? absorbedEm : 0;
756
+ const amountEm = own.amountEm - claimedEm;
757
+ if (amountEm <= EPSILON$4) continue;
758
+ units.push(AdjustmentUnit.of(adjustment, own, [{
759
+ slot,
760
+ capacityEm: amountEm
761
+ }]));
762
+ }
763
+ return units;
764
+ }
765
+ //#endregion
766
+ //#region src/composer/internal/japanese-paragraph.ts
767
+ const JapaneseParagraph = {
768
+ prefixAdvances: (advancesEm) => {
769
+ const prefix = [0];
770
+ for (const advance of advancesEm) prefix.push((prefix.at(-1) ?? 0) + advance);
771
+ return prefix;
772
+ },
773
+ of: (characters, boundaries, profile, lineLengthEm) => {
774
+ const prefixEm = JapaneseParagraph.prefixAdvances(characters.map(({ boxAdvanceEm }) => boxAdvanceEm));
775
+ const nextVisible = Array.from({ length: characters.length + 1 }).fill(characters.length);
776
+ const previousVisible = Array.from({ length: characters.length + 1 });
777
+ for (let index = characters.length - 1; index >= 0; index -= 1) nextVisible[index] = characters[index]?.sourceGap === true ? nextVisible[index + 1] ?? characters.length : index;
778
+ for (let end = 1; end <= characters.length; end += 1) previousVisible[end] = characters[end - 1]?.sourceGap === true ? previousVisible[end - 1] : end - 1;
779
+ const edges = characters.map(({ characterClass, effectiveClass }) => ({
780
+ paragraphHead: profile.lineStartSpacing(effectiveClass, "paragraph-start"),
781
+ turnedHead: profile.lineStartSpacing(effectiveClass, "turned-over"),
782
+ end: profile.lineEndSpacing(effectiveClass),
783
+ sourceMid: profile.spacingCharacter(characterClass, "mid-line"),
784
+ sourceEdge: profile.spacingCharacter(characterClass, "line-edge"),
785
+ canHang: profile.canHang(effectiveClass)
786
+ }));
787
+ return {
788
+ elements: characters,
789
+ resolveCandidate: (start, end, metrics) => {
790
+ const contentStart = nextVisible[start] ?? characters.length;
791
+ const values = [];
792
+ const head = edges[contentStart];
793
+ const headSpacing = (start === 0 ? head?.paragraphHead : head?.turnedHead) ?? null;
794
+ if (headSpacing !== null) values.push({
795
+ slot: {
796
+ kind: "gap",
797
+ boundary: contentStart
798
+ },
799
+ spacing: headSpacing,
800
+ absorbsPrecedingEm: 0
801
+ });
802
+ for (let index = contentStart; index < end; index += 1) {
803
+ if (characters[index]?.sourceGap === true) continue;
804
+ const edge = edges[index];
805
+ const spacing = (index === contentStart || index === end - 1 ? edge?.sourceEdge : edge?.sourceMid) ?? null;
806
+ if (spacing !== null) values.push({
807
+ slot: {
808
+ kind: "character",
809
+ index
810
+ },
811
+ spacing,
812
+ absorbsPrecedingEm: 0
813
+ });
814
+ }
815
+ const finalSlots = [];
816
+ for (let boundary = contentStart + 1; boundary < end; boundary += 1) {
817
+ const rule = boundaries[boundary];
818
+ if (rule?.spacing === void 0 || rule.spacing === null) continue;
819
+ const slot = {
820
+ kind: "gap",
821
+ boundary
822
+ };
823
+ values.push({
824
+ slot,
825
+ spacing: rule.spacing,
826
+ absorbsPrecedingEm: 0
827
+ });
828
+ if (rule.finalStretch) finalSlots.push(slot);
829
+ }
830
+ const tail = edges[end - 1]?.end ?? null;
831
+ if (tail !== null) values.push({
832
+ slot: {
833
+ kind: "gap",
834
+ boundary: end
835
+ },
836
+ spacing: tail.spacing,
837
+ absorbsPrecedingEm: tail.absorbsPrecedingEm
838
+ });
839
+ const lastVisible = previousVisible[end];
840
+ for (const extra of metrics?.extraSpacings ?? []) {
841
+ const index = values.findIndex(({ slot }) => slot.kind === "gap" && slot.boundary === extra.boundary);
842
+ const existing = values[index];
843
+ if (existing === void 0) values.push({
844
+ slot: {
845
+ kind: "gap",
846
+ boundary: extra.boundary
847
+ },
848
+ spacing: {
849
+ kind: "glue",
850
+ naturalWidthEm: extra.widthEm
851
+ },
852
+ absorbsPrecedingEm: 0
853
+ });
854
+ else values[index] = {
855
+ ...existing,
856
+ spacing: {
857
+ ...existing.spacing,
858
+ naturalWidthEm: existing.spacing.naturalWidthEm + extra.widthEm
859
+ }
860
+ };
861
+ }
862
+ const extraBoxesEm = metrics?.boxAdjustments.reduce((sum, box) => sum + box.advanceEm - (characters[box.index]?.boxAdvanceEm ?? 0), 0) ?? 0;
863
+ const line = CandidateLine.resolve({
864
+ start,
865
+ contentStart,
866
+ end,
867
+ suppressedIndexes: Array.from({ length: contentStart - start }, (_, index) => start + index),
868
+ boxesSizeEm: (prefixEm[end] ?? 0) - (prefixEm[contentStart] ?? 0) + extraBoxesEm,
869
+ terminal: nextVisible[end] === characters.length,
870
+ lastVisible,
871
+ lastAdvance: lastVisible === void 0 ? 0 : metrics?.boxAdjustments.find((box) => box.index === lastVisible)?.advanceEm ?? characters[lastVisible]?.boxAdvanceEm ?? 0,
872
+ canHang: lastVisible !== void 0 && edges[lastVisible]?.canHang === true,
873
+ pairValues: values,
874
+ unitsFor: (direction) => adjustmentUnits(values, direction),
875
+ finalExpansion: {
876
+ slots: finalSlots,
877
+ costPerEm: profile.finalStretchCostPerEm
878
+ }
879
+ }, lineLengthEm);
880
+ return metrics === void 0 ? line : {
881
+ ...line,
882
+ boxAdjustments: metrics.boxAdjustments
883
+ };
884
+ }
885
+ };
886
+ }
887
+ };
888
+ //#endregion
889
+ //#region src/composer/internal/japanese-typesetting-rules.ts
890
+ /**
891
+ * The JLReq character classes (文字クラス) these rules distinguish. The bare `cl-NN` identifiers are
892
+ * kept rather than descriptive names so the spacing and penalty tables below can be read side by
893
+ * side with the tables in the specification.
894
+ *
895
+ * JLReq defines thirty classes. Math (cl-17, cl-18) and warichu brackets (cl-28, cl-29) are absent
896
+ * because a novel is not set with them. The base-group classes are absent for a different reason: a
897
+ * novel does carry ruby (cl-22, cl-23), but ruby is an annotation the composer positions
898
+ * separately, and `JapaneseCharacter` cannot describe a base group at all — nor a reference mark
899
+ * (cl-20) or a subscripted base (cl-21). A base character is therefore classified here as whatever
900
+ * it is on its own, which for the kanji that usually carries ruby means cl-19.
901
+ *
902
+ * @see https://www.w3.org/TR/jlreq/#character_classes
903
+ */
904
+ const japaneseCharacterClasses = [
905
+ "cl-01",
906
+ "cl-02",
907
+ "cl-03",
908
+ "cl-04",
909
+ "cl-05",
910
+ "cl-06",
911
+ "cl-07",
912
+ "cl-08",
913
+ "cl-09",
914
+ "cl-10",
915
+ "cl-11",
916
+ "cl-12",
917
+ "cl-13",
918
+ "cl-14",
919
+ "cl-15",
920
+ "cl-16",
921
+ "cl-19",
922
+ "cl-24",
923
+ "cl-25",
924
+ "cl-26",
925
+ "cl-27",
926
+ "cl-30"
927
+ ];
928
+ const boundaryClasses = [
929
+ ...japaneseCharacterClasses,
930
+ "cl-22",
931
+ "cl-23"
932
+ ];
933
+ const OPENING = /* @__PURE__ */ new Set(`${OPENING_BRACKETS}【〘〝⦅«`);
934
+ const CLOSING = /* @__PURE__ */ new Set(`${CLOSING_BRACKETS}】〙〟⦆»`);
935
+ const HYPHENS = /* @__PURE__ */ new Set("‐‑⁃–");
936
+ const DIVIDING_MARKS = new Set(DIVIDING_PUNCTUATION);
937
+ const COMBINED_DIVIDING_MARKS = new Set(TATE_CHU_YOKO_MARKS);
938
+ const MIDDLE_DOTS = /* @__PURE__ */ new Set("・:;");
939
+ const SMALL_KANA = /* @__PURE__ */ new Set("ァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖ");
940
+ const ITERATION_MARKS = /* @__PURE__ */ new Set("ヽヾゝゞ々〻");
941
+ const INSEPARABLE = /* @__PURE__ */ new Set("—―…‥─〳〴〵");
942
+ const PREFIXED_ABBREVIATIONS = /* @__PURE__ */ new Set("¥$£##");
943
+ const POSTFIXED_ABBREVIATIONS = /* @__PURE__ */ new Set("°′″%‰");
944
+ const UNIT_SYMBOLS = /* @__PURE__ */ new Set("℃℉㎀㎁㎂㎃㎄㎅㎆㎇㎈㎉㎊㎋㎌㎍㎎㎏㎐㎑㎒㎓㎔㎕㎖㎗㎘㎙㎚㎛㎜㎝㎞㎟㎠㎡㎢");
945
+ const GROUPED_NUMERAL = /^[0-90-9]$/u;
946
+ const HIRAGANA = /^\p{Script=Hiragana}$/u;
947
+ const KATAKANA = /^\p{Script=Katakana}$/u;
948
+ const WESTERN = /^[\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}]$/u;
949
+ /**
950
+ * Order matters: a character inside a tate-chu-yoko group is cl-30 (3.2.5) and an upright Latin
951
+ * letter or Arabic numeral is cl-19 (3.2.4), whatever either would be on its own; anything left
952
+ * unmatched is cl-19 too, the class JLReq reserves for kanji and the like. A tate-chu-yoko `!?`
953
+ * stays cl-04, the class JLReq gives `⁉`, so it still may not open a line.
954
+ *
955
+ * The quarter em 3.2.6 sets between Japanese and a Western character or numeral applies only to a
956
+ * character rotated 90 degrees in vertical writing mode or mixed into horizontal writing mode —
957
+ * 3.2.6 states that scope explicitly. A character set upright keeps its own rule, 3.2.4, which
958
+ * reads it as a full-width monospace glyph classed with kanji and set solid against its neighbours,
959
+ * the same as a tate-chu-yoko run's 3.2.5.
960
+ */
961
+ function classify({ value, presentation }) {
962
+ if (presentation === "tate-chu-yoko") return COMBINED_DIVIDING_MARKS.has(value) ? "cl-04" : "cl-30";
963
+ if (presentation === "upright") return "cl-19";
964
+ if (OPENING.has(value)) return "cl-01";
965
+ if (CLOSING.has(value)) return "cl-02";
966
+ if (HYPHENS.has(value)) return "cl-03";
967
+ if (DIVIDING_MARKS.has(value)) return "cl-04";
968
+ if (MIDDLE_DOTS.has(value)) return "cl-05";
969
+ if (value === "。" || value === ".") return "cl-06";
970
+ if (value === "、" || value === ",") return "cl-07";
971
+ if (INSEPARABLE.has(value)) return "cl-08";
972
+ if (ITERATION_MARKS.has(value)) return "cl-09";
973
+ if (value === "ー") return "cl-10";
974
+ if (SMALL_KANA.has(value)) return "cl-11";
975
+ if (PREFIXED_ABBREVIATIONS.has(value)) return "cl-12";
976
+ if (POSTFIXED_ABBREVIATIONS.has(value)) return "cl-13";
977
+ if (value === " ") return "cl-14";
978
+ if (HIRAGANA.test(value)) return "cl-15";
979
+ if (KATAKANA.test(value)) return "cl-16";
980
+ if (GROUPED_NUMERAL.test(value)) return "cl-24";
981
+ if (UNIT_SYMBOLS.has(value)) return "cl-25";
982
+ if (value === " ") return "cl-26";
983
+ if (WESTERN.test(value)) return "cl-27";
984
+ return "cl-19";
985
+ }
986
+ /**
987
+ * 無印 in 表1 with no colour in 表6: the pair is set solid and line adjustment leaves it alone.
988
+ */
989
+ const SOLID = {
990
+ kind: "glue",
991
+ naturalWidthEm: 0
992
+ };
993
+ /**
994
+ * `1/4` in 表6: set solid, but line adjustment may open it up to a quarter em.
995
+ */
996
+ const SOLID_EXPANDABLE = {
997
+ kind: "glue",
998
+ naturalWidthEm: 0,
999
+ stretch: {
1000
+ category: "solid",
1001
+ amountEm: .25,
1002
+ granularity: "continuous"
1003
+ }
1004
+ };
1005
+ /**
1006
+ * `1/2` in 表1 with `1/2-0` in 表3: a half em that line adjustment may take down to solid.
1007
+ */
1008
+ const HALF = {
1009
+ kind: "glue",
1010
+ naturalWidthEm: .5,
1011
+ shrink: {
1012
+ category: "punctuation",
1013
+ amountEm: .5,
1014
+ granularity: "continuous"
1015
+ }
1016
+ };
1017
+ /**
1018
+ * `1/2` in both tables: the half em exists but is not a reduction opportunity.
1019
+ */
1020
+ const FIXED_HALF = {
1021
+ kind: "glue",
1022
+ naturalWidthEm: .5
1023
+ };
1024
+ /**
1025
+ * `1/4` in 表1 with `1/4-0` in 表3: a quarter em that may go down to solid.
1026
+ */
1027
+ const QUARTER = {
1028
+ kind: "glue",
1029
+ naturalWidthEm: .25,
1030
+ shrink: {
1031
+ category: "middle-dot",
1032
+ amountEm: .25,
1033
+ granularity: "continuous"
1034
+ }
1035
+ };
1036
+ /**
1037
+ * `1/4` in both tables: the quarter em exists but is not a reduction opportunity.
1038
+ */
1039
+ const FIXED_QUARTER = {
1040
+ kind: "glue",
1041
+ naturalWidthEm: .25
1042
+ };
1043
+ /**
1044
+ * `1/4-1/8` in 表3 with `1/4-1/2` in 表6: the Japanese-to-western quarter em, reducible to an eighth
1045
+ * and expandable to a half. 3.2.6 sets this only against a Western character rotated 90 degrees or
1046
+ * mixed into horizontal text (cl-27) and a numeral or unit symbol (cl-24, cl-25); an upright
1047
+ * Western character classifies as cl-19 instead and never reaches this constant.
1048
+ */
1049
+ const MIXED_TEXT_QUARTER = {
1050
+ kind: "glue",
1051
+ naturalWidthEm: .25,
1052
+ shrink: {
1053
+ category: "mixed-text",
1054
+ amountEm: .125,
1055
+ granularity: "continuous"
1056
+ },
1057
+ stretch: {
1058
+ category: "mixed-text",
1059
+ amountEm: .25,
1060
+ granularity: "continuous"
1061
+ }
1062
+ };
1063
+ /**
1064
+ * The same quarter em where 表6 leaves it out of the expansion: between a unit symbol and the
1065
+ * quantity on either side of it.
1066
+ */
1067
+ const UNIT_SYMBOL_QUARTER = {
1068
+ kind: "glue",
1069
+ naturalWidthEm: .25,
1070
+ shrink: {
1071
+ category: "mixed-text",
1072
+ amountEm: .125,
1073
+ granularity: "continuous"
1074
+ }
1075
+ };
1076
+ /**
1077
+ * 表1 注3 with 表3 注1: two middle dots meet, and their quarters go solid together.
1078
+ */
1079
+ const TWO_QUARTERS = {
1080
+ kind: "glue",
1081
+ naturalWidthEm: .5,
1082
+ shrink: {
1083
+ category: "middle-dot",
1084
+ amountEm: .5,
1085
+ granularity: "continuous"
1086
+ }
1087
+ };
1088
+ /**
1089
+ * 表1 注5 with 表3 注2: a full stop's fixed half em plus the following middle dot's quarter.
1090
+ */
1091
+ const FIXED_HALF_AND_QUARTER = {
1092
+ kind: "glue",
1093
+ naturalWidthEm: .75,
1094
+ shrink: {
1095
+ category: "middle-dot",
1096
+ amountEm: .25,
1097
+ granularity: "continuous"
1098
+ }
1099
+ };
1100
+ /**
1101
+ * 表1 注5 with 表3 注3: a comma's half em plus the following middle dot's quarter.
1102
+ *
1103
+ * 表3 spends the quarter at stage 4 and the half at stage 5, which one capacity cannot say. Both are
1104
+ * reducible to solid, so the total is right and only the order collapses — and `、・` is rare enough
1105
+ * that the order never shows.
1106
+ */
1107
+ const HALF_AND_QUARTER = {
1108
+ kind: "glue",
1109
+ naturalWidthEm: .75,
1110
+ shrink: {
1111
+ category: "punctuation",
1112
+ amountEm: .75,
1113
+ granularity: "continuous"
1114
+ }
1115
+ };
1116
+ /**
1117
+ * 欧文間隔(cl-26)の三分アキ. JLReq states the western word space as a third em by rule rather than by
1118
+ * measurement, and makes it the first space a line adjustment reaches for: down to a 四分アキ when the
1119
+ * line is over (3.8.3 a), up to a 二分アキ when it is short (3.8.4 a).
1120
+ *
1121
+ * The two capacities are written as differences from the limits rather than as `1/12` and `1/6`,
1122
+ * because a third of an em is not representable in binary while the limits JLReq states are: this
1123
+ * way `naturalWidthEm - shrink.amountEm` is exactly 0.25 and `naturalWidthEm + stretch.amountEm`
1124
+ * exactly 0.5.
1125
+ */
1126
+ const WORD_SPACE = {
1127
+ kind: "glue",
1128
+ naturalWidthEm: 1 / 3,
1129
+ shrink: {
1130
+ category: "word-space",
1131
+ amountEm: 1 / 3 - 1 / 4,
1132
+ granularity: "continuous"
1133
+ },
1134
+ stretch: {
1135
+ category: "word-space",
1136
+ amountEm: 1 / 2 - 1 / 3,
1137
+ granularity: "continuous"
1138
+ }
1139
+ };
1140
+ /**
1141
+ * 表1 注13 と 表3 注4: a word space at a line head or a line end has no width and is no adjustment
1142
+ * opportunity, since the white would fall outside the text area where no reader can see it. 注13
1143
+ * adds ただし,この部分が移動し,これとは異なる配置になった場合は,欧文間隔(cl-26)の空き量を確保する — which is why the answer is given per
1144
+ * candidate line and not once per source line.
1145
+ */
1146
+ const EDGE_WORD_SPACE = {
1147
+ kind: "glue",
1148
+ naturalWidthEm: 0
1149
+ };
1150
+ /**
1151
+ * The classes that take a quarter em against a numeral, unit symbol or western character rotated 90
1152
+ * degrees or mixed into horizontal text (3.2.6) — an upright western character classifies as cl-19
1153
+ * and is covered by the general kanji rows and columns instead. Dividing punctuation is on the list
1154
+ * in that direction only, because the space before a `?` or `!` is solid (3.1.6).
1155
+ */
1156
+ const JAPANESE_BEFORE_WESTERN = [
1157
+ "cl-04",
1158
+ "cl-09",
1159
+ "cl-10",
1160
+ "cl-11",
1161
+ "cl-15",
1162
+ "cl-16",
1163
+ "cl-19",
1164
+ "cl-30",
1165
+ "cl-22",
1166
+ "cl-23"
1167
+ ];
1168
+ const JAPANESE_AFTER_WESTERN = [
1169
+ "cl-09",
1170
+ "cl-10",
1171
+ "cl-11",
1172
+ "cl-15",
1173
+ "cl-16",
1174
+ "cl-19",
1175
+ "cl-30",
1176
+ "cl-22",
1177
+ "cl-23"
1178
+ ];
1179
+ const NUMERAL_UNIT_OR_WESTERN = [
1180
+ "cl-24",
1181
+ "cl-25",
1182
+ "cl-27"
1183
+ ];
1184
+ const PUNCTUATION_TAKING_SPACE_AFTER = [
1185
+ "cl-02",
1186
+ "cl-06",
1187
+ "cl-07"
1188
+ ];
1189
+ /**
1190
+ * 表6 の色付きの小間: the classes a solid pair may be opened up between at stage 3. Neither list holds a
1191
+ * bracket, a middle dot, a full stop, a comma, a hyphen, an ideographic space or a word space —
1192
+ * JLReq opens none of those — and only the left-hand list holds a numeral, unit symbol or western
1193
+ * character, since the space on their Japanese side belongs to stage 2 instead.
1194
+ */
1195
+ const EXPANDABLE_BEFORE = [
1196
+ "cl-08",
1197
+ "cl-09",
1198
+ "cl-10",
1199
+ "cl-11",
1200
+ "cl-12",
1201
+ "cl-13",
1202
+ "cl-15",
1203
+ "cl-16",
1204
+ "cl-19",
1205
+ "cl-24",
1206
+ "cl-25",
1207
+ "cl-27",
1208
+ "cl-30",
1209
+ "cl-22",
1210
+ "cl-23"
1211
+ ];
1212
+ const EXPANDABLE_AFTER = [
1213
+ "cl-08",
1214
+ "cl-09",
1215
+ "cl-10",
1216
+ "cl-11",
1217
+ "cl-12",
1218
+ "cl-13",
1219
+ "cl-15",
1220
+ "cl-16",
1221
+ "cl-19",
1222
+ "cl-30",
1223
+ "cl-22",
1224
+ "cl-23"
1225
+ ];
1226
+ /**
1227
+ * The gray, blue and pink cells of 表6: all pairs that join the fourth and final expansion stage.
1228
+ * The finite capacities above cover the blue and pink cells at stages three and two; once those
1229
+ * stages are exhausted, JLReq 3.8.4 d adds the remainder equally across every cell named here.
1230
+ *
1231
+ * A cl-08/cl-08 cell is blue only for different kinds of mark (注4). BoundaryRule excludes binding
1232
+ * sequences using the original character values.
1233
+ */
1234
+ const FINAL_EXPANSION_BASIC_AFTER = /* @__PURE__ */ new Set([
1235
+ "cl-01",
1236
+ "cl-08",
1237
+ "cl-12",
1238
+ "cl-13",
1239
+ "cl-14",
1240
+ "cl-15",
1241
+ "cl-16",
1242
+ "cl-19",
1243
+ "cl-24",
1244
+ "cl-25",
1245
+ "cl-26",
1246
+ "cl-27",
1247
+ "cl-30"
1248
+ ]);
1249
+ const FINAL_EXPANSION_WIDE_AFTER = /* @__PURE__ */ new Set([
1250
+ ...FINAL_EXPANSION_BASIC_AFTER,
1251
+ "cl-09",
1252
+ "cl-10",
1253
+ "cl-11"
1254
+ ]);
1255
+ const FINAL_EXPANSION_AFTER = new Map([
1256
+ ...[
1257
+ "cl-02",
1258
+ "cl-03",
1259
+ "cl-04",
1260
+ "cl-05",
1261
+ "cl-06",
1262
+ "cl-07",
1263
+ "cl-14"
1264
+ ].map((left) => [left, FINAL_EXPANSION_BASIC_AFTER]),
1265
+ ...[
1266
+ "cl-09",
1267
+ "cl-10",
1268
+ "cl-11",
1269
+ "cl-13",
1270
+ "cl-15",
1271
+ "cl-16",
1272
+ "cl-19",
1273
+ "cl-30"
1274
+ ].map((left) => [left, FINAL_EXPANSION_WIDE_AFTER]),
1275
+ ["cl-08", FINAL_EXPANSION_WIDE_AFTER],
1276
+ ["cl-12", /* @__PURE__ */ new Set([
1277
+ "cl-08",
1278
+ "cl-09",
1279
+ "cl-10",
1280
+ "cl-11",
1281
+ "cl-12",
1282
+ "cl-13",
1283
+ "cl-15",
1284
+ "cl-16",
1285
+ "cl-19",
1286
+ "cl-25",
1287
+ "cl-26",
1288
+ "cl-27",
1289
+ "cl-30"
1290
+ ])],
1291
+ ["cl-24", /* @__PURE__ */ new Set([
1292
+ "cl-01",
1293
+ "cl-08",
1294
+ "cl-09",
1295
+ "cl-10",
1296
+ "cl-11",
1297
+ "cl-12",
1298
+ "cl-14",
1299
+ "cl-15",
1300
+ "cl-16",
1301
+ "cl-19",
1302
+ "cl-25",
1303
+ "cl-26",
1304
+ "cl-30"
1305
+ ])],
1306
+ ["cl-25", /* @__PURE__ */ new Set([
1307
+ "cl-01",
1308
+ "cl-08",
1309
+ "cl-09",
1310
+ "cl-10",
1311
+ "cl-11",
1312
+ "cl-12",
1313
+ "cl-13",
1314
+ "cl-14",
1315
+ "cl-15",
1316
+ "cl-16",
1317
+ "cl-19",
1318
+ "cl-24",
1319
+ "cl-26",
1320
+ "cl-30"
1321
+ ])],
1322
+ ["cl-26", new Set(japaneseCharacterClasses.filter((characterClass) => characterClass !== "cl-02"))],
1323
+ ["cl-27", /* @__PURE__ */ new Set([
1324
+ "cl-01",
1325
+ "cl-08",
1326
+ "cl-09",
1327
+ "cl-10",
1328
+ "cl-11",
1329
+ "cl-12",
1330
+ "cl-13",
1331
+ "cl-14",
1332
+ "cl-15",
1333
+ "cl-16",
1334
+ "cl-19",
1335
+ "cl-26",
1336
+ "cl-30"
1337
+ ])]
1338
+ ]);
1339
+ function canExpandAtFinalStage(left, right) {
1340
+ if (left === "cl-14" || left === "cl-26" || left === "cl-27" || right === "cl-14" || right === "cl-26" || right === "cl-27") return false;
1341
+ const leftClass = left === "cl-22" || left === "cl-23" ? "cl-19" : left;
1342
+ const rightClass = right === "cl-22" || right === "cl-23" ? "cl-19" : right;
1343
+ if (FINAL_EXPANSION_AFTER.get(leftClass)?.has(rightClass) !== true) return false;
1344
+ return pairSpacing(left, right).naturalWidthEm === 0;
1345
+ }
1346
+ /**
1347
+ * The classes JLReq 3.1.2 sets on a half em rather than a full one.
1348
+ */
1349
+ const HALF_EM_CLASSES = /* @__PURE__ */ new Set([
1350
+ "cl-01",
1351
+ "cl-02",
1352
+ "cl-05",
1353
+ "cl-06",
1354
+ "cl-07"
1355
+ ]);
1356
+ const PAIR_SPACING_TABLE = new Map([
1357
+ {
1358
+ left: "any",
1359
+ right: "any",
1360
+ spacing: SOLID
1361
+ },
1362
+ {
1363
+ left: EXPANDABLE_BEFORE,
1364
+ right: EXPANDABLE_AFTER,
1365
+ spacing: SOLID_EXPANDABLE
1366
+ },
1367
+ {
1368
+ left: [
1369
+ "cl-08",
1370
+ "cl-12",
1371
+ "cl-13"
1372
+ ],
1373
+ right: NUMERAL_UNIT_OR_WESTERN,
1374
+ spacing: SOLID_EXPANDABLE
1375
+ },
1376
+ {
1377
+ left: ["cl-08"],
1378
+ right: ["cl-24", "cl-25"],
1379
+ spacing: SOLID
1380
+ },
1381
+ {
1382
+ left: ["cl-12"],
1383
+ right: ["cl-24"],
1384
+ spacing: SOLID
1385
+ },
1386
+ {
1387
+ left: ["cl-24"],
1388
+ right: ["cl-13"],
1389
+ spacing: SOLID
1390
+ },
1391
+ {
1392
+ left: [
1393
+ "cl-09",
1394
+ "cl-10",
1395
+ "cl-11"
1396
+ ],
1397
+ right: ["cl-08"],
1398
+ spacing: SOLID
1399
+ },
1400
+ {
1401
+ left: "any",
1402
+ right: ["cl-01"],
1403
+ spacing: HALF
1404
+ },
1405
+ {
1406
+ left: "any",
1407
+ right: ["cl-05"],
1408
+ spacing: QUARTER
1409
+ },
1410
+ {
1411
+ left: "any",
1412
+ right: PUNCTUATION_TAKING_SPACE_AFTER,
1413
+ spacing: SOLID
1414
+ },
1415
+ {
1416
+ left: JAPANESE_BEFORE_WESTERN,
1417
+ right: NUMERAL_UNIT_OR_WESTERN,
1418
+ spacing: MIXED_TEXT_QUARTER
1419
+ },
1420
+ {
1421
+ left: NUMERAL_UNIT_OR_WESTERN,
1422
+ right: JAPANESE_AFTER_WESTERN,
1423
+ spacing: MIXED_TEXT_QUARTER
1424
+ },
1425
+ {
1426
+ left: ["cl-25"],
1427
+ right: ["cl-24"],
1428
+ spacing: UNIT_SYMBOL_QUARTER
1429
+ },
1430
+ {
1431
+ left: ["cl-27"],
1432
+ right: ["cl-25"],
1433
+ spacing: UNIT_SYMBOL_QUARTER
1434
+ },
1435
+ {
1436
+ left: ["cl-24"],
1437
+ right: ["cl-25"],
1438
+ spacing: FIXED_QUARTER
1439
+ },
1440
+ {
1441
+ left: ["cl-02", "cl-07"],
1442
+ right: "any",
1443
+ spacing: HALF
1444
+ },
1445
+ {
1446
+ left: ["cl-06"],
1447
+ right: "any",
1448
+ spacing: FIXED_HALF
1449
+ },
1450
+ {
1451
+ left: ["cl-02"],
1452
+ right: ["cl-05"],
1453
+ spacing: QUARTER
1454
+ },
1455
+ {
1456
+ left: ["cl-06"],
1457
+ right: ["cl-05"],
1458
+ spacing: FIXED_HALF_AND_QUARTER
1459
+ },
1460
+ {
1461
+ left: ["cl-07"],
1462
+ right: ["cl-05"],
1463
+ spacing: HALF_AND_QUARTER
1464
+ },
1465
+ {
1466
+ left: PUNCTUATION_TAKING_SPACE_AFTER,
1467
+ right: PUNCTUATION_TAKING_SPACE_AFTER,
1468
+ spacing: SOLID
1469
+ },
1470
+ {
1471
+ left: ["cl-02"],
1472
+ right: ["cl-14"],
1473
+ spacing: SOLID
1474
+ },
1475
+ {
1476
+ left: ["cl-05"],
1477
+ right: "any",
1478
+ spacing: QUARTER
1479
+ },
1480
+ {
1481
+ left: ["cl-05"],
1482
+ right: ["cl-05"],
1483
+ spacing: TWO_QUARTERS
1484
+ },
1485
+ {
1486
+ left: ["cl-01"],
1487
+ right: "any",
1488
+ spacing: SOLID
1489
+ },
1490
+ {
1491
+ left: ["cl-01"],
1492
+ right: ["cl-05"],
1493
+ spacing: QUARTER
1494
+ },
1495
+ {
1496
+ left: ["cl-14"],
1497
+ right: "any",
1498
+ spacing: SOLID
1499
+ },
1500
+ {
1501
+ left: ["cl-14"],
1502
+ right: ["cl-05"],
1503
+ spacing: QUARTER
1504
+ },
1505
+ {
1506
+ left: "any",
1507
+ right: ["cl-26"],
1508
+ spacing: SOLID
1509
+ },
1510
+ {
1511
+ left: PUNCTUATION_TAKING_SPACE_AFTER,
1512
+ right: ["cl-26"],
1513
+ spacing: FIXED_HALF
1514
+ },
1515
+ {
1516
+ left: ["cl-05"],
1517
+ right: ["cl-26"],
1518
+ spacing: FIXED_QUARTER
1519
+ },
1520
+ {
1521
+ left: ["cl-26"],
1522
+ right: "any",
1523
+ spacing: SOLID
1524
+ },
1525
+ {
1526
+ left: ["cl-26"],
1527
+ right: ["cl-01"],
1528
+ spacing: FIXED_HALF
1529
+ },
1530
+ {
1531
+ left: ["cl-26"],
1532
+ right: ["cl-05"],
1533
+ spacing: FIXED_QUARTER
1534
+ },
1535
+ {
1536
+ left: ["cl-08"],
1537
+ right: ["cl-08"],
1538
+ spacing: SOLID_EXPANDABLE
1539
+ }
1540
+ ].flatMap(({ left, right, spacing }) => (left === "any" ? boundaryClasses : left).flatMap((leftClass) => (right === "any" ? boundaryClasses : right).map((rightClass) => [`${leftClass}/${rightClass}`, spacing]))));
1541
+ function pairSpacing(left, right) {
1542
+ return PAIR_SPACING_TABLE.get(`${left}/${right}`) ?? SOLID;
1543
+ }
1544
+ /**
1545
+ * `null` removes the break opportunity outright rather than pricing it, because the three rules
1546
+ * below are prohibitions and not preferences.
1547
+ *
1548
+ * Line-end prohibition (行末禁則, JLReq 3.1.8): an opening bracket or a prefixed abbreviation may not
1549
+ * be the last character on a line, since it belongs to what follows it.
1550
+ *
1551
+ * Line-start prohibition (行頭禁則, JLReq 3.1.7): closing brackets, hyphens, dividing punctuation,
1552
+ * middle dots, full stops, commas, iteration marks, the prolonged sound mark, small kana and
1553
+ * postfixed abbreviations may not open a line.
1554
+ *
1555
+ * Binding sequences (JLReq C.2 note 5) depend on the actual characters and are resolved separately
1556
+ * by BoundaryRule.
1557
+ */
1558
+ function breakPenalty(left, right) {
1559
+ if (left === "cl-01" || left === "cl-12") return null;
1560
+ 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;
1561
+ return 0;
1562
+ }
1563
+ const defaultJapaneseTypesettingRules = {
1564
+ classify,
1565
+ /**
1566
+ * Opening and closing brackets, full stops, commas and middle dots are set on a half em (JLReq
1567
+ * 3.1.2); the space the pair table puts around them is what brings each back to a full em.
1568
+ *
1569
+ * A fullwidth glyph keeps drawing its ink where the font put it, so halving the advance alone
1570
+ * would leave the ink outside the box. The offset pulls it back: an opening bracket draws in the
1571
+ * trailing half of its em, a middle dot in the centre, and the rest in the leading half.
1572
+ */
1573
+ boxMetrics: (characterClass, measuredAdvanceEm) => {
1574
+ if (characterClass === "cl-26") return {
1575
+ advanceEm: 0,
1576
+ renderOffsetEm: 0
1577
+ };
1578
+ const advanceEm = HALF_EM_CLASSES.has(characterClass) ? Math.min(.5, measuredAdvanceEm) : measuredAdvanceEm;
1579
+ const inkOverhangEm = measuredAdvanceEm - advanceEm;
1580
+ return {
1581
+ advanceEm,
1582
+ renderOffsetEm: characterClass === "cl-01" ? -inkOverhangEm : characterClass === "cl-05" ? -inkOverhangEm / 2 : 0
1583
+ };
1584
+ },
1585
+ pairSpacing,
1586
+ spacingCharacter: (characterClass, position) => characterClass !== "cl-26" ? null : position === "mid-line" ? WORD_SPACE : EDGE_WORD_SPACE,
1587
+ /**
1588
+ * The half em trailing a closing bracket, full stop or comma at the line end, and the quarter em
1589
+ * trailing a middle dot there (JLReq 3.1.9). The book style selects their order and visual cost.
1590
+ *
1591
+ * Both are all-or-nothing. 3.1.9 admits the full アキ or ベタ組 and forbids anything in between, so a
1592
+ * line whose overflow is smaller than the アキ has to find the difference somewhere else — or break
1593
+ * elsewhere. A line-end middle dot takes the quarter before it into the same amount (表3 注5).
1594
+ */
1595
+ lineEndSpacing: (last) => last === "cl-02" || last === "cl-06" || last === "cl-07" ? {
1596
+ spacing: {
1597
+ kind: "glue",
1598
+ naturalWidthEm: .5,
1599
+ shrink: {
1600
+ category: "line-end",
1601
+ amountEm: .5,
1602
+ granularity: "all-or-nothing"
1603
+ }
1604
+ },
1605
+ absorbsPrecedingEm: 0
1606
+ } : last === "cl-05" ? {
1607
+ spacing: {
1608
+ kind: "glue",
1609
+ naturalWidthEm: .25,
1610
+ shrink: {
1611
+ category: "line-end",
1612
+ amountEm: .25,
1613
+ granularity: "all-or-nothing"
1614
+ }
1615
+ },
1616
+ absorbsPrecedingEm: .25
1617
+ } : null,
1618
+ canExpandAtFinalStage,
1619
+ breakPenalty,
1620
+ /**
1621
+ * Hanging punctuation (ぶら下げ組, JLReq 3.8.2) lets a full stop or comma sit outside the text area
1622
+ * instead of forcing the line to be respaced. JLReq admits no other class for it.
1623
+ */
1624
+ canHang: (characterClass) => characterClass === "cl-07" || characterClass === "cl-06"
1625
+ };
1626
+ const defaultJapaneseTypesettingProfile = { of: (rules, style) => {
1627
+ const resolved = /* @__PURE__ */ new WeakMap();
1628
+ const spacing = (rule) => {
1629
+ const cached = resolved.get(rule);
1630
+ if (cached !== void 0) return cached;
1631
+ const result = {
1632
+ kind: rule.kind,
1633
+ naturalWidthEm: rule.naturalWidthEm,
1634
+ ...rule.shrink === void 0 ? {} : { shrink: {
1635
+ amountEm: rule.shrink.amountEm,
1636
+ granularity: rule.shrink.granularity,
1637
+ ...style.adjustment("shrink", rule.shrink.category)
1638
+ } },
1639
+ ...rule.stretch === void 0 ? {} : { stretch: {
1640
+ amountEm: rule.stretch.amountEm,
1641
+ granularity: rule.stretch.granularity,
1642
+ ...style.adjustment("stretch", rule.stretch.category)
1643
+ } }
1644
+ };
1645
+ resolved.set(rule, result);
1646
+ return result;
1647
+ };
1648
+ return {
1649
+ classify: rules.classify,
1650
+ boxMetrics: rules.boxMetrics,
1651
+ pairSpacing: (left, right) => spacing(rules.pairSpacing(left, right)),
1652
+ spacingCharacter: (characterClass, position) => {
1653
+ const rule = rules.spacingCharacter(characterClass, position);
1654
+ return rule === null ? null : {
1655
+ ...spacing(rule),
1656
+ kind: "glue"
1657
+ };
1658
+ },
1659
+ lineStartSpacing: style.lineStartSpacing,
1660
+ lineEndSpacing: (last) => {
1661
+ const rule = rules.lineEndSpacing(last);
1662
+ return rule === null ? null : {
1663
+ spacing: spacing(rule.spacing),
1664
+ absorbsPrecedingEm: rule.absorbsPrecedingEm
1665
+ };
1666
+ },
1667
+ finalStretchCostPerEm: style.finalStretchCostPerEm,
1668
+ canExpandAtFinalStage: rules.canExpandAtFinalStage,
1669
+ breakPenalty: rules.breakPenalty,
1670
+ canHang: rules.canHang
1671
+ };
1672
+ } }.of(defaultJapaneseTypesettingRules, defaultBookStyle);
1673
+ const InlineSpan = { schema: readonlyObject({
1674
+ offsetEm: v.pipe(v.number(), v.finite()),
1675
+ advanceEm: v.pipe(v.number(), v.finite(), v.minValue(0))
1676
+ }) };
1677
+ const MeasuredCluster = { schema: readonlyObject({
1678
+ textRange: MeasurementTextRange.schema,
1679
+ layoutSpan: InlineSpan.schema,
1680
+ renderSpan: InlineSpan.schema
1681
+ }) };
1682
+ //#endregion
1683
+ //#region src/composer/run-measurement.ts
1684
+ const advanceEm = v.pipe(v.number(), v.finite(), v.minValue(0));
1685
+ const RunMeasurementSchema = v.variant("kind", [readonlyObject({
1686
+ kind: v.literal("advance"),
1687
+ advanceEm
1688
+ }), readonlyObject({
1689
+ kind: v.literal("clustered"),
1690
+ advanceEm,
1691
+ clusters: readonlyArray(MeasuredCluster.schema)
1692
+ })]);
1693
+ const RunMeasurement = {
1694
+ schema: RunMeasurementSchema,
1695
+ parseFor: (request, raw) => {
1696
+ const result = v.safeParse(RunMeasurementSchema, raw);
1697
+ if (!result.success) return void 0;
1698
+ const measurement = result.output;
1699
+ if (measurement.kind === "advance") return measurement;
1700
+ const boundaries = new Set([...graphemeSegmenter.segment(request.text)].map(({ index }) => index));
1701
+ boundaries.add(request.text.length);
1702
+ let textEnd = 0;
1703
+ let layoutEnd = 0;
1704
+ for (const cluster of measurement.clusters) {
1705
+ const { textRange, layoutSpan } = cluster;
1706
+ if (textRange.start !== textEnd || textRange.end <= textRange.start || !boundaries.has(textRange.start) || !boundaries.has(textRange.end) || Math.abs(layoutSpan.offsetEm - layoutEnd) > 1e-9) return void 0;
1707
+ textEnd = textRange.end;
1708
+ layoutEnd = layoutSpan.offsetEm + layoutSpan.advanceEm;
1709
+ }
1710
+ return textEnd === request.text.length && Math.abs(layoutEnd - measurement.advanceEm) <= 1e-9 ? measurement : void 0;
1711
+ }
1712
+ };
1713
+ //#endregion
1714
+ //#region src/composer/internal/measurement-session.ts
1715
+ const MeasurementSession = { create: (provider, settings) => {
1716
+ const cache = /* @__PURE__ */ new Map();
1717
+ const get = (text, kind, presentation = "mixed") => {
1718
+ const request = kind === "base" ? {
1719
+ text,
1720
+ kind,
1721
+ presentation,
1722
+ fontPreset: settings.appearance.fontPreset,
1723
+ fontSizePt: settings.appearance.fontSizePt,
1724
+ scale: 1,
1725
+ writingMode: "vertical-rl"
1726
+ } : {
1727
+ text,
1728
+ kind,
1729
+ presentation: "mixed",
1730
+ fontPreset: settings.appearance.fontPreset,
1731
+ fontSizePt: settings.appearance.fontSizePt,
1732
+ scale: defaultBookStyle.annotation.sizeEm,
1733
+ writingMode: "vertical-rl"
1734
+ };
1735
+ const key = JSON.stringify(request);
1736
+ if (cache.has(key)) return cache.get(key);
1737
+ const measurement = RunMeasurement.parseFor(request, provider(request));
1738
+ cache.set(key, measurement);
1739
+ return measurement;
1740
+ };
1741
+ return {
1742
+ get,
1743
+ pieces: (text, kind, presentation) => {
1744
+ const full = get(text, kind, presentation);
1745
+ if (full === void 0) return void 0;
1746
+ if (full.kind === "clustered") return full.clusters.map((cluster) => ({
1747
+ textRange: cluster.textRange,
1748
+ layoutSpan: cluster.layoutSpan,
1749
+ renderSpan: cluster.renderSpan,
1750
+ value: text.slice(cluster.textRange.start, cluster.textRange.end)
1751
+ }));
1752
+ const segments = [...graphemeSegmenter.segment(text)];
1753
+ if (kind === "base" && presentation === "tate-chu-yoko") {
1754
+ const advanceEm = full.advanceEm / segments.length;
1755
+ return segments.map(({ segment, index }, position) => {
1756
+ const span = {
1757
+ offsetEm: position * advanceEm,
1758
+ advanceEm
1759
+ };
1760
+ return {
1761
+ value: segment,
1762
+ textRange: MeasurementTextRange.of({
1763
+ start: index,
1764
+ end: index + segment.length
1765
+ }),
1766
+ layoutSpan: span,
1767
+ renderSpan: span
1768
+ };
1769
+ });
1770
+ }
1771
+ const pieces = [];
1772
+ let offsetEm = 0;
1773
+ for (const { segment, index } of segments) {
1774
+ const measured = get(segment, kind, presentation);
1775
+ if (measured === void 0) return void 0;
1776
+ const cluster = measured.kind === "clustered" ? measured.clusters[0] : void 0;
1777
+ pieces.push({
1778
+ value: segment,
1779
+ textRange: MeasurementTextRange.of({
1780
+ start: index,
1781
+ end: index + segment.length
1782
+ }),
1783
+ layoutSpan: {
1784
+ offsetEm,
1785
+ advanceEm: measured.advanceEm
1786
+ },
1787
+ renderSpan: {
1788
+ offsetEm: offsetEm + (cluster?.renderSpan.offsetEm ?? 0),
1789
+ advanceEm: cluster?.renderSpan.advanceEm ?? measured.advanceEm
1790
+ }
1791
+ });
1792
+ offsetEm += measured.advanceEm;
1793
+ }
1794
+ return pieces;
1795
+ }
1796
+ };
1797
+ } };
1798
+ //#endregion
1799
+ //#region src/composer/source-placement.ts
1800
+ const memberRange = v.pipe(ManuscriptRange.schema, v.check(({ graphemes }) => graphemes.end === graphemes.start + 1));
1801
+ const SourcePlacement = {
1802
+ schema: v.variant("kind", [readonlyObject({
1803
+ kind: v.literal("exact"),
1804
+ range: memberRange,
1805
+ layoutSpan: InlineSpan.schema
1806
+ }), readonlyObject({
1807
+ kind: v.literal("shared"),
1808
+ ranges: v.pipe(readonlyArray(memberRange), v.minLength(2)),
1809
+ layoutSpan: InlineSpan.schema
1810
+ })]),
1811
+ ranges: (placement) => placement.kind === "exact" ? [placement.range] : placement.ranges
1812
+ };
1813
+ //#endregion
1814
+ //#region src/composer/internal/render-unit-contract.ts
1815
+ const renderUnitEntries = {
1816
+ value: v.pipe(v.string(), v.nonEmpty()),
1817
+ range: ManuscriptRange.schema,
1818
+ layoutSpan: InlineSpan.schema,
1819
+ renderSpan: InlineSpan.schema,
1820
+ sources: v.pipe(readonlyArray(SourcePlacement.schema), v.nonEmpty())
1821
+ };
1822
+ function validRenderSources(unit) {
1823
+ const ranges = unit.sources.flatMap(SourcePlacement.ranges);
1824
+ if ([...graphemeSegmenter.segment(unit.value)].length !== ranges.length || unit.value.length !== unit.range.display.end - unit.range.display.start || ranges[0]?.source.start !== unit.range.source.start) return false;
1825
+ let end = unit.range.graphemes.start;
1826
+ let displayEnd = unit.range.display.start;
1827
+ let sourceEnd = unit.range.source.start;
1828
+ let layoutEnd = unit.layoutSpan.offsetEm;
1829
+ for (const placement of unit.sources) {
1830
+ if (Math.abs(placement.layoutSpan.offsetEm - layoutEnd) > 1e-9) return false;
1831
+ layoutEnd += placement.layoutSpan.advanceEm;
1832
+ for (const range of SourcePlacement.ranges(placement)) {
1833
+ if (range.graphemes.start !== end || range.display.start !== displayEnd || range.source.start < sourceEnd) return false;
1834
+ end = range.graphemes.end;
1835
+ displayEnd = range.display.end;
1836
+ sourceEnd = range.source.end;
1837
+ }
1838
+ }
1839
+ return end === unit.range.graphemes.end && displayEnd === unit.range.display.end && sourceEnd === unit.range.source.end && Math.abs(layoutEnd - unit.layoutSpan.offsetEm - unit.layoutSpan.advanceEm) <= 1e-9;
1840
+ }
1841
+ const CombinedGlyphUnit = { schema: v.pipe(readonlyObject({
1842
+ ...renderUnitEntries,
1843
+ kind: v.literal("combined-glyph"),
1844
+ presentation: v.picklist(["tate-chu-yoko", "sideways"]),
1845
+ disposition: v.literal("placed")
1846
+ }), v.check((unit) => unit.range.graphemes.end >= unit.range.graphemes.start + 2 && validRenderSources(unit))) };
1847
+ //#endregion
1848
+ //#region src/composer/composed-inline-item.ts
1849
+ const signedEm = () => v.pipe(v.number(), v.finite());
1850
+ const GeneratedGlueItemSchema = readonlyObject({
1851
+ kind: v.literal("glue"),
1852
+ origin: v.literal("generated"),
1853
+ offsetEm: signedEm(),
1854
+ widthEm: signedEm(),
1855
+ naturalWidthEm: signedEm(),
1856
+ adjustment: v.union([
1857
+ v.literal("natural"),
1858
+ v.literal("shrunk"),
1859
+ v.literal("stretched")
1860
+ ])
1861
+ });
1862
+ const SourceGlueItemSchema = readonlyObject({
1863
+ kind: v.literal("glue"),
1864
+ origin: v.literal("source"),
1865
+ value: v.string(),
1866
+ range: ManuscriptRange.schema,
1867
+ offsetEm: signedEm(),
1868
+ widthEm: signedEm(),
1869
+ naturalWidthEm: signedEm(),
1870
+ adjustment: v.union([
1871
+ v.literal("natural"),
1872
+ v.literal("shrunk"),
1873
+ v.literal("stretched")
1874
+ ])
1875
+ });
1876
+ const KernItemSchema = readonlyObject({
1877
+ kind: v.literal("kern"),
1878
+ offsetEm: signedEm(),
1879
+ widthEm: signedEm()
1880
+ });
1881
+ const SuppressedItemSchema = readonlyObject({
1882
+ kind: v.literal("suppressed"),
1883
+ value: v.string(),
1884
+ range: ManuscriptRange.schema,
1885
+ reason: v.literal("question-or-exclamation-gap")
1886
+ });
1887
+ const ComposedGlue = { schema: v.union([GeneratedGlueItemSchema, SourceGlueItemSchema]) };
1888
+ const ComposedKern = { schema: KernItemSchema };
1889
+ const SuppressedInlineItem = { schema: SuppressedItemSchema };
1890
+ const SingleGlyphUnit = { schema: v.pipe(readonlyObject({
1891
+ ...renderUnitEntries,
1892
+ kind: v.literal("glyph"),
1893
+ presentation: v.picklist([
1894
+ "mixed",
1895
+ "upright",
1896
+ "sideways"
1897
+ ]),
1898
+ disposition: v.picklist(["placed", "hanging"])
1899
+ }), v.check((unit) => unit.range.graphemes.end === unit.range.graphemes.start + 1 && validRenderSources(unit)), v.check((unit) => unit.disposition !== "hanging" || unit.layoutSpan.advanceEm === 0)) };
1900
+ const PositionedInlineItem = {
1901
+ schema: v.union([
1902
+ SingleGlyphUnit.schema,
1903
+ CombinedGlyphUnit.schema,
1904
+ ComposedGlue.schema,
1905
+ ComposedKern.schema,
1906
+ SuppressedInlineItem.schema
1907
+ ]),
1908
+ isRenderUnit: (item) => item.kind === "glyph" || item.kind === "combined-glyph"
1909
+ };
1910
+ //#endregion
1911
+ //#region src/composer/internal/novel-source-contract.ts
1912
+ function sameRange(left, right) {
1913
+ return left.source.start === right.source.start && left.source.end === right.source.end && left.display.start === right.display.start && left.display.end === right.display.end && left.graphemes.start === right.graphemes.start && left.graphemes.end === right.graphemes.end;
1914
+ }
1915
+ function rangeKey(range) {
1916
+ return `${range.source.start}:${range.source.end}:${range.display.start}:${range.display.end}:${range.graphemes.start}:${range.graphemes.end}`;
1917
+ }
1918
+ const NovelSourceContract = { matches: (manuscript, lines) => {
1919
+ const expected = manuscript.graphemes.filter(({ value }) => ![
1920
+ "\n",
1921
+ "\r",
1922
+ "\r\n"
1923
+ ].includes(value));
1924
+ const members = new Map(expected.map((grapheme) => [grapheme.range.graphemes.start, grapheme]));
1925
+ const seen = /* @__PURE__ */ new Set();
1926
+ for (const line of lines) for (const item of line.items) {
1927
+ const ranges = PositionedInlineItem.isRenderUnit(item) ? item.sources.flatMap(SourcePlacement.ranges) : item.kind === "suppressed" || item.kind === "glue" && item.origin === "source" ? [item.range] : [];
1928
+ if (ranges.length === 0) continue;
1929
+ if (!("value" in item) || item.value !== manuscript.displayText.slice(item.range.display.start, item.range.display.end)) return false;
1930
+ for (const range of ranges) {
1931
+ const member = members.get(range.graphemes.start);
1932
+ if (member === void 0 || seen.has(range.graphemes.start) || !sameRange(member.range, range)) return false;
1933
+ seen.add(range.graphemes.start);
1934
+ }
1935
+ }
1936
+ if (seen.size !== expected.length) return false;
1937
+ const readings = /* @__PURE__ */ new Map();
1938
+ for (const { annotations } of lines) for (const fragment of annotations) {
1939
+ if (fragment.kind !== "ruby") continue;
1940
+ const key = rangeKey(fragment.annotationRange);
1941
+ const parts = readings.get(key);
1942
+ if (parts === void 0) readings.set(key, [fragment.reading]);
1943
+ else parts.push(fragment.reading);
1944
+ }
1945
+ return manuscript.annotations.every((annotation) => {
1946
+ if (annotation.kind !== "ruby") return true;
1947
+ const reading = annotation.reading.kind === "group" ? annotation.reading.text : annotation.reading.segments.join("");
1948
+ return (readings.get(rangeKey(annotation.range)) ?? []).join("") === reading;
1949
+ });
1950
+ } };
1951
+ //#endregion
1952
+ //#region src/composer/internal/paragraph-evaluation.ts
1953
+ const EPSILON$3 = 1e-9;
1954
+ function scoreFor(line, previousFitness) {
1955
+ const mode = line.break.kind;
1956
+ const fitness = Math.min(3, Math.floor(line.deformationRatio * 4));
1957
+ const transition = Math.abs(previousFitness - fitness) > 1 ? 1 : 0;
1958
+ const modeCounts = {
1959
+ forced: mode === "forced" ? 1 : 0,
1960
+ stretched: mode === "stretched" ? 1 : 0,
1961
+ hanging: mode === "hanging" ? 1 : 0,
1962
+ shrunk: mode === "shrunk" && line.deformationCost > EPSILON$3 ? 1 : 0
1963
+ };
1964
+ return {
1965
+ score: [
1966
+ modeCounts.forced,
1967
+ modeCounts.stretched,
1968
+ modeCounts.hanging,
1969
+ modeCounts.shrunk,
1970
+ line.deformationCost,
1971
+ line.deformationRatio ** 3,
1972
+ transition
1973
+ ],
1974
+ fitness
1975
+ };
1976
+ }
1977
+ function addScore(left, right) {
1978
+ return [
1979
+ left[0] + right[0],
1980
+ left[1] + right[1],
1981
+ left[2] + right[2],
1982
+ left[3] + right[3],
1983
+ left[4] + right[4],
1984
+ left[5] + right[5],
1985
+ left[6] + right[6]
1986
+ ];
1987
+ }
1988
+ const defaultParagraphEvaluation = { scoreFor };
1989
+ //#endregion
1990
+ //#region src/composer/internal/paragraph-layout.ts
1991
+ const EPSILON$2 = 1e-9;
1992
+ function skipSourceGaps(atoms, start) {
1993
+ let cursor = start;
1994
+ while (atoms[cursor]?.sourceGap === true) cursor += 1;
1995
+ return cursor;
1996
+ }
1997
+ function previousVisible(atoms, end) {
1998
+ for (let index = end - 1; index >= 0; index -= 1) if (atoms[index]?.sourceGap === false) return index;
1999
+ }
2000
+ /**
2001
+ * Whether a shorter line ending somewhere inside `(contentStart, end)` was ever an option — checked
2002
+ * as a plain short-circuiting scan rather than building the boundary list first, since this runs
2003
+ * once per over-long candidate the optimizer considers.
2004
+ */
2005
+ function hasEarlierBoundary(boundaryAllowed, contentStart, end) {
2006
+ for (let boundary = contentStart + 1; boundary < end; boundary += 1) if (boundaryAllowed(boundary - 1, boundary)) return true;
2007
+ return false;
2008
+ }
2009
+ /**
2010
+ * The line-end offsets a state's path took, earliest first. Rebuilt on demand by walking `previous`
2011
+ * rather than carried on every `State`: every candidate line attempted would otherwise pay for a
2012
+ * copy of its whole path, when `isBetter` only ever reads it on the exact-score tie this comparison
2013
+ * breaks — for `boundaryAllowed` predicates permissive enough to fully saturate the optimizer's
2014
+ * search window, ties are rare enough that this deferred cost is negligible.
2015
+ */
2016
+ function breaksOf(state, states) {
2017
+ const result = [];
2018
+ let cursor = state;
2019
+ while (cursor?.line !== null && cursor?.line !== void 0) {
2020
+ result.push(cursor.line.end);
2021
+ cursor = cursor.previous === null ? void 0 : states.get(cursor.previous.index)?.get(cursor.previous.key);
2022
+ }
2023
+ return result.reverse();
2024
+ }
2025
+ function compareBreaks(left, right) {
2026
+ const length = Math.min(left.length, right.length);
2027
+ for (let index = 0; index < length; index += 1) {
2028
+ const difference = (right[index] ?? 0) - (left[index] ?? 0);
2029
+ if (difference !== 0) return difference;
2030
+ }
2031
+ return left.length - right.length;
2032
+ }
2033
+ function isBetter(candidateState, current, states) {
2034
+ if (current === void 0) return true;
2035
+ for (let index = 0; index < candidateState.score.length; index += 1) {
2036
+ const difference = (candidateState.score[index] ?? 0) - (current.score[index] ?? 0);
2037
+ if (Math.abs(difference) > EPSILON$2) return difference < 0;
2038
+ }
2039
+ return compareBreaks(breaksOf(candidateState, states), breaksOf(current, states)) < 0;
2040
+ }
2041
+ function layoutParagraph(atoms, lineLengthEm, resolveCandidate, boundaryAllowed, evaluation = defaultParagraphEvaluation) {
2042
+ if (atoms.length === 0) return [];
2043
+ const stateKey = (cursor, fitness) => `${cursor}:${fitness}`;
2044
+ const states = /* @__PURE__ */ new Map([[0, /* @__PURE__ */ new Map([[stateKey(0, 0), {
2045
+ score: [
2046
+ 0,
2047
+ 0,
2048
+ 0,
2049
+ 0,
2050
+ 0,
2051
+ 0,
2052
+ 0
2053
+ ],
2054
+ fitness: 0,
2055
+ cursor: 0,
2056
+ previous: null,
2057
+ line: null
2058
+ }]])]]);
2059
+ for (let start = 0; start < atoms.length; start += 1) {
2060
+ const activeStates = states.get(start);
2061
+ if (activeStates === void 0) continue;
2062
+ const contentStart = skipSourceGaps(atoms, start);
2063
+ if (contentStart === atoms.length) {
2064
+ const terminalStates = states.get(atoms.length) ?? /* @__PURE__ */ new Map();
2065
+ for (const state of activeStates.values()) {
2066
+ const key = stateKey(state.cursor, state.fitness);
2067
+ if (isBetter(state, terminalStates.get(key), states)) terminalStates.set(key, state);
2068
+ }
2069
+ states.set(atoms.length, terminalStates);
2070
+ continue;
2071
+ }
2072
+ const candidatesByEnd = /* @__PURE__ */ new Map();
2073
+ for (const state of activeStates.values()) {
2074
+ let minimumSizeEm = 0;
2075
+ for (let end = contentStart + 1; end <= atoms.length; end += 1) {
2076
+ minimumSizeEm += atoms[end - 1]?.boxAdvanceEm ?? 0;
2077
+ const right = skipSourceGaps(atoms, end);
2078
+ const left = previousVisible(atoms, end);
2079
+ if (atoms[end - 1]?.sourceGap === true) continue;
2080
+ if (end < atoms.length && right < atoms.length && (left === void 0 || !boundaryAllowed(left, right))) continue;
2081
+ const candidateKey = `${end}:${state.cursor}`;
2082
+ let line = candidatesByEnd.get(candidateKey);
2083
+ if (!candidatesByEnd.has(candidateKey)) {
2084
+ line = resolveCandidate(start, end, state.cursor);
2085
+ candidatesByEnd.set(candidateKey, line);
2086
+ }
2087
+ if (line === void 0) {
2088
+ if (minimumSizeEm > lineLengthEm * 2 && end > contentStart + 1) break;
2089
+ continue;
2090
+ }
2091
+ if (line.break.kind === "forced" && line.inlineSizeEm > lineLengthEm + EPSILON$2 && hasEarlierBoundary(boundaryAllowed, contentStart, end)) {
2092
+ if (line.inlineSizeEm > lineLengthEm * 2 && end > contentStart + 1) break;
2093
+ continue;
2094
+ }
2095
+ const lineScore = evaluation.scoreFor(line, state.fitness);
2096
+ const nextState = {
2097
+ score: addScore(state.score, lineScore.score),
2098
+ fitness: lineScore.fitness,
2099
+ cursor: line.nextCursor ?? 0,
2100
+ previous: {
2101
+ index: start,
2102
+ key: stateKey(state.cursor, state.fitness)
2103
+ },
2104
+ line
2105
+ };
2106
+ const statesAtEnd = states.get(end) ?? /* @__PURE__ */ new Map();
2107
+ const nextKey = stateKey(nextState.cursor, nextState.fitness);
2108
+ if (isBetter(nextState, statesAtEnd.get(nextKey), states)) {
2109
+ statesAtEnd.set(nextKey, nextState);
2110
+ states.set(end, statesAtEnd);
2111
+ }
2112
+ if (line.inlineSizeEm > lineLengthEm * 2 && line.break.kind === "forced" && end > contentStart + 1) break;
2113
+ }
2114
+ }
2115
+ }
2116
+ const terminal = [...states.get(atoms.length)?.values() ?? []].reduce((best, state) => isBetter(state, best, states) ? state : best, void 0);
2117
+ if (terminal === void 0) return [];
2118
+ const lines = [];
2119
+ let cursor = terminal;
2120
+ while (cursor?.line !== null && cursor?.line !== void 0) {
2121
+ lines.push(cursor.line);
2122
+ cursor = cursor.previous === null ? void 0 : states.get(cursor.previous.index)?.get(cursor.previous.key);
2123
+ }
2124
+ lines.reverse();
2125
+ const last = lines.at(-1);
2126
+ if (last !== void 0 && last.end < atoms.length) lines[lines.length - 1] = {
2127
+ ...last,
2128
+ suppressedIndexes: [...last.suppressedIndexes, ...Array.from({ length: atoms.length - last.end }, (_, index) => last.end + index)]
2129
+ };
2130
+ return lines;
2131
+ }
2132
+ //#endregion
2133
+ //#region src/composer/internal/ruby-association.ts
2134
+ const RubyAssociation = {
2135
+ collect: (annotations, graphemes) => annotations.flatMap((annotation) => {
2136
+ if (annotation.kind !== "ruby") return [];
2137
+ const first = graphemes[0];
2138
+ if (first === void 0) return [];
2139
+ const start = Math.max(0, annotation.range.graphemes.start - first.range.graphemes.start);
2140
+ const end = Math.min(graphemes.length, annotation.range.graphemes.end - first.range.graphemes.start);
2141
+ const indexes = Array.from({ length: Math.max(0, end - start) }, (_, index) => start + index);
2142
+ return indexes.length === 0 ? [] : [{
2143
+ baseRange: annotation.range,
2144
+ reading: annotation.reading,
2145
+ indexes
2146
+ }];
2147
+ }),
2148
+ fittableInteriors: (associations, advancesEm, lineLengthEm, fittableGroups) => {
2149
+ const interiors = /* @__PURE__ */ new Set();
2150
+ for (const association of associations) {
2151
+ if (association.reading.kind !== "group") continue;
2152
+ if (fittableGroups !== void 0 && !fittableGroups.has(association)) continue;
2153
+ const first = association.indexes[0];
2154
+ const last = association.indexes.at(-1);
2155
+ if (first === void 0 || last === void 0) continue;
2156
+ if (association.indexes.reduce((total, index) => total + (advancesEm[index] ?? 0), 0) > lineLengthEm) continue;
2157
+ for (let boundary = first + 1; boundary <= last; boundary += 1) interiors.add(boundary);
2158
+ }
2159
+ return interiors;
2160
+ }
2161
+ };
2162
+ //#endregion
2163
+ //#region src/composer/internal/jukugo-ruby.ts
2164
+ const EPSILON$1 = 1e-9;
2165
+ function boundsFor(readings, edges, spacings, outer) {
2166
+ let shiftEm = 0;
2167
+ return readings.map((reading, index) => {
2168
+ const spacing = spacings[index] ?? {
2169
+ beforeEm: 0,
2170
+ afterEm: 0
2171
+ };
2172
+ shiftEm += (spacings[index - 1]?.afterEm ?? 0) + spacing.beforeEm;
2173
+ const before = index === 0 && !outer ? 0 : reading.beforeEm;
2174
+ const after = index === readings.length - 1 && !outer ? 0 : reading.afterEm;
2175
+ const lower = [{
2176
+ positionEm: reading.baseOffsetEm - before - reading.beforeSpacingEm,
2177
+ perSpacingEm: shiftEm - spacing.beforeEm
2178
+ }];
2179
+ const upper = [{
2180
+ positionEm: reading.baseOffsetEm + reading.baseAdvanceEm + after + reading.afterSpacingEm - reading.readingAdvanceEm,
2181
+ perSpacingEm: shiftEm + spacing.afterEm
2182
+ }];
2183
+ if (index === 0 && edges.head) {
2184
+ const head = {
2185
+ positionEm: reading.baseOffsetEm,
2186
+ perSpacingEm: shiftEm
2187
+ };
2188
+ lower.push(head);
2189
+ upper.push(head);
2190
+ } else if (index === readings.length - 1 && edges.tail) {
2191
+ const tail = {
2192
+ positionEm: reading.baseOffsetEm + reading.baseAdvanceEm - reading.readingAdvanceEm,
2193
+ perSpacingEm: shiftEm
2194
+ };
2195
+ lower.push(tail);
2196
+ upper.push(tail);
2197
+ }
2198
+ return {
2199
+ lower,
2200
+ upper
2201
+ };
2202
+ });
2203
+ }
2204
+ function place$1(readings, bounds) {
2205
+ const latest = [];
2206
+ let following = Number.POSITIVE_INFINITY;
2207
+ for (let index = readings.length - 1; index >= 0; index -= 1) {
2208
+ const reading = readings[index];
2209
+ const bound = bounds[index];
2210
+ if (reading === void 0 || bound === void 0) return void 0;
2211
+ const upper = Math.min(...bound.upper.map(({ positionEm }) => positionEm));
2212
+ const position = Math.min(upper, following - reading.readingAdvanceEm);
2213
+ if (position < Math.max(...bound.lower.map(({ positionEm }) => positionEm)) - EPSILON$1) return void 0;
2214
+ latest[index] = position;
2215
+ following = position;
2216
+ }
2217
+ const offsetsEm = [];
2218
+ let precedingEnd = Number.NEGATIVE_INFINITY;
2219
+ for (const [index, reading] of readings.entries()) {
2220
+ const bound = bounds[index];
2221
+ if (bound === void 0) return void 0;
2222
+ const position = Math.max(precedingEnd, ...bound.lower.map(({ positionEm }) => positionEm), Math.min(reading.baseOffsetEm, latest[index] ?? reading.baseOffsetEm));
2223
+ offsetsEm.push(position);
2224
+ precedingEnd = position + reading.readingAdvanceEm;
2225
+ }
2226
+ return offsetsEm;
2227
+ }
2228
+ const JukugoRuby = { resolve: (readings, edges) => {
2229
+ const empty = readings.map(() => ({
2230
+ beforeEm: 0,
2231
+ afterEm: 0
2232
+ }));
2233
+ for (const outer of [false, true]) {
2234
+ const offsetsEm = place$1(readings, boundsFor(readings, edges, empty, outer));
2235
+ if (offsetsEm !== void 0) return {
2236
+ kind: "placed",
2237
+ offsetsEm
2238
+ };
2239
+ }
2240
+ const weights = readings.map((reading) => reading.readingAdvanceEm > reading.intrinsicBaseAdvanceEm + EPSILON$1 ? reading.readingAdvanceEm : 0);
2241
+ const total = weights.reduce((sum, weight) => sum + weight, 0);
2242
+ if (total <= EPSILON$1) return { kind: "refused" };
2243
+ const shares = weights.map((weight, index) => {
2244
+ const share = weight / total;
2245
+ if (index === 0 && edges.head) return {
2246
+ beforeEm: 0,
2247
+ afterEm: share
2248
+ };
2249
+ if (index === readings.length - 1 && edges.tail) return {
2250
+ beforeEm: share,
2251
+ afterEm: 0
2252
+ };
2253
+ return {
2254
+ beforeEm: share / 2,
2255
+ afterEm: share / 2
2256
+ };
2257
+ });
2258
+ const bounds = boundsFor(readings, edges, shares, true);
2259
+ let amountEm = 0;
2260
+ for (let start = 0; start < readings.length; start += 1) {
2261
+ let advanceEm = 0;
2262
+ for (let end = start; end < readings.length; end += 1) {
2263
+ const reading = readings[end];
2264
+ if (reading === void 0) return { kind: "refused" };
2265
+ advanceEm += reading.readingAdvanceEm;
2266
+ for (const lower of bounds[start]?.lower ?? []) for (const upper of bounds[end]?.upper ?? []) {
2267
+ const missing = advanceEm - reading.readingAdvanceEm + lower.positionEm - upper.positionEm;
2268
+ if (missing <= EPSILON$1) continue;
2269
+ const capacity = upper.perSpacingEm - lower.perSpacingEm;
2270
+ if (capacity <= EPSILON$1) return { kind: "refused" };
2271
+ amountEm = Math.max(amountEm, missing / capacity);
2272
+ }
2273
+ }
2274
+ }
2275
+ if (amountEm <= EPSILON$1) return { kind: "refused" };
2276
+ return {
2277
+ kind: "needs-spacing",
2278
+ spacings: shares.map((share) => ({
2279
+ beforeEm: share.beforeEm * amountEm,
2280
+ afterEm: share.afterEm * amountEm
2281
+ }))
2282
+ };
2283
+ } };
2284
+ //#endregion
2285
+ //#region src/composer/internal/ruby-boundary.ts
2286
+ const RubyBoundary = { overhang: (neighbor, side, spacingEm, rubySizeEm) => {
2287
+ if (neighbor === void 0 || neighbor.effectiveClass === "cl-22" || neighbor.effectiveClass === "cl-23") return 0;
2288
+ const { lexicalClass, advanceEm } = neighbor;
2289
+ if ([
2290
+ "cl-08",
2291
+ "cl-10",
2292
+ "cl-11",
2293
+ "cl-14",
2294
+ "cl-15",
2295
+ "cl-16"
2296
+ ].some((value) => value === lexicalClass)) return Math.min(rubySizeEm, advanceEm);
2297
+ if ([
2298
+ "cl-05",
2299
+ "cl-24",
2300
+ "cl-25",
2301
+ "cl-27"
2302
+ ].some((value) => value === lexicalClass)) return Math.min(rubySizeEm / 2, Math.max(0, spacingEm));
2303
+ const closing = lexicalClass === "cl-02" || lexicalClass === "cl-06" || lexicalClass === "cl-07";
2304
+ if (side === "before" && closing || side === "after" && lexicalClass === "cl-01") return Math.min(rubySizeEm, Math.max(0, spacingEm));
2305
+ if (side === "after" && closing || side === "before" && lexicalClass === "cl-01") return Math.min(rubySizeEm, advanceEm);
2306
+ return 0;
2307
+ } };
2308
+ //#endregion
2309
+ //#region src/composer/internal/ruby-line-layout.ts
2310
+ const EPSILON = 1e-9;
2311
+ const { sizeEm: rubySizeEm } = defaultBookStyle.annotation;
2312
+ function positions(bases, line) {
2313
+ const boxes = new Map(line.boxAdjustments?.map((box) => [box.index, box]));
2314
+ const gaps = new Map(line.pairSpacings.map((gap) => [gap.boundary, gap.widthEm]));
2315
+ const spaces = new Map(line.characterSpacings.map((space) => [space.index, space.widthEm]));
2316
+ const offsets = /* @__PURE__ */ new Map();
2317
+ let offsetEm = 0;
2318
+ for (let index = line.contentStart; index < line.end; index += 1) {
2319
+ offsetEm += gaps.get(index) ?? 0;
2320
+ const advanceEm = index === line.hangingIndex ? 0 : spaces.get(index) ?? boxes.get(index)?.advanceEm ?? bases[index]?.advanceEm ?? 0;
2321
+ offsets.set(index, {
2322
+ offsetEm,
2323
+ advanceEm
2324
+ });
2325
+ offsetEm += advanceEm;
2326
+ }
2327
+ return {
2328
+ offsets,
2329
+ gaps
2330
+ };
2331
+ }
2332
+ function allowances(bases, segment, line, gaps, ownSpacing = {
2333
+ beforeEm: 0,
2334
+ afterEm: 0
2335
+ }) {
2336
+ const first = segment.association.indexes[0] ?? segment.start;
2337
+ const last = segment.association.indexes.at(-1) ?? segment.end - 1;
2338
+ const jukugo = segment.association.reading.kind === "jukugo";
2339
+ return {
2340
+ before: jukugo && segment.start > first && segment.start > line.contentStart ? rubySizeEm : RubyBoundary.overhang(segment.start > line.contentStart ? bases[segment.start - 1] : void 0, "before", Math.max(0, (gaps.get(segment.start) ?? 0) - ownSpacing.beforeEm), rubySizeEm),
2341
+ after: jukugo && segment.end <= last && segment.end < line.end ? rubySizeEm : RubyBoundary.overhang(segment.end < line.end ? bases[segment.end] : void 0, "after", Math.max(0, (gaps.get(segment.end) ?? 0) - ownSpacing.afterEm), rubySizeEm)
2342
+ };
2343
+ }
2344
+ function readingAdvance(segment) {
2345
+ return segment.pieces.reduce((sum, piece) => sum + piece.layoutSpan.advanceEm, 0);
2346
+ }
2347
+ function place(segment, baseOffsetEm, baseAdvanceEm, before, after, resolvedOffsetEm) {
2348
+ const total = readingAdvance(segment);
2349
+ const overhang = total > baseAdvanceEm ? Math.min(total - baseAdvanceEm, before + after) : 0;
2350
+ const leading = Math.min(before, overhang / 2 + Math.max(0, overhang / 2 - after));
2351
+ const edgeGap = segment.association.reading.kind !== "jukugo" && total < baseAdvanceEm && segment.pieces.length > 0 ? (baseAdvanceEm - total) / (2 * segment.pieces.length) : 0;
2352
+ let offsetEm = resolvedOffsetEm ?? baseOffsetEm - leading + edgeGap;
2353
+ return {
2354
+ segment,
2355
+ items: segment.pieces.map((piece, index) => {
2356
+ const item = {
2357
+ value: piece.value,
2358
+ textRange: piece.textRange,
2359
+ placement: {
2360
+ side: "before",
2361
+ inlineSpan: {
2362
+ offsetEm: offsetEm + piece.renderSpan.offsetEm - piece.layoutSpan.offsetEm,
2363
+ advanceEm: piece.renderSpan.advanceEm
2364
+ },
2365
+ blockOffsetEm: -rubySizeEm,
2366
+ blockSizeEm: rubySizeEm
2367
+ }
2368
+ };
2369
+ offsetEm += piece.layoutSpan.advanceEm;
2370
+ if (index < segment.pieces.length - 1) offsetEm += 2 * edgeGap;
2371
+ return item;
2372
+ })
2373
+ };
2374
+ }
2375
+ const RubyLineLayout = {
2376
+ index: (segments) => {
2377
+ const byStart = /* @__PURE__ */ new Map();
2378
+ for (const segment of segments) {
2379
+ const sameStart = byStart.get(segment.start);
2380
+ if (sameStart === void 0) byStart.set(segment.start, [segment]);
2381
+ else sameStart.push(segment);
2382
+ }
2383
+ return byStart;
2384
+ },
2385
+ resolve: (bases, segments, start, end, resolve, assignedSegments = []) => {
2386
+ const selected = [];
2387
+ for (let index = start; index < end; index += 1) for (const segment of segments.get(index) ?? []) if (segment.end <= end) selected.push(segment);
2388
+ selected.push(...assignedSegments);
2389
+ selected.sort((left, right) => left.start - right.start);
2390
+ const jukugoCounts = /* @__PURE__ */ new Map();
2391
+ for (const segment of selected) if (segment.association.reading.kind === "jukugo") jukugoCounts.set(segment.association, (jukugoCounts.get(segment.association) ?? 0) + 1);
2392
+ const expansions = /* @__PURE__ */ new Map();
2393
+ const extraGaps = /* @__PURE__ */ new Map();
2394
+ const jukugoSpacings = /* @__PURE__ */ new Map();
2395
+ const metrics = () => {
2396
+ const boxAdjustments = [];
2397
+ for (const [index, extraEm] of expansions) {
2398
+ const base = bases[index];
2399
+ if (base !== void 0) boxAdjustments.push({
2400
+ index,
2401
+ advanceEm: base.advanceEm + extraEm,
2402
+ renderOffsetEm: base.renderOffsetEm + extraEm / 2
2403
+ });
2404
+ }
2405
+ return {
2406
+ boxAdjustments,
2407
+ extraSpacings: [...extraGaps].map(([boundary, widthEm]) => ({
2408
+ boundary,
2409
+ widthEm
2410
+ }))
2411
+ };
2412
+ };
2413
+ let line = resolve();
2414
+ for (let pass = 0; pass <= selected.length * 2 + 2; pass += 1) {
2415
+ const { offsets, gaps } = positions(bases, line);
2416
+ const placements = [];
2417
+ let changed = false;
2418
+ for (const segment of selected) {
2419
+ const first = offsets.get(segment.start);
2420
+ const last = offsets.get(segment.end - 1);
2421
+ if (first === void 0 || last === void 0) return void 0;
2422
+ const span = last.offsetEm + last.advanceEm - first.offsetEm;
2423
+ const { before, after } = allowances(bases, segment, line, gaps);
2424
+ const missing = readingAdvance(segment) - span - before - after;
2425
+ const sharedJukugo = (jukugoCounts.get(segment.association) ?? 0) > 1;
2426
+ if (missing > EPSILON && !sharedJukugo) {
2427
+ const indexes = Array.from({ length: segment.end - segment.start }, (_, index) => segment.start + index).filter((index) => bases[index]?.sourceGap === false && bases[index].advanceEm > 0);
2428
+ if (indexes.length === 0) return void 0;
2429
+ for (const index of indexes) expansions.set(index, (expansions.get(index) ?? 0) + missing / indexes.length);
2430
+ changed = true;
2431
+ }
2432
+ placements.push(place(segment, first.offsetEm, span, before, after));
2433
+ }
2434
+ if (changed) {
2435
+ line = resolve(metrics());
2436
+ continue;
2437
+ }
2438
+ for (let index = 0; index < selected.length; index += 1) {
2439
+ const first = selected[index];
2440
+ if (first?.association.reading.kind !== "jukugo") continue;
2441
+ const group = [first];
2442
+ while (selected[index + group.length]?.association === first.association) {
2443
+ const next = selected[index + group.length];
2444
+ if (next === void 0) break;
2445
+ if (next.start < (group.at(-1)?.end ?? next.start)) return void 0;
2446
+ group.push(next);
2447
+ }
2448
+ const readings = group.flatMap((segment) => {
2449
+ const baseStart = offsets.get(segment.start);
2450
+ const baseEnd = offsets.get(segment.end - 1);
2451
+ if (baseStart === void 0 || baseEnd === void 0) return [];
2452
+ const baseAdvanceEm = baseEnd.offsetEm + baseEnd.advanceEm - baseStart.offsetEm;
2453
+ const total = readingAdvance(segment);
2454
+ const spacing = jukugoSpacings.get(segment);
2455
+ const limits = allowances(bases, segment, line, gaps, spacing);
2456
+ return [{
2457
+ baseOffsetEm: baseStart.offsetEm,
2458
+ baseAdvanceEm,
2459
+ intrinsicBaseAdvanceEm: bases.slice(segment.start, segment.end).reduce((sum, base) => sum + base.advanceEm, 0),
2460
+ readingAdvanceEm: total,
2461
+ beforeEm: limits.before,
2462
+ afterEm: limits.after,
2463
+ beforeSpacingEm: spacing?.beforeEm ?? 0,
2464
+ afterSpacingEm: spacing?.afterEm ?? 0
2465
+ }];
2466
+ });
2467
+ const result = JukugoRuby.resolve(readings, {
2468
+ head: first.start === line.contentStart,
2469
+ tail: group.at(-1)?.end === line.end
2470
+ });
2471
+ if (result.kind === "refused") return void 0;
2472
+ if (result.kind === "needs-spacing") {
2473
+ for (const [groupIndex, spacing] of result.spacings.entries()) {
2474
+ const segment = group[groupIndex];
2475
+ if (segment === void 0) return void 0;
2476
+ const existing = jukugoSpacings.get(segment);
2477
+ jukugoSpacings.set(segment, {
2478
+ beforeEm: (existing?.beforeEm ?? 0) + spacing.beforeEm,
2479
+ afterEm: (existing?.afterEm ?? 0) + spacing.afterEm
2480
+ });
2481
+ for (const [boundary, widthEm] of [[segment.start, spacing.beforeEm], [segment.end, spacing.afterEm]]) if (widthEm > EPSILON) extraGaps.set(boundary, (extraGaps.get(boundary) ?? 0) + widthEm);
2482
+ }
2483
+ changed = true;
2484
+ } else for (const [groupIndex, segment] of group.entries()) {
2485
+ const reading = readings[groupIndex];
2486
+ const offsetEm = result.offsetsEm[groupIndex];
2487
+ if (reading === void 0 || offsetEm === void 0) return void 0;
2488
+ placements[index + groupIndex] = place(segment, reading.baseOffsetEm, reading.baseAdvanceEm, reading.beforeEm, reading.afterEm, offsetEm);
2489
+ }
2490
+ index += group.length - 1;
2491
+ }
2492
+ if (changed) {
2493
+ line = resolve(metrics());
2494
+ continue;
2495
+ }
2496
+ for (let index = 1; index < placements.length; index += 1) {
2497
+ const left = placements[index - 1];
2498
+ const right = placements[index];
2499
+ const leftItem = left?.items.at(-1);
2500
+ const rightItem = right?.items[0];
2501
+ if (left === void 0 || right === void 0 || leftItem === void 0 || rightItem === void 0) continue;
2502
+ const clearance = right.segment.start > left.segment.end ? rubySizeEm : 0;
2503
+ const missing = leftItem.placement.inlineSpan.offsetEm + leftItem.placement.inlineSpan.advanceEm + clearance - rightItem.placement.inlineSpan.offsetEm;
2504
+ if (missing <= EPSILON) continue;
2505
+ if (right.segment.start < left.segment.end) return void 0;
2506
+ extraGaps.set(right.segment.start, (extraGaps.get(right.segment.start) ?? 0) + missing);
2507
+ changed = true;
2508
+ }
2509
+ if (!changed) return {
2510
+ line: {
2511
+ ...line,
2512
+ boxAdjustments: metrics().boxAdjustments
2513
+ },
2514
+ placements
2515
+ };
2516
+ line = resolve(metrics());
2517
+ }
2518
+ }
2519
+ };
2520
+ //#endregion
2521
+ //#region src/composer/internal/source-space.ts
2522
+ const SourceSpace = { collect: (graphemes, advancesEm) => {
2523
+ const gaps = new Set(questionOrExclamationSpacings(graphemes.map(({ value }) => value).join("")).filter((spacing) => spacing.kind === "valid").map(({ gap }) => gap.start));
2524
+ let offset = 0;
2525
+ return graphemes.flatMap((grapheme, index) => {
2526
+ const separator = gaps.has(offset);
2527
+ offset += grapheme.value.length;
2528
+ if (separator) return [{
2529
+ grapheme,
2530
+ index,
2531
+ purpose: "question-or-exclamation-separator",
2532
+ naturalWidthEm: advancesEm[index] ?? 0,
2533
+ edgeBehavior: "suppress"
2534
+ }];
2535
+ if (grapheme.value === " ") return [{
2536
+ grapheme,
2537
+ index,
2538
+ purpose: "ideographic",
2539
+ naturalWidthEm: advancesEm[index] ?? 0,
2540
+ edgeBehavior: "preserve"
2541
+ }];
2542
+ if (grapheme.value === " ") return [{
2543
+ grapheme,
2544
+ index,
2545
+ purpose: "western-word",
2546
+ naturalWidthEm: 1 / 3,
2547
+ edgeBehavior: "zero"
2548
+ }];
2549
+ return [];
2550
+ });
2551
+ } };
2552
+ //#endregion
2553
+ //#region src/appearance/paper-size.ts
2554
+ const PAPER_SIZES = {
2555
+ a4: {
2556
+ id: "a4",
2557
+ label: "A4",
2558
+ widthMm: 210,
2559
+ heightMm: 297
2560
+ },
2561
+ a5: {
2562
+ id: "a5",
2563
+ label: "A5",
2564
+ widthMm: 148,
2565
+ heightMm: 210
2566
+ },
2567
+ a6: {
2568
+ id: "a6",
2569
+ label: "A6(文庫)",
2570
+ widthMm: 105,
2571
+ heightMm: 148
2572
+ },
2573
+ "jis-b5": {
2574
+ id: "jis-b5",
2575
+ label: "B5(JIS)",
2576
+ widthMm: 182,
2577
+ heightMm: 257
2578
+ },
2579
+ "jis-b6": {
2580
+ id: "jis-b6",
2581
+ label: "B6(JIS)",
2582
+ widthMm: 128,
2583
+ heightMm: 182
2584
+ },
2585
+ shinsho: {
2586
+ id: "shinsho",
2587
+ label: "新書",
2588
+ widthMm: 106,
2589
+ heightMm: 173
2590
+ }
2591
+ };
2592
+ const PaperSize = {
2593
+ of: (id) => PAPER_SIZES[id],
2594
+ all: Object.values(PAPER_SIZES)
2595
+ };
2596
+ //#endregion
2597
+ //#region src/internal/length.ts
2598
+ const PT_PER_MM = 72 / 25.4;
2599
+ function ptToMm(pt) {
2600
+ return pt / PT_PER_MM;
2601
+ }
2602
+ function mmToPt(mm) {
2603
+ return mm * PT_PER_MM;
2604
+ }
2605
+ //#endregion
2606
+ //#region src/composer/manuscript-geometry.ts
2607
+ const STAGE_GAP_CELLS = 2;
2608
+ const LINE_GAP_CELLS = .5;
2609
+ const nonNegativeLength = () => v.pipe(v.number(), v.finite(), v.minValue(0));
2610
+ const ManuscriptGeometrySchema = readonlyObject({
2611
+ paperWidthMm: nonNegativeLength(),
2612
+ paperHeightMm: nonNegativeLength(),
2613
+ cellSizeMm: nonNegativeLength(),
2614
+ lineGapMm: nonNegativeLength(),
2615
+ fontSizePt: FontSizePt.schema,
2616
+ contentWidthMm: nonNegativeLength(),
2617
+ contentHeightMm: nonNegativeLength(),
2618
+ marginInlineMm: v.pipe(v.number(), v.finite()),
2619
+ marginBlockMm: v.pipe(v.number(), v.finite()),
2620
+ fitsPaper: v.boolean()
2621
+ });
2622
+ /**
2623
+ * Every stage stacked vertically, plus the gap between adjacent stages.
2624
+ */
2625
+ function contentHeightInEm(flow) {
2626
+ return flow.stagesPerPage * flow.lineLengthEm + STAGE_GAP_CELLS * (flow.stagesPerPage - 1);
2627
+ }
2628
+ /**
2629
+ * Every vertical line side by side, plus the gap between adjacent lines.
2630
+ */
2631
+ function contentWidthInEm(flow) {
2632
+ return flow.linesPerStage + LINE_GAP_CELLS * (flow.linesPerStage - 1);
2633
+ }
2634
+ const ManuscriptGeometry = {
2635
+ schema: ManuscriptGeometrySchema,
2636
+ of: (flow, appearance) => {
2637
+ const paper = PaperSize.of(appearance.paperSize);
2638
+ const cellSizeMm = ptToMm(appearance.fontSizePt);
2639
+ const contentWidthMm = contentWidthInEm(flow) * cellSizeMm;
2640
+ const contentHeightMm = contentHeightInEm(flow) * cellSizeMm;
2641
+ const marginInlineMm = (paper.widthMm - contentWidthMm) / 2;
2642
+ const marginBlockMm = (paper.heightMm - contentHeightMm) / 2;
2643
+ return {
2644
+ paperWidthMm: paper.widthMm,
2645
+ paperHeightMm: paper.heightMm,
2646
+ cellSizeMm,
2647
+ lineGapMm: LINE_GAP_CELLS * cellSizeMm,
2648
+ fontSizePt: appearance.fontSizePt,
2649
+ contentWidthMm,
2650
+ contentHeightMm,
2651
+ marginInlineMm,
2652
+ marginBlockMm,
2653
+ fitsPaper: marginInlineMm >= 0 && marginBlockMm >= 0
2654
+ };
2655
+ },
2656
+ /**
2657
+ * The largest step-aligned point size whose text area still fits the paper.
2658
+ */
2659
+ maxFontSizePt: (flow, paperSizeId) => {
2660
+ const paper = PaperSize.of(paperSizeId);
2661
+ const maxCellSizeMm = Math.min(paper.widthMm / contentWidthInEm(flow), paper.heightMm / contentHeightInEm(flow));
2662
+ const steps = Math.floor(mmToPt(maxCellSizeMm) / FontSizePt.range.step + 1e-9);
2663
+ const aligned = Math.round(steps * FontSizePt.range.step * 1e3) / 1e3;
2664
+ return Math.min(FontSizePt.range.max, Math.max(FontSizePt.range.min, aligned));
2665
+ }
2666
+ };
2667
+ const CompositionStatistics = { schema: readonlyObject({
2668
+ chars: nonNegativeInteger(),
2669
+ sourceLines: nonNegativeInteger(),
2670
+ pages: nonNegativeInteger()
2671
+ }) };
2672
+ const AnnotationPlacement = { schema: readonlyObject({
2673
+ side: v.picklist(["before", "after"]),
2674
+ inlineSpan: InlineSpan.schema,
2675
+ blockOffsetEm: v.pipe(v.number(), v.finite()),
2676
+ blockSizeEm: v.pipe(v.number(), v.finite(), v.minValue(0))
2677
+ }) };
2678
+ //#endregion
2679
+ //#region src/composer/composed-annotation-fragment.ts
2680
+ const common = {
2681
+ annotationRange: ManuscriptRange.schema,
2682
+ fragmentRange: ManuscriptRange.schema,
2683
+ continuation: v.picklist([
2684
+ "whole",
2685
+ "start",
2686
+ "middle",
2687
+ "end"
2688
+ ])
2689
+ };
2690
+ const readingItem = readonlyObject({
2691
+ value: v.pipe(v.string(), v.nonEmpty()),
2692
+ textRange: MeasurementTextRange.schema,
2693
+ placement: AnnotationPlacement.schema
2694
+ });
2695
+ const ComposedAnnotationFragment = { schema: v.pipe(v.variant("kind", [
2696
+ readonlyObject({
2697
+ kind: v.literal("bold"),
2698
+ ...common
2699
+ }),
2700
+ readonlyObject({
2701
+ kind: v.literal("italic"),
2702
+ ...common
2703
+ }),
2704
+ readonlyObject({
2705
+ kind: v.literal("emphasis"),
2706
+ mark: v.string(),
2707
+ placements: readonlyArray(AnnotationPlacement.schema),
2708
+ ...common
2709
+ }),
2710
+ readonlyObject({
2711
+ kind: v.literal("ruby"),
2712
+ rubyKind: RubyKind.schema,
2713
+ reading: v.string(),
2714
+ readingItems: readonlyArray(readingItem),
2715
+ ...common
2716
+ })
2717
+ ]), v.check((fragment) => {
2718
+ const { annotationRange, fragmentRange } = fragment;
2719
+ if (fragmentRange.graphemes.start < annotationRange.graphemes.start || fragmentRange.graphemes.end > annotationRange.graphemes.end || fragmentRange.source.start < annotationRange.source.start || fragmentRange.source.end > annotationRange.source.end || fragmentRange.display.start < annotationRange.display.start || fragmentRange.display.end > annotationRange.display.end) return false;
2720
+ const starts = fragmentRange.graphemes.start === annotationRange.graphemes.start;
2721
+ const ends = fragmentRange.graphemes.end === annotationRange.graphemes.end;
2722
+ const continuation = starts && ends ? "whole" : starts ? "start" : ends ? "end" : "middle";
2723
+ if (fragment.continuation !== continuation) return false;
2724
+ if (fragment.kind !== "ruby") return true;
2725
+ const boundaries = new Set([...graphemeSegmenter.segment(fragment.reading)].map(({ index }) => index));
2726
+ boundaries.add(fragment.reading.length);
2727
+ let end = 0;
2728
+ for (const item of fragment.readingItems) {
2729
+ if (item.textRange.start !== end || item.textRange.end <= end || !boundaries.has(item.textRange.end) || fragment.reading.slice(end, item.textRange.end) !== item.value) return false;
2730
+ end = item.textRange.end;
2731
+ }
2732
+ return end === fragment.reading.length;
2733
+ })) };
2734
+ const LineBreakResult = { schema: v.variant("kind", [
2735
+ readonlyObject({ kind: v.literal("natural") }),
2736
+ readonlyObject({ kind: v.literal("shrunk") }),
2737
+ readonlyObject({ kind: v.literal("stretched") }),
2738
+ readonlyObject({ kind: v.literal("hanging") }),
2739
+ readonlyObject({ kind: v.literal("forced") }),
2740
+ readonlyObject({ kind: v.literal("paragraph-end") })
2741
+ ]) };
2742
+ const NovelLine = {
2743
+ schema: v.pipe(readonlyObject({
2744
+ range: v.nullable(ManuscriptRange.schema),
2745
+ inlineSizeEm: v.pipe(v.number(), v.finite(), v.minValue(0)),
2746
+ items: readonlyArray(PositionedInlineItem.schema),
2747
+ break: LineBreakResult.schema,
2748
+ annotations: readonlyArray(ComposedAnnotationFragment.schema)
2749
+ }), v.check((line) => {
2750
+ const ranges = line.items.flatMap((item) => PositionedInlineItem.isRenderUnit(item) ? item.sources.flatMap(SourcePlacement.ranges) : item.kind === "suppressed" || item.kind === "glue" && item.origin === "source" ? [item.range] : []);
2751
+ if (line.range === null) return ranges.length === 0 && line.items.length === 0 && line.annotations.length === 0 && line.inlineSizeEm === 0;
2752
+ ranges.sort((left, right) => left.graphemes.start - right.graphemes.start);
2753
+ const first = ranges[0];
2754
+ const last = ranges.at(-1);
2755
+ if (first === void 0 || last === void 0) return false;
2756
+ let end = line.range.graphemes.start;
2757
+ for (const range of ranges) {
2758
+ if (range.graphemes.start !== end) return false;
2759
+ end = range.graphemes.end;
2760
+ }
2761
+ return end === line.range.graphemes.end && first.source.start === line.range.source.start && last.source.end === line.range.source.end && first.display.start === line.range.display.start && last.display.end === line.range.display.end && line.annotations.every(({ fragmentRange }) => line.range !== null && fragmentRange.graphemes.start >= line.range.graphemes.start && fragmentRange.graphemes.end <= line.range.graphemes.end && fragmentRange.source.start >= line.range.source.start && fragmentRange.source.end <= line.range.source.end && fragmentRange.display.start >= line.range.display.start && fragmentRange.display.end <= line.range.display.end);
2762
+ })),
2763
+ empty: () => ({
2764
+ range: null,
2765
+ inlineSizeEm: 0,
2766
+ items: [],
2767
+ break: { kind: "paragraph-end" },
2768
+ annotations: []
2769
+ })
2770
+ };
2771
+ const NovelStage = {
2772
+ schema: readonlyObject({
2773
+ range: v.nullable(ManuscriptRange.schema),
2774
+ lines: readonlyArray(NovelLine.schema)
2775
+ }),
2776
+ of: (lines) => ({
2777
+ lines,
2778
+ range: ManuscriptRange.merge(lines.map(({ range }) => range))
2779
+ })
2780
+ };
2781
+ const NovelPage = {
2782
+ schema: readonlyObject({
2783
+ range: v.nullable(ManuscriptRange.schema),
2784
+ stages: readonlyArray(NovelStage.schema)
2785
+ }),
2786
+ of: (stages) => ({
2787
+ stages,
2788
+ range: ManuscriptRange.merge(stages.map(({ range }) => range))
2789
+ })
2790
+ };
2791
+ const NovelLayout = { schema: readonlyObject({
2792
+ pages: readonlyArray(NovelPage.schema),
2793
+ geometry: ManuscriptGeometry.schema,
2794
+ stats: CompositionStatistics.schema
2795
+ }) };
2796
+ //#endregion
2797
+ //#region src/composer/run-measurer.ts
2798
+ function graphemeWidth(grapheme) {
2799
+ let width = 1;
2800
+ for (const character of grapheme) {
2801
+ const codePoint = character.codePointAt(0);
2802
+ if (codePoint !== void 0) {
2803
+ const characterWidth = "!?‼⁇⁈⁉".includes(character) ? 2 : eastAsianWidth(codePoint, { ambiguousAsWide: true });
2804
+ width = Math.max(width, characterWidth);
2805
+ }
2806
+ }
2807
+ return width / 2;
2808
+ }
2809
+ /**
2810
+ * Deterministic DOM-independent metrics. Aggregate width does not imply a shared cluster.
2811
+ */
2812
+ const logicalRunMeasurer = (request) => {
2813
+ const segments = [...graphemeSegmenter.segment(request.text)];
2814
+ return {
2815
+ kind: "advance",
2816
+ advanceEm: (request.kind === "base" && request.presentation === "upright" ? segments.length : request.kind === "base" && request.presentation === "tate-chu-yoko" ? 1 : segments.reduce((total, { segment }) => total + graphemeWidth(segment), 0)) * request.scale
2817
+ };
2818
+ };
2819
+ //#endregion
2820
+ //#region src/composer/novel-composer.ts
2821
+ const COMPOSER_ID = NamespacedId.of("kg/novel");
2822
+ const TYPESETTING_PROFILE = defaultJapaneseTypesettingProfile;
2823
+ function displayedLines(graphemes) {
2824
+ const lines = [[]];
2825
+ let endedWithLineBreak = false;
2826
+ for (const grapheme of graphemes) {
2827
+ if (grapheme.value === "\n" || grapheme.value === "\r" || grapheme.value === "\r\n") {
2828
+ lines.push([]);
2829
+ endedWithLineBreak = true;
2830
+ continue;
2831
+ }
2832
+ lines.at(-1)?.push(grapheme);
2833
+ endedWithLineBreak = false;
2834
+ }
2835
+ if (lines.length > 1 && endedWithLineBreak) lines.pop();
2836
+ return lines;
2837
+ }
2838
+ function measureSourceLine(sourceLine, annotations, measurements) {
2839
+ const runs = CompositionRun.recognize(sourceLine);
2840
+ const rubyAssociations = RubyAssociation.collect(annotations, sourceLine);
2841
+ const atoms = [];
2842
+ for (const [runIndex, run] of runs.entries()) {
2843
+ const text = run.members.map(({ value }) => value).join("");
2844
+ const pieces = measurements.pieces(text, "base", run.presentation);
2845
+ if (pieces === void 0) return void 0;
2846
+ const combineRun = run.presentation === "tate-chu-yoko" || pieces.length < run.members.length;
2847
+ let memberStart = 0;
2848
+ for (const piece of pieces) {
2849
+ const memberCount = [...graphemeSegmenter.segment(piece.value)].length;
2850
+ const members = run.members.slice(memberStart, memberStart + memberCount);
2851
+ const clusterRange = memberCount > 1 ? ManuscriptRange.merge(members.map(({ range }) => range)) : null;
2852
+ for (const [memberIndex, grapheme] of members.entries()) {
2853
+ const first = memberIndex === 0;
2854
+ const renderAdvanceEm = first ? piece.renderSpan.advanceEm : 0;
2855
+ const measuredAdvanceEm = first ? piece.layoutSpan.advanceEm : 0;
2856
+ const characterClass = TYPESETTING_PROFILE.classify({
2857
+ value: grapheme.value,
2858
+ presentation: run.presentation
2859
+ });
2860
+ const metrics = TYPESETTING_PROFILE.boxMetrics(characterClass, measuredAdvanceEm);
2861
+ atoms.push({
2862
+ grapheme,
2863
+ runIndex,
2864
+ characterClass,
2865
+ intrinsicBoxAdvanceEm: metrics.advanceEm,
2866
+ boxAdvanceEm: metrics.advanceEm,
2867
+ renderAdvanceEm,
2868
+ renderOffsetEm: metrics.renderOffsetEm + (first ? piece.renderSpan.offsetEm - piece.layoutSpan.offsetEm : 0),
2869
+ presentation: run.presentation,
2870
+ clusterRange,
2871
+ combineRun
2872
+ });
2873
+ }
2874
+ memberStart += memberCount;
2875
+ }
2876
+ }
2877
+ const candidateReadings = [];
2878
+ const groupReadings = [];
2879
+ const baseAdvances = atoms.map((atom) => atom.boxAdvanceEm);
2880
+ const physicalRange = (first, last) => {
2881
+ const left = atoms[first];
2882
+ const right = atoms[last];
2883
+ while (first > 0 && left?.combineRun === true && atoms[first - 1]?.runIndex === left.runIndex) first -= 1;
2884
+ while (last + 1 < atoms.length && right?.combineRun === true && atoms[last + 1]?.runIndex === right.runIndex) last += 1;
2885
+ return {
2886
+ start: first,
2887
+ end: last + 1
2888
+ };
2889
+ };
2890
+ for (const association of rubyAssociations) {
2891
+ const { indexes } = association;
2892
+ if ((association.reading.kind === "group" ? [association.reading.text] : association.reading.segments).some((text) => measurements.pieces(text, "ruby") === void 0)) return void 0;
2893
+ if (association.reading.kind === "group") {
2894
+ const pieces = measurements.pieces(association.reading.text, "ruby");
2895
+ if (pieces === void 0) return void 0;
2896
+ const group = GroupRubyAllocation.prepare(association, pieces, baseAdvances);
2897
+ if (group === void 0) return void 0;
2898
+ groupReadings.push(group);
2899
+ continue;
2900
+ }
2901
+ for (const [segmentIndex, index] of indexes.entries()) {
2902
+ const segment = association.reading.segments[segmentIndex];
2903
+ const atom = atoms[index];
2904
+ if (segment === void 0 || atom === void 0) continue;
2905
+ const pieces = measurements.pieces(segment, "ruby");
2906
+ if (pieces === void 0) return void 0;
2907
+ candidateReadings.push({
2908
+ association,
2909
+ ...physicalRange(index, index),
2910
+ pieces
2911
+ });
2912
+ }
2913
+ }
2914
+ const sourceSpaces = SourceSpace.collect(sourceLine, atoms.map(({ boxAdvanceEm }) => boxAdvanceEm));
2915
+ return {
2916
+ atoms,
2917
+ runs,
2918
+ rubyAssociations,
2919
+ candidateReadings,
2920
+ groupReadings,
2921
+ sourceSpaces,
2922
+ suppressedIndexes: new Set(sourceSpaces.filter((space) => space.edgeBehavior === "suppress").map((space) => space.index))
2923
+ };
2924
+ }
2925
+ function continuationFor(annotation, range) {
2926
+ const starts = range.graphemes.start === annotation.range.graphemes.start;
2927
+ const ends = range.graphemes.end === annotation.range.graphemes.end;
2928
+ if (starts && ends) return "whole";
2929
+ if (starts) return "start";
2930
+ if (ends) return "end";
2931
+ return "middle";
2932
+ }
2933
+ function annotationFragmentRange(graphemes, suppressed, sourceGlues, annotation) {
2934
+ return ManuscriptRange.merge([
2935
+ ...graphemes.flatMap((unit) => unit.sources.flatMap(SourcePlacement.ranges)).filter((range) => ManuscriptRange.overlaps(range, annotation.range)),
2936
+ ...suppressed.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range),
2937
+ ...sourceGlues.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range)
2938
+ ]);
2939
+ }
2940
+ function annotationFragments(graphemes, suppressed, sourceGlues, annotations, candidatePlacements, emphasisForUnit) {
2941
+ const fragments = [];
2942
+ for (const annotation of annotations) {
2943
+ const covered = graphemes.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range));
2944
+ const fragmentRange = annotationFragmentRange(graphemes, suppressed, sourceGlues, annotation);
2945
+ const first = covered[0];
2946
+ const last = covered.at(-1);
2947
+ if (fragmentRange === null || first === void 0 || last === void 0) continue;
2948
+ const common = {
2949
+ annotationRange: annotation.range,
2950
+ fragmentRange,
2951
+ continuation: continuationFor(annotation, fragmentRange)
2952
+ };
2953
+ switch (annotation.kind) {
2954
+ case "bold":
2955
+ case "italic":
2956
+ fragments.push({
2957
+ kind: annotation.kind,
2958
+ ...common
2959
+ });
2960
+ break;
2961
+ case "emphasis":
2962
+ fragments.push({
2963
+ kind: "emphasis",
2964
+ mark: annotation.mark,
2965
+ placements: covered.flatMap((unit) => {
2966
+ const mark = emphasisForUnit(unit);
2967
+ return mark?.annotation === annotation ? [mark.placement] : [];
2968
+ }),
2969
+ ...common
2970
+ });
2971
+ break;
2972
+ case "ruby": {
2973
+ const resolved = candidatePlacements.filter(({ segment }) => segment.association.baseRange === annotation.range);
2974
+ if (resolved.length > 0) {
2975
+ let textOffset = 0;
2976
+ const readingItems = resolved.flatMap(({ items }) => {
2977
+ const mapped = items.map((item) => ({
2978
+ ...item,
2979
+ textRange: MeasurementTextRange.of({
2980
+ start: textOffset + item.textRange.start,
2981
+ end: textOffset + item.textRange.end
2982
+ })
2983
+ }));
2984
+ textOffset += items.at(-1)?.textRange.end ?? 0;
2985
+ return mapped;
2986
+ });
2987
+ fragments.push({
2988
+ kind: "ruby",
2989
+ rubyKind: annotation.reading.kind,
2990
+ reading: readingItems.map(({ value }) => value).join(""),
2991
+ readingItems,
2992
+ ...common
2993
+ });
2994
+ break;
2995
+ }
2996
+ return;
2997
+ }
2998
+ }
2999
+ }
3000
+ for (const unit of graphemes.filter((glyph) => glyph.kind === "combined-glyph")) {
3001
+ const readings = fragments.filter((fragment) => fragment.kind === "ruby" && ManuscriptRange.overlaps(fragment.fragmentRange, unit.range));
3002
+ for (const [index, left] of readings.entries()) {
3003
+ if (left.kind !== "ruby") continue;
3004
+ if (readings.slice(index + 1).some((right) => right.kind === "ruby" && left.readingItems.some(({ placement: a }) => right.readingItems.some(({ placement: b }) => a.inlineSpan.offsetEm < b.inlineSpan.offsetEm + b.inlineSpan.advanceEm - 1e-9 && b.inlineSpan.offsetEm < a.inlineSpan.offsetEm + a.inlineSpan.advanceEm - 1e-9 && a.blockOffsetEm < b.blockOffsetEm + b.blockSizeEm && b.blockOffsetEm < a.blockOffsetEm + a.blockSizeEm)))) return void 0;
3005
+ }
3006
+ }
3007
+ return fragments;
3008
+ }
3009
+ function renderUnits(raw) {
3010
+ const items = [];
3011
+ for (let index = 0; index < raw.length; index += 1) {
3012
+ const first = raw[index];
3013
+ if (first === void 0) continue;
3014
+ if (first.kind !== "member") {
3015
+ items.push(first);
3016
+ continue;
3017
+ }
3018
+ const members = [first];
3019
+ if (first.atom.combineRun) while (index + 1 < raw.length) {
3020
+ const next = raw[index + 1];
3021
+ if (next?.kind !== "member" || next.atom.runIndex !== first.atom.runIndex) break;
3022
+ members.push(next);
3023
+ index += 1;
3024
+ }
3025
+ const last = members.at(-1);
3026
+ if (last === void 0) return void 0;
3027
+ const range = ManuscriptRange.merge(members.map(({ atom }) => atom.grapheme.range));
3028
+ if (range === null) return void 0;
3029
+ const sources = [];
3030
+ for (let memberIndex = 0; memberIndex < members.length; memberIndex += 1) {
3031
+ const member = members[memberIndex];
3032
+ if (member === void 0) return void 0;
3033
+ const cluster = member.atom.clusterRange;
3034
+ if (cluster === null) {
3035
+ sources.push({
3036
+ kind: "exact",
3037
+ range: member.atom.grapheme.range,
3038
+ layoutSpan: member.layoutSpan
3039
+ });
3040
+ continue;
3041
+ }
3042
+ const shared = members.slice(memberIndex, memberIndex + cluster.graphemes.end - cluster.graphemes.start);
3043
+ if (shared.some(({ atom }) => atom.clusterRange === null || atom.clusterRange.graphemes.start !== cluster.graphemes.start || atom.clusterRange.graphemes.end !== cluster.graphemes.end)) return void 0;
3044
+ const end = shared.at(-1);
3045
+ if (end === void 0 || shared.length !== cluster.graphemes.end - cluster.graphemes.start) return void 0;
3046
+ sources.push({
3047
+ kind: "shared",
3048
+ ranges: shared.map(({ atom }) => atom.grapheme.range),
3049
+ layoutSpan: {
3050
+ offsetEm: member.layoutSpan.offsetEm,
3051
+ advanceEm: end.layoutSpan.offsetEm + end.layoutSpan.advanceEm - member.layoutSpan.offsetEm
3052
+ }
3053
+ });
3054
+ memberIndex += shared.length - 1;
3055
+ }
3056
+ let renderStart = Number.POSITIVE_INFINITY;
3057
+ let renderEnd = Number.NEGATIVE_INFINITY;
3058
+ for (const { atom, renderSpan } of members) {
3059
+ if (atom.clusterRange !== null && atom.grapheme.range.graphemes.start !== atom.clusterRange.graphemes.start) continue;
3060
+ renderStart = Math.min(renderStart, renderSpan.offsetEm);
3061
+ renderEnd = Math.max(renderEnd, renderSpan.offsetEm + renderSpan.advanceEm);
3062
+ }
3063
+ const common = {
3064
+ value: members.map(({ atom }) => atom.grapheme.value).join(""),
3065
+ range,
3066
+ sources,
3067
+ layoutSpan: {
3068
+ offsetEm: first.layoutSpan.offsetEm,
3069
+ advanceEm: last.layoutSpan.offsetEm + last.layoutSpan.advanceEm - first.layoutSpan.offsetEm
3070
+ },
3071
+ renderSpan: {
3072
+ offsetEm: renderStart,
3073
+ advanceEm: renderEnd - renderStart
3074
+ }
3075
+ };
3076
+ const presentation = first.atom.presentation;
3077
+ if (first.atom.combineRun) {
3078
+ if (members.length < 2 || presentation !== "tate-chu-yoko" && presentation !== "sideways") return void 0;
3079
+ items.push({
3080
+ ...common,
3081
+ kind: "combined-glyph",
3082
+ presentation,
3083
+ disposition: "placed"
3084
+ });
3085
+ } else {
3086
+ if (presentation === "tate-chu-yoko") return void 0;
3087
+ items.push({
3088
+ ...common,
3089
+ kind: "glyph",
3090
+ presentation,
3091
+ disposition: first.disposition
3092
+ });
3093
+ }
3094
+ }
3095
+ return items;
3096
+ }
3097
+ function positionedLine(atoms, sourceGapIndexes, plan) {
3098
+ const items = plan.suppressedIndexes.flatMap((index) => {
3099
+ const atom = atoms[index];
3100
+ return atom === void 0 ? [] : [{
3101
+ kind: "suppressed",
3102
+ value: atom.grapheme.value,
3103
+ range: atom.grapheme.range,
3104
+ reason: "question-or-exclamation-gap"
3105
+ }];
3106
+ });
3107
+ const spacings = new Map(plan.pairSpacings.map((spacing) => [spacing.boundary, spacing]));
3108
+ const characterSpacings = new Map(plan.characterSpacings.map((spacing) => [spacing.index, spacing]));
3109
+ const boxAdjustments = new Map(plan.boxAdjustments?.map((box) => [box.index, box]));
3110
+ let offsetEm = 0;
3111
+ const adjustmentOf = (widthEm, naturalWidthEm) => widthEm < naturalWidthEm ? "shrunk" : widthEm > naturalWidthEm ? "stretched" : "natural";
3112
+ const positionSpacing = (spacing) => {
3113
+ if (spacing.kind === "kern") items.push({
3114
+ kind: "kern",
3115
+ offsetEm,
3116
+ widthEm: spacing.widthEm
3117
+ });
3118
+ else items.push({
3119
+ kind: "glue",
3120
+ origin: "generated",
3121
+ offsetEm,
3122
+ widthEm: spacing.widthEm,
3123
+ naturalWidthEm: spacing.naturalWidthEm,
3124
+ adjustment: adjustmentOf(spacing.widthEm, spacing.naturalWidthEm)
3125
+ });
3126
+ offsetEm += spacing.widthEm;
3127
+ };
3128
+ for (let index = plan.contentStart; index < plan.end; index += 1) {
3129
+ const spacing = spacings.get(index);
3130
+ if (spacing !== void 0) positionSpacing(spacing);
3131
+ const atom = atoms[index];
3132
+ if (atom === void 0) continue;
3133
+ const characterSpacing = characterSpacings.get(index);
3134
+ if (sourceGapIndexes.has(index) || characterSpacing !== void 0) {
3135
+ const naturalWidthEm = characterSpacing?.naturalWidthEm ?? atom.boxAdvanceEm;
3136
+ const widthEm = characterSpacing?.widthEm ?? atom.boxAdvanceEm;
3137
+ items.push({
3138
+ kind: "glue",
3139
+ origin: "source",
3140
+ value: atom.grapheme.value,
3141
+ range: atom.grapheme.range,
3142
+ offsetEm,
3143
+ widthEm,
3144
+ naturalWidthEm,
3145
+ adjustment: adjustmentOf(widthEm, naturalWidthEm)
3146
+ });
3147
+ offsetEm += widthEm;
3148
+ continue;
3149
+ }
3150
+ const hanging = plan.hangingIndex === index;
3151
+ const adjusted = boxAdjustments.get(index);
3152
+ const layoutAdvanceEm = hanging ? 0 : adjusted?.advanceEm ?? atom.boxAdvanceEm;
3153
+ items.push({
3154
+ kind: "member",
3155
+ atom,
3156
+ layoutSpan: {
3157
+ offsetEm,
3158
+ advanceEm: layoutAdvanceEm
3159
+ },
3160
+ renderSpan: {
3161
+ offsetEm: offsetEm + (adjusted?.renderOffsetEm ?? atom.renderOffsetEm),
3162
+ advanceEm: atom.renderAdvanceEm
3163
+ },
3164
+ disposition: hanging ? "hanging" : "placed"
3165
+ });
3166
+ offsetEm += layoutAdvanceEm;
3167
+ }
3168
+ const trailingSpacing = spacings.get(plan.end);
3169
+ if (trailingSpacing !== void 0) positionSpacing(trailingSpacing);
3170
+ const positioned = renderUnits(items);
3171
+ if (positioned === void 0) return void 0;
3172
+ const ranges = positioned.flatMap((item) => PositionedInlineItem.isRenderUnit(item) || item.kind === "suppressed" || item.kind === "glue" && item.origin === "source" ? [item.range] : []);
3173
+ return {
3174
+ range: ManuscriptRange.merge(ranges),
3175
+ inlineSizeEm: Math.max(0, plan.inlineSizeEm),
3176
+ items: positioned,
3177
+ break: plan.break,
3178
+ annotations: []
3179
+ };
3180
+ }
3181
+ function wrapSourceLine(sourceLine, annotations, settings) {
3182
+ if (sourceLine.atoms.length === 0) return ManuscriptResult.succeed([NovelLine.empty()]);
3183
+ const profile = TYPESETTING_PROFILE;
3184
+ const characters = sourceLine.atoms.map((atom, index) => ({
3185
+ boxAdvanceEm: atom.boxAdvanceEm,
3186
+ sourceGap: sourceLine.suppressedIndexes.has(index),
3187
+ characterClass: atom.characterClass
3188
+ }));
3189
+ const rubyInteriors = RubyAssociation.fittableInteriors(sourceLine.rubyAssociations, characters.map(({ boxAdvanceEm }) => boxAdvanceEm), settings.flow.lineLengthEm, new Set(sourceLine.groupReadings.filter((group) => Math.max(group.basePrefixEm.at(-1) ?? 0, group.pieces.reduce((sum, piece) => sum + piece.layoutSpan.advanceEm, 0)) <= settings.flow.lineLengthEm).map((group) => group.association)));
3190
+ const rubyByIndex = new Map(sourceLine.rubyAssociations.flatMap((association) => association.indexes.map((index) => [index, association])));
3191
+ const rubyBases = sourceLine.atoms.map((atom, index) => {
3192
+ const association = rubyByIndex.get(index);
3193
+ return {
3194
+ lexicalClass: atom.characterClass,
3195
+ effectiveClass: association === void 0 ? atom.characterClass : association.reading.kind === "jukugo" ? "cl-23" : "cl-22",
3196
+ advanceEm: atom.boxAdvanceEm,
3197
+ renderOffsetEm: atom.renderOffsetEm,
3198
+ sourceGap: sourceLine.suppressedIndexes.has(index)
3199
+ };
3200
+ });
3201
+ const resolveBoundary = (leftIndex, rightIndex) => {
3202
+ const left = sourceLine.atoms[leftIndex];
3203
+ const right = sourceLine.atoms[rightIndex];
3204
+ const leftBase = rubyBases[leftIndex];
3205
+ const rightBase = rubyBases[rightIndex];
3206
+ if (left === void 0 || right === void 0 || leftBase === void 0 || rightBase === void 0) return void 0;
3207
+ const compound = rubyByIndex.get(leftIndex);
3208
+ const rubySpacingInterior = compound !== void 0 && compound === rubyByIndex.get(rightIndex) && compound.reading.kind !== "mono";
3209
+ return BoundaryRule.resolve(leftBase.effectiveClass, rightBase.effectiveClass, profile, {
3210
+ bindingSequence: leftBase.effectiveClass === "cl-08" && rightBase.effectiveClass === "cl-08" && BindingSequence.contains(left.grapheme.value, right.grapheme.value),
3211
+ runInterior: left.runIndex === right.runIndex,
3212
+ rubyInterior: rubyInteriors.has(rightIndex),
3213
+ rubySpacingInterior,
3214
+ sourceGap: sourceLine.suppressedIndexes.has(leftIndex) || sourceLine.suppressedIndexes.has(rightIndex)
3215
+ });
3216
+ };
3217
+ const boundaries = Array.from({ length: characters.length + 1 }, (_, boundary) => resolveBoundary(boundary - 1, boundary));
3218
+ const nextVisible = Array.from({ length: characters.length + 1 }).fill(characters.length);
3219
+ for (let index = characters.length - 1; index >= 0; index -= 1) nextVisible[index] = characters[index]?.sourceGap === true ? nextVisible[index + 1] ?? characters.length : index;
3220
+ const acrossGaps = characters.map((_, left) => resolveBoundary(left, nextVisible[left + 1] ?? characters.length));
3221
+ const paragraph = JapaneseParagraph.of(rubyBases.map((base) => ({
3222
+ boxAdvanceEm: base.advanceEm,
3223
+ characterClass: base.lexicalClass,
3224
+ effectiveClass: base.effectiveClass,
3225
+ sourceGap: base.sourceGap
3226
+ })), boundaries, profile, settings.flow.lineLengthEm);
3227
+ const readingIndex = RubyLineLayout.index(sourceLine.candidateReadings);
3228
+ const groupByIndex = new Map(sourceLine.groupReadings.flatMap((group) => group.association.indexes.map((index) => [index, group])));
3229
+ const resolvedRuby = /* @__PURE__ */ new WeakMap();
3230
+ const emphasisBySource = /* @__PURE__ */ new Map();
3231
+ const globalStart = sourceLine.atoms[0]?.grapheme.range.graphemes.start ?? 0;
3232
+ for (const [order, annotation] of annotations.entries()) {
3233
+ if (annotation.kind !== "emphasis") continue;
3234
+ const start = Math.max(annotation.range.graphemes.start, globalStart);
3235
+ const end = Math.min(annotation.range.graphemes.end, globalStart + characters.length);
3236
+ for (let index = start; index < end; index += 1) if (!emphasisBySource.has(index)) emphasisBySource.set(index, {
3237
+ annotation,
3238
+ order
3239
+ });
3240
+ }
3241
+ const emphasisForUnit = (unit) => {
3242
+ if (!EmphasisPlacement.accepts(sourceLine.atoms[unit.range.graphemes.start - globalStart]?.characterClass)) return void 0;
3243
+ let selected;
3244
+ for (let index = unit.range.graphemes.start; index < unit.range.graphemes.end; index += 1) {
3245
+ const candidate = emphasisBySource.get(index);
3246
+ if (candidate !== void 0 && (selected === void 0 || candidate.order < selected.order)) selected = candidate;
3247
+ }
3248
+ return selected === void 0 ? void 0 : {
3249
+ annotation: selected.annotation,
3250
+ placement: EmphasisPlacement.of(unit.renderSpan)
3251
+ };
3252
+ };
3253
+ let refusalReason = "render units or reading clusters cannot be placed without splitting or collision";
3254
+ const plans = layoutParagraph(paragraph.elements, settings.flow.lineLengthEm, (start, end, cursor) => {
3255
+ if (sourceLine.candidateReadings.length === 0 && sourceLine.groupReadings.length === 0 && emphasisBySource.size === 0) return paragraph.resolveCandidate(start, end);
3256
+ const effectiveEnd = nextVisible[end] === characters.length ? characters.length : end;
3257
+ const groups = /* @__PURE__ */ new Set();
3258
+ for (let index = start; index < effectiveEnd; index += 1) {
3259
+ const group = groupByIndex.get(index);
3260
+ if (group !== void 0) groups.add(group);
3261
+ }
3262
+ const assigned = [];
3263
+ let nextCursor = cursor;
3264
+ for (const group of groups) {
3265
+ const allocation = GroupRubyAllocation.assign(group, start, effectiveEnd, nextCursor, settings.flow.lineLengthEm);
3266
+ const physical = allocation.segment;
3267
+ const first = sourceLine.atoms[Math.max(physical.start, nextVisible[start] ?? start)];
3268
+ const last = sourceLine.atoms[Math.min(physical.end, end) - 1];
3269
+ if (first === void 0 || last === void 0) return void 0;
3270
+ const firstRun = first.combineRun ? sourceLine.runs[first.runIndex] : void 0;
3271
+ const lastRun = last.combineRun ? sourceLine.runs[last.runIndex] : void 0;
3272
+ const globalStart = sourceLine.atoms[0]?.grapheme.range.graphemes.start ?? 0;
3273
+ assigned.push({
3274
+ ...physical,
3275
+ start: firstRun === void 0 ? Math.max(physical.start, nextVisible[start] ?? start) : firstRun.range.graphemes.start - globalStart,
3276
+ end: lastRun === void 0 ? Math.min(physical.end, end) : lastRun.range.graphemes.end - globalStart
3277
+ });
3278
+ nextCursor = allocation.nextCursor;
3279
+ }
3280
+ const result = RubyLineLayout.resolve(rubyBases, readingIndex, start, end, (metrics) => paragraph.resolveCandidate(start, end, metrics), assigned);
3281
+ if (result === void 0) return void 0;
3282
+ const areas = result.placements.map((reading) => reading.items.map((item) => item.placement));
3283
+ if (emphasisBySource.size > 0) {
3284
+ const positioned = positionedLine(sourceLine.atoms, sourceLine.suppressedIndexes, result.line);
3285
+ if (positioned === void 0) return void 0;
3286
+ for (const unit of positioned.items.filter(PositionedInlineItem.isRenderUnit)) {
3287
+ const mark = emphasisForUnit(unit);
3288
+ if (mark !== void 0 && mark.annotation.mark !== "") areas.push([mark.placement]);
3289
+ }
3290
+ }
3291
+ const clearance = AnnotationClearance.resolve(areas);
3292
+ if (clearance.kind === "refused") {
3293
+ refusalReason = clearance.reason;
3294
+ return;
3295
+ }
3296
+ if (result.line.boxAdjustments !== void 0) resolvedRuby.set(result.line.boxAdjustments, result.placements);
3297
+ return {
3298
+ ...result.line,
3299
+ nextCursor
3300
+ };
3301
+ }, (left, right) => (right === left + 1 ? boundaries[right] : acrossGaps[left])?.break.kind !== "prohibited");
3302
+ if (plans.length === 0) return ManuscriptResult.fail({ reason: refusalReason });
3303
+ const lines = [];
3304
+ for (const plan of plans) {
3305
+ const line = positionedLine(sourceLine.atoms, sourceLine.suppressedIndexes, plan);
3306
+ if (line === void 0) return ManuscriptResult.fail({ reason: "render units cannot preserve their source members" });
3307
+ lines.push(line);
3308
+ }
3309
+ const annotated = [];
3310
+ for (const [index, line] of lines.entries()) {
3311
+ const plan = plans[index];
3312
+ const fragments = annotationFragments(line.items.filter(PositionedInlineItem.isRenderUnit), line.items.filter((item) => item.kind === "suppressed"), line.items.flatMap((item) => item.kind === "glue" && item.origin === "source" ? [item] : []), annotations, plan?.boxAdjustments === void 0 ? [] : resolvedRuby.get(plan.boxAdjustments) ?? [], emphasisForUnit);
3313
+ if (fragments === void 0) return ManuscriptResult.fail({ reason: "annotation fragments cannot preserve their reading positions" });
3314
+ annotated.push({
3315
+ ...line,
3316
+ annotations: fragments
3317
+ });
3318
+ }
3319
+ return ManuscriptResult.succeed(annotated);
3320
+ }
3321
+ function blankLines(count) {
3322
+ return Array.from({ length: count }, NovelLine.empty);
3323
+ }
3324
+ function buildPages(contentLines, settings) {
3325
+ const { linesPerStage, stagesPerPage } = settings.flow;
3326
+ const { stage: stageOffset, page: pageOffset } = settings.offsets;
3327
+ const usablePerStage = linesPerStage - LineOffset.total(stageOffset);
3328
+ const usablePerPage = usablePerStage * stagesPerPage;
3329
+ const pages = [];
3330
+ let contentIndex = 0;
3331
+ while (contentIndex < contentLines.length || pages.length === 0) {
3332
+ const stages = [];
3333
+ for (let stageIndex = 0; stageIndex < stagesPerPage; stageIndex += 1) {
3334
+ const lines = [];
3335
+ for (let position = 0; position < linesPerStage; position += 1) {
3336
+ const usableIndex = stageIndex * usablePerStage + position - stageOffset.leading;
3337
+ const withinStage = position >= stageOffset.leading && position < linesPerStage - stageOffset.trailing;
3338
+ const withinPage = usableIndex >= pageOffset.leading && usableIndex < usablePerPage - pageOffset.trailing;
3339
+ const next = contentLines[contentIndex];
3340
+ if (!withinStage || !withinPage || next === void 0) {
3341
+ lines.push(NovelLine.empty());
3342
+ continue;
3343
+ }
3344
+ lines.push(next);
3345
+ contentIndex += 1;
3346
+ }
3347
+ stages.push(NovelStage.of(lines));
3348
+ }
3349
+ pages.push(NovelPage.of(stages));
3350
+ }
3351
+ return pages;
3352
+ }
3353
+ function createCompose(provider) {
3354
+ return (manuscript, settings) => {
3355
+ const measurements = MeasurementSession.create(provider, settings);
3356
+ const sourceLines = displayedLines(manuscript.graphemes);
3357
+ const measured = sourceLines.map((line) => measureSourceLine(line, manuscript.annotations, measurements));
3358
+ if (measured.some((line) => line === void 0)) return ManuscriptResult.fail({ reason: "run measurer returned invalid metrics or cluster mappings" });
3359
+ const manuscriptLines = [];
3360
+ for (const line of measured) {
3361
+ if (line === void 0) continue;
3362
+ const wrapped = wrapSourceLine(line, manuscript.annotations, settings);
3363
+ if (!wrapped.ok) return ManuscriptResult.fail(wrapped.error);
3364
+ manuscriptLines.push(...wrapped.value);
3365
+ }
3366
+ if (!NovelSourceContract.matches(manuscript, manuscriptLines)) return ManuscriptResult.fail({ reason: "composition did not preserve source members or ruby readings" });
3367
+ const pages = buildPages([
3368
+ ...blankLines(settings.offsets.document.leading),
3369
+ ...manuscriptLines,
3370
+ ...blankLines(settings.offsets.document.trailing)
3371
+ ], settings);
3372
+ return ManuscriptResult.succeed({
3373
+ pages,
3374
+ geometry: ManuscriptGeometry.of(settings.flow, settings.appearance),
3375
+ stats: {
3376
+ chars: sourceLines.reduce((total, line) => total + line.length, 0),
3377
+ sourceLines: sourceLines.length,
3378
+ pages: pages.length
3379
+ }
3380
+ });
3381
+ };
3382
+ }
3383
+ function createNovelComposer(options) {
3384
+ return {
3385
+ id: COMPOSER_ID,
3386
+ settingsSchema: NovelCompositionSettings.schema,
3387
+ layoutSchema: NovelLayout.schema,
3388
+ compose: createCompose(options.measurer)
3389
+ };
3390
+ }
3391
+ const novelComposer = createNovelComposer({ measurer: logicalRunMeasurer });
3392
+ //#endregion
3393
+ export { PaperSize as a, MeasurementTextRange as c, ManuscriptOffsets as d, ManuscriptAppearanceSettings as f, ParsedManuscript as g, FontPresetId as h, ManuscriptGeometry as i, NovelCompositionSettings as l, FontSizePt as m, novelComposer as n, RunMeasurement as o, PaperSizeId as p, logicalRunMeasurer as r, MeasuredCluster as s, createNovelComposer as t, NovelFlowSettings as u };