@sushichan044/kg-core 0.9.4 → 0.10.0

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