@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.
@@ -1,2340 +0,0 @@
1
- import { c as ManuscriptResult, d as NamespacedId, f as ManuscriptRange, h as readonlyObject, i as OPENING_BRACKETS, m as readonlyArray, o as graphemeSegmenter, p as nonNegativeInteger, r as CLOSING_BRACKETS, t as questionOrExclamationSpacings } from "./question-or-exclamation-spacing-B5o23ZLf.mjs";
2
- import * as v from "valibot";
3
- import { eastAsianWidth } from "get-east-asian-width";
4
- const BoldAnnotation = { schema: readonlyObject({
5
- kind: v.literal("bold"),
6
- range: ManuscriptRange.schema
7
- }) };
8
- const EmphasisAnnotation = { schema: readonlyObject({
9
- kind: v.literal("emphasis"),
10
- range: ManuscriptRange.schema,
11
- mark: v.string()
12
- }) };
13
- const ItalicAnnotation = { schema: readonlyObject({
14
- kind: v.literal("italic"),
15
- range: ManuscriptRange.schema
16
- }) };
17
- //#endregion
18
- //#region src/parser/annotation/ruby-annotation.ts
19
- const GroupRubyKindSchema = v.literal("group");
20
- const MonoRubyKindSchema = v.literal("mono");
21
- const JukugoRubyKindSchema = v.literal("jukugo");
22
- const RubyKindSchema = v.union([
23
- GroupRubyKindSchema,
24
- MonoRubyKindSchema,
25
- JukugoRubyKindSchema
26
- ]);
27
- const RubyReadingSchema = v.variant("kind", [
28
- readonlyObject({
29
- kind: GroupRubyKindSchema,
30
- text: v.pipe(v.string(), v.nonEmpty())
31
- }),
32
- readonlyObject({
33
- kind: MonoRubyKindSchema,
34
- segments: v.pipe(v.array(v.pipe(v.string(), v.nonEmpty())), v.nonEmpty(), v.readonly())
35
- }),
36
- readonlyObject({
37
- kind: JukugoRubyKindSchema,
38
- segments: v.pipe(v.array(v.pipe(v.string(), v.nonEmpty())), v.nonEmpty(), v.readonly())
39
- })
40
- ]);
41
- const RubyAnnotationSchema = readonlyObject({
42
- kind: v.literal("ruby"),
43
- range: ManuscriptRange.schema,
44
- reading: RubyReadingSchema
45
- });
46
- const RubyKind = { schema: RubyKindSchema };
47
- const RubyAnnotation = { schema: RubyAnnotationSchema };
48
- const ManuscriptAnnotation = {
49
- schema: v.variant("kind", [
50
- RubyAnnotation.schema,
51
- BoldAnnotation.schema,
52
- ItalicAnnotation.schema,
53
- EmphasisAnnotation.schema
54
- ]),
55
- /**
56
- * Stable order for rendering: outermost span first, then by variant name.
57
- */
58
- compare: (left, right) => left.range.source.start - right.range.source.start || left.range.source.end - right.range.source.end || left.kind.localeCompare(right.kind)
59
- };
60
- const ParsedGrapheme = { schema: readonlyObject({
61
- value: v.string(),
62
- range: ManuscriptRange.schema
63
- }) };
64
- //#endregion
65
- //#region src/parser/parsed-manuscript.ts
66
- const ParsedManuscriptSchema = readonlyObject({
67
- source: v.string(),
68
- displayText: v.string(),
69
- graphemes: readonlyArray(ParsedGrapheme.schema),
70
- annotations: readonlyArray(ManuscriptAnnotation.schema)
71
- });
72
- function rangeFits(range, manuscript) {
73
- return range.source.end <= manuscript.source.length && range.display.end <= manuscript.displayText.length && range.graphemes.end <= manuscript.graphemes.length;
74
- }
75
- function hasValidRubyAssociations(manuscript) {
76
- const ruby = manuscript.annotations.filter((annotation) => annotation.kind === "ruby");
77
- return ruby.every((annotation, index) => {
78
- const baseLength = annotation.range.graphemes.end - annotation.range.graphemes.start;
79
- if (baseLength <= 0) return false;
80
- if (annotation.reading.kind !== "group" && annotation.reading.segments.length !== baseLength) return false;
81
- return ruby.slice(index + 1).every((other) => !ManuscriptRange.overlaps(annotation.range, other.range));
82
- });
83
- }
84
- function isConsistent(manuscript, source) {
85
- const joined = manuscript.graphemes.map(({ value }) => value).join("");
86
- const sequential = manuscript.graphemes.every(({ range }, index) => range.graphemes.start === index && range.graphemes.end === index + 1 && rangeFits(range, manuscript));
87
- return manuscript.source === source && manuscript.displayText === joined && sequential && manuscript.annotations.every(({ range }) => rangeFits(range, manuscript)) && hasValidRubyAssociations(manuscript);
88
- }
89
- const CONTRACT_MESSAGE = "parser output must contain consistent source, display, grapheme, and annotation ranges and valid ruby associations";
90
- const ParsedManuscript = {
91
- schema: ParsedManuscriptSchema,
92
- /**
93
- * The parser contract as a schema: shape, plus the cross-field invariants that make the three
94
- * coordinate systems line up. Annotations come out in a stable order regardless of parser.
95
- */
96
- contractFor: (source) => v.pipe(ParsedManuscriptSchema, v.check((manuscript) => isConsistent(manuscript, source), CONTRACT_MESSAGE), v.transform((manuscript) => ({
97
- ...manuscript,
98
- annotations: [...manuscript.annotations].sort(ManuscriptAnnotation.compare)
99
- })))
100
- };
101
- //#endregion
102
- //#region src/appearance/font-preset-id.ts
103
- const FontPresetIdSchema = v.picklist(["mincho", "gothic"]);
104
- const FontPresetId = {
105
- schema: FontPresetIdSchema,
106
- is: (value) => v.is(FontPresetIdSchema, value)
107
- };
108
- //#endregion
109
- //#region src/appearance/font-size-pt.ts
110
- const RANGE = {
111
- min: 6,
112
- max: 24,
113
- step: .5
114
- };
115
- const FontSizePtSchema = v.pipe(v.number(), v.finite(), v.minValue(RANGE.min), v.maxValue(RANGE.max), v.multipleOf(RANGE.step));
116
- const FontSizePt = {
117
- schema: FontSizePtSchema,
118
- range: RANGE,
119
- is: (value) => v.is(FontSizePtSchema, value)
120
- };
121
- //#endregion
122
- //#region src/appearance/paper-size-id.ts
123
- const PaperSizeIdSchema = v.picklist([
124
- "a4",
125
- "a5",
126
- "a6",
127
- "jis-b5",
128
- "jis-b6",
129
- "shinsho"
130
- ]);
131
- const PaperSizeId = {
132
- schema: PaperSizeIdSchema,
133
- is: (value) => v.is(PaperSizeIdSchema, value)
134
- };
135
- const ManuscriptAppearanceSettings = {
136
- schema: readonlyObject({
137
- paperSize: PaperSizeId.schema,
138
- fontSizePt: FontSizePt.schema,
139
- fontPreset: FontPresetId.schema
140
- }),
141
- defaults: {
142
- paperSize: "a5",
143
- fontSizePt: 9,
144
- fontPreset: "mincho"
145
- }
146
- };
147
- const LineOffset = {
148
- schema: readonlyObject({
149
- leading: nonNegativeInteger(),
150
- trailing: nonNegativeInteger()
151
- }),
152
- total: (offset) => offset.leading + offset.trailing
153
- };
154
- //#endregion
155
- //#region src/composer/manuscript-offsets.ts
156
- const MAX_DOCUMENT_OFFSET = 1e4;
157
- const ManuscriptOffsets = {
158
- schema: readonlyObject({
159
- document: readonlyObject({
160
- leading: nonNegativeInteger(MAX_DOCUMENT_OFFSET),
161
- trailing: nonNegativeInteger(MAX_DOCUMENT_OFFSET)
162
- }),
163
- page: LineOffset.schema,
164
- stage: LineOffset.schema
165
- }),
166
- defaults: {
167
- document: {
168
- leading: 0,
169
- trailing: 0
170
- },
171
- page: {
172
- leading: 0,
173
- trailing: 0
174
- },
175
- stage: {
176
- leading: 0,
177
- trailing: 0
178
- }
179
- },
180
- /**
181
- * Stage offsets must leave at least one usable line in the stage.
182
- */
183
- maxStageTotal: (flow) => Math.max(0, flow.linesPerStage - 1),
184
- /**
185
- * Page offsets must leave at least one usable line on the page, after stage offsets apply.
186
- */
187
- maxPageTotal: (flow, stage) => Math.max(0, (flow.linesPerStage - LineOffset.total(stage)) * flow.stagesPerPage - 1)
188
- };
189
- //#endregion
190
- //#region src/composer/novel-flow-settings.ts
191
- const RANGES = {
192
- lineLengthEm: {
193
- min: 10,
194
- max: 60
195
- },
196
- linesPerStage: {
197
- min: 10,
198
- max: 60
199
- },
200
- stagesPerPage: {
201
- min: 1,
202
- max: 3
203
- }
204
- };
205
- const boundedCount = (bounds) => v.pipe(v.number(), v.finite(), v.integer(), v.minValue(bounds.min), v.maxValue(bounds.max));
206
- const NovelFlowSettings = {
207
- schema: readonlyObject({
208
- lineLengthEm: boundedCount(RANGES.lineLengthEm),
209
- linesPerStage: boundedCount(RANGES.linesPerStage),
210
- stagesPerPage: boundedCount(RANGES.stagesPerPage)
211
- }),
212
- ranges: RANGES,
213
- defaults: {
214
- lineLengthEm: 27,
215
- linesPerStage: 23,
216
- stagesPerPage: 2
217
- }
218
- };
219
- //#endregion
220
- //#region src/composer/composition-settings.ts
221
- const NovelCompositionSettingsSchema = v.pipe(readonlyObject({
222
- flow: NovelFlowSettings.schema,
223
- offsets: ManuscriptOffsets.schema,
224
- appearance: ManuscriptAppearanceSettings.schema
225
- }), v.check(({ flow, offsets }) => LineOffset.total(offsets.stage) <= ManuscriptOffsets.maxStageTotal(flow) && LineOffset.total(offsets.page) <= ManuscriptOffsets.maxPageTotal(flow, offsets.stage), "page or stage offsets leave no usable novel lines"));
226
- const NovelCompositionSettings = {
227
- schema: NovelCompositionSettingsSchema,
228
- defaults: {
229
- flow: NovelFlowSettings.defaults,
230
- offsets: ManuscriptOffsets.defaults,
231
- appearance: ManuscriptAppearanceSettings.defaults
232
- },
233
- is: (value) => v.is(NovelCompositionSettingsSchema, value)
234
- };
235
- const InlineMeasurement = { schema: readonlyObject({ advanceEm: v.pipe(v.number(), v.finite(), v.minValue(0)) }) };
236
- function graphemeWidth(grapheme) {
237
- let width = 1;
238
- for (const character of grapheme) {
239
- const codePoint = character.codePointAt(0);
240
- if (codePoint !== void 0) width = Math.max(width, eastAsianWidth(codePoint, { ambiguousAsWide: true }));
241
- }
242
- return width / 2;
243
- }
244
- /**
245
- * Deterministic Japanese-context measurement in logical em units.
246
- */
247
- const logicalInlineMeasurer = (request) => {
248
- const { text } = request;
249
- const width = [...graphemeSegmenter.segment(text)].reduce((total, { segment }) => total + graphemeWidth(segment), 0);
250
- if (request.role === "ruby") return { advanceEm: width / 2 };
251
- return { advanceEm: request.presentation === "upright" ? [...graphemeSegmenter.segment(text)].length : width };
252
- };
253
- //#endregion
254
- //#region src/composer/internal/japanese-typesetting-profile.ts
255
- /**
256
- * The JLReq character classes (文字クラス) this profile distinguishes. The bare `cl-NN` identifiers are
257
- * kept rather than descriptive names so the spacing and penalty tables below can be read side by
258
- * side with the tables in the specification.
259
- *
260
- * JLReq defines thirty classes. Math (cl-17, cl-18) and warichu brackets (cl-28, cl-29) are absent
261
- * because a novel is not set with them. The base-group classes are absent for a different reason: a
262
- * novel does carry ruby (cl-22, cl-23), but ruby is an annotation the composer positions
263
- * separately, and `JapaneseCharacter` cannot describe a base group at all — nor a reference mark
264
- * (cl-20) or a subscripted base (cl-21). A base character is therefore classified here as whatever
265
- * it is on its own, which for the kanji that usually carries ruby means cl-19.
266
- *
267
- * @see https://www.w3.org/TR/jlreq/#character_classes
268
- */
269
- const japaneseCharacterClasses = [
270
- "cl-01",
271
- "cl-02",
272
- "cl-03",
273
- "cl-04",
274
- "cl-05",
275
- "cl-06",
276
- "cl-07",
277
- "cl-08",
278
- "cl-09",
279
- "cl-10",
280
- "cl-11",
281
- "cl-12",
282
- "cl-13",
283
- "cl-14",
284
- "cl-15",
285
- "cl-16",
286
- "cl-19",
287
- "cl-24",
288
- "cl-25",
289
- "cl-26",
290
- "cl-27",
291
- "cl-30"
292
- ];
293
- const OPENING = /* @__PURE__ */ new Set(`${OPENING_BRACKETS}【〘〝⦅«`);
294
- const CLOSING = /* @__PURE__ */ new Set(`${CLOSING_BRACKETS}】〙〟⦆»`);
295
- const HYPHENS = /* @__PURE__ */ new Set("‐‑⁃–");
296
- const DIVIDING_PUNCTUATION = /* @__PURE__ */ new Set("!?‼⁇⁈⁉");
297
- const MIDDLE_DOTS = /* @__PURE__ */ new Set("・:;");
298
- const SMALL_KANA = /* @__PURE__ */ new Set("ァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖ");
299
- const ITERATION_MARKS = /* @__PURE__ */ new Set("ヽヾゝゞ々〻");
300
- const INSEPARABLE = /* @__PURE__ */ new Set("—―…‥─〳〵");
301
- const PREFIXED_ABBREVIATIONS = /* @__PURE__ */ new Set("¥$£##");
302
- const POSTFIXED_ABBREVIATIONS = /* @__PURE__ */ new Set("°′″%‰");
303
- const UNIT_SYMBOLS = /* @__PURE__ */ new Set("℃℉㎀㎁㎂㎃㎄㎅㎆㎇㎈㎉㎊㎋㎌㎍㎎㎏㎐㎑㎒㎓㎔㎕㎖㎗㎘㎙㎚㎛㎜㎝㎞㎟㎠㎡㎢");
304
- const GROUPED_NUMERAL = /^[0-90-9]$/u;
305
- const HIRAGANA = /^\p{Script=Hiragana}$/u;
306
- const KATAKANA = /^\p{Script=Katakana}$/u;
307
- const WESTERN = /^[\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}]$/u;
308
- /**
309
- * Order matters: a character inside a tate-chu-yoko group is cl-30 (3.2.5) and an upright Latin
310
- * letter or Arabic numeral is cl-19 (3.2.4), whatever either would be on its own; anything left
311
- * unmatched is cl-19 too, the class JLReq reserves for kanji and the like.
312
- *
313
- * The quarter em 3.2.6 sets between Japanese and a Western character or numeral applies only to a
314
- * character rotated 90 degrees in vertical writing mode or mixed into horizontal writing mode —
315
- * 3.2.6 states that scope explicitly. A character set upright keeps its own rule, 3.2.4, which
316
- * reads it as a full-width monospace glyph classed with kanji and set solid against its neighbours,
317
- * the same as a tate-chu-yoko run's 3.2.5.
318
- */
319
- function classify({ value, presentation }) {
320
- if (presentation === "tate-chu-yoko") return "cl-30";
321
- if (presentation === "upright") return "cl-19";
322
- if (OPENING.has(value)) return "cl-01";
323
- if (CLOSING.has(value)) return "cl-02";
324
- if (HYPHENS.has(value)) return "cl-03";
325
- if (DIVIDING_PUNCTUATION.has(value)) return "cl-04";
326
- if (MIDDLE_DOTS.has(value)) return "cl-05";
327
- if (value === "。" || value === ".") return "cl-06";
328
- if (value === "、" || value === ",") return "cl-07";
329
- if (INSEPARABLE.has(value)) return "cl-08";
330
- if (ITERATION_MARKS.has(value)) return "cl-09";
331
- if (value === "ー") return "cl-10";
332
- if (SMALL_KANA.has(value)) return "cl-11";
333
- if (PREFIXED_ABBREVIATIONS.has(value)) return "cl-12";
334
- if (POSTFIXED_ABBREVIATIONS.has(value)) return "cl-13";
335
- if (value === " ") return "cl-14";
336
- if (HIRAGANA.test(value)) return "cl-15";
337
- if (KATAKANA.test(value)) return "cl-16";
338
- if (GROUPED_NUMERAL.test(value)) return "cl-24";
339
- if (UNIT_SYMBOLS.has(value)) return "cl-25";
340
- if (value === " ") return "cl-26";
341
- if (WESTERN.test(value)) return "cl-27";
342
- return "cl-19";
343
- }
344
- /**
345
- * The stages JLReq 3.8.3 spends inter-character space in, as glue priorities: the lower the number,
346
- * the earlier the paragraph optimizer takes it.
347
- *
348
- * JLReq puts the western word space first and the line-end reduction second; this profile swaps the
349
- * two, because a priority here says how visible an adjustment is rather than where 3.8.3's list
350
- * puts it. Priority 0 is capacity the paragraph optimizer spends for nothing, and a line that spent
351
- * only priority 0 reads as tightly set as an unadjusted one. That is true of the line end and of
352
- * nothing else: the half em after a closing bracket, full stop or comma — and the quarter em after
353
- * a middle dot — falls outside the text area once that character sits at the line end, so taking it
354
- * changes nothing on the page (3.1.9). A 欧文間隔 sits between two words the reader is looking at, so
355
- * narrowing it does change the page. Spending the invisible space first can only leave the word
356
- * spaces nearer their 三分アキ than JLReq's order would, never further from it.
357
- *
358
- * Stages two and three are one priority because a line has a single line end, so the two never
359
- * compete.
360
- */
361
- const REDUCTION_STAGE = {
362
- /**
363
- * Stages 2 and 3: the space after a closing bracket, full stop, comma or middle dot at line end.
364
- */
365
- lineEnd: 0,
366
- /**
367
- * Stage 1: the western word space, from its 三分アキ down to a 四分アキ.
368
- */
369
- wordSpace: 1,
370
- /**
371
- * Stage 4: the quarter em on either side of a mid-line middle dot.
372
- */
373
- middleDot: 2,
374
- /**
375
- * Stage 5: the half em before an opening bracket and after a closing bracket or comma.
376
- */
377
- punctuation: 3,
378
- /**
379
- * Stage 6: the quarter em between Japanese and a numeral, unit symbol or western character.
380
- */
381
- mixedText: 4
382
- };
383
- /**
384
- * The stages JLReq 3.8.4 adds inter-character space in, as glue priorities on the same reading as
385
- * {@link REDUCTION_STAGE}: the lower the number, the earlier the paragraph optimizer takes it. A
386
- * line is either reduced or expanded, never both, so the two scales never meet.
387
- *
388
- * Nothing here takes priority 0. Expanding is always visible, and priority 0 means an adjustment
389
- * the optimizer is charged nothing for.
390
- */
391
- const EXPANSION_STAGE = {
392
- /**
393
- * Stage 1: the western word space, from its 三分アキ up to a 二分アキ.
394
- */
395
- wordSpace: 1,
396
- /**
397
- * Stage 2: the quarter em between Japanese and a numeral, unit symbol or western character.
398
- */
399
- mixedText: 2,
400
- /**
401
- * Stage 3: everywhere else 表6 admits, from solid up to a quarter em.
402
- */
403
- solid: 3,
404
- /**
405
- * Stage 4: the unbounded remainder, spread equally across every colored or gray cell of 表6.
406
- */
407
- final: 4
408
- };
409
- /**
410
- * 無印 in 表1 with no colour in 表6: the pair is set solid and line adjustment leaves it alone.
411
- */
412
- const SOLID = {
413
- kind: "glue",
414
- naturalWidthEm: 0
415
- };
416
- /**
417
- * `1/4` in 表6: set solid, but line adjustment may open it up to a quarter em.
418
- */
419
- const SOLID_EXPANDABLE = {
420
- kind: "glue",
421
- naturalWidthEm: 0,
422
- stretch: {
423
- priority: EXPANSION_STAGE.solid,
424
- amountEm: .25,
425
- granularity: "continuous"
426
- }
427
- };
428
- /**
429
- * `1/2` in 表1 with `1/2-0` in 表3: a half em that line adjustment may take down to solid.
430
- */
431
- const HALF = {
432
- kind: "glue",
433
- naturalWidthEm: .5,
434
- shrink: {
435
- priority: REDUCTION_STAGE.punctuation,
436
- amountEm: .5,
437
- granularity: "continuous"
438
- }
439
- };
440
- /**
441
- * `1/2` in both tables: the half em exists but is not a reduction opportunity.
442
- */
443
- const FIXED_HALF = {
444
- kind: "glue",
445
- naturalWidthEm: .5
446
- };
447
- /**
448
- * `1/4` in 表1 with `1/4-0` in 表3: a quarter em that may go down to solid.
449
- */
450
- const QUARTER = {
451
- kind: "glue",
452
- naturalWidthEm: .25,
453
- shrink: {
454
- priority: REDUCTION_STAGE.middleDot,
455
- amountEm: .25,
456
- granularity: "continuous"
457
- }
458
- };
459
- /**
460
- * `1/4` in both tables: the quarter em exists but is not a reduction opportunity.
461
- */
462
- const FIXED_QUARTER = {
463
- kind: "glue",
464
- naturalWidthEm: .25
465
- };
466
- /**
467
- * `1/4-1/8` in 表3 with `1/4-1/2` in 表6: the Japanese-to-western quarter em, reducible to an eighth
468
- * and expandable to a half. 3.2.6 sets this only against a Western character rotated 90 degrees or
469
- * mixed into horizontal text (cl-27) and a numeral or unit symbol (cl-24, cl-25); an upright
470
- * Western character classifies as cl-19 instead and never reaches this constant.
471
- */
472
- const MIXED_TEXT_QUARTER = {
473
- kind: "glue",
474
- naturalWidthEm: .25,
475
- shrink: {
476
- priority: REDUCTION_STAGE.mixedText,
477
- amountEm: .125,
478
- granularity: "continuous"
479
- },
480
- stretch: {
481
- priority: EXPANSION_STAGE.mixedText,
482
- amountEm: .25,
483
- granularity: "continuous"
484
- }
485
- };
486
- /**
487
- * The same quarter em where 表6 leaves it out of the expansion: between a unit symbol and the
488
- * quantity on either side of it.
489
- */
490
- const UNIT_SYMBOL_QUARTER = {
491
- kind: "glue",
492
- naturalWidthEm: .25,
493
- shrink: {
494
- priority: REDUCTION_STAGE.mixedText,
495
- amountEm: .125,
496
- granularity: "continuous"
497
- }
498
- };
499
- /**
500
- * 表1 注3 with 表3 注1: two middle dots meet, and their quarters go solid together.
501
- */
502
- const TWO_QUARTERS = {
503
- kind: "glue",
504
- naturalWidthEm: .5,
505
- shrink: {
506
- priority: REDUCTION_STAGE.middleDot,
507
- amountEm: .5,
508
- granularity: "continuous"
509
- }
510
- };
511
- /**
512
- * 表1 注5 with 表3 注2: a full stop's fixed half em plus the following middle dot's quarter.
513
- */
514
- const FIXED_HALF_AND_QUARTER = {
515
- kind: "glue",
516
- naturalWidthEm: .75,
517
- shrink: {
518
- priority: REDUCTION_STAGE.middleDot,
519
- amountEm: .25,
520
- granularity: "continuous"
521
- }
522
- };
523
- /**
524
- * 表1 注5 with 表3 注3: a comma's half em plus the following middle dot's quarter.
525
- *
526
- * 表3 spends the quarter at stage 4 and the half at stage 5, which one capacity cannot say. Both are
527
- * reducible to solid, so the total is right and only the order collapses — and `、・` is rare enough
528
- * that the order never shows.
529
- */
530
- const HALF_AND_QUARTER = {
531
- kind: "glue",
532
- naturalWidthEm: .75,
533
- shrink: {
534
- priority: REDUCTION_STAGE.punctuation,
535
- amountEm: .75,
536
- granularity: "continuous"
537
- }
538
- };
539
- /**
540
- * 分離禁止文字 repeated (a 2倍ダッシュ, a 2倍リーダ) must read as one continuous rule (3.1.10).
541
- */
542
- const DASH_JOINT = {
543
- kind: "kern",
544
- naturalWidthEm: 0
545
- };
546
- /**
547
- * 欧文間隔(cl-26)の三分アキ. JLReq states the western word space as a third em by rule rather than by
548
- * measurement, and makes it the first space a line adjustment reaches for: down to a 四分アキ when the
549
- * line is over (3.8.3 a), up to a 二分アキ when it is short (3.8.4 a).
550
- *
551
- * The two capacities are written as differences from the limits rather than as `1/12` and `1/6`,
552
- * because a third of an em is not representable in binary while the limits JLReq states are: this
553
- * way `naturalWidthEm - shrink.amountEm` is exactly 0.25 and `naturalWidthEm + stretch.amountEm`
554
- * exactly 0.5.
555
- */
556
- const WORD_SPACE = {
557
- kind: "glue",
558
- naturalWidthEm: 1 / 3,
559
- shrink: {
560
- priority: REDUCTION_STAGE.wordSpace,
561
- amountEm: 1 / 3 - 1 / 4,
562
- granularity: "continuous"
563
- },
564
- stretch: {
565
- priority: EXPANSION_STAGE.wordSpace,
566
- amountEm: 1 / 2 - 1 / 3,
567
- granularity: "continuous"
568
- }
569
- };
570
- /**
571
- * 表1 注13 と 表3 注4: a word space at a line head or a line end has no width and is no adjustment
572
- * opportunity, since the white would fall outside the text area where no reader can see it. 注13
573
- * adds ただし,この部分が移動し,これとは異なる配置になった場合は,欧文間隔(cl-26)の空き量を確保する — which is why the answer is given per
574
- * candidate line and not once per source line.
575
- */
576
- const EDGE_WORD_SPACE = {
577
- kind: "glue",
578
- naturalWidthEm: 0
579
- };
580
- /**
581
- * The classes that take a quarter em against a numeral, unit symbol or western character rotated 90
582
- * degrees or mixed into horizontal text (3.2.6) — an upright western character classifies as cl-19
583
- * and is covered by the general kanji rows and columns instead. Dividing punctuation is on the list
584
- * in that direction only, because the space before a `?` or `!` is solid (3.1.6).
585
- */
586
- const JAPANESE_BEFORE_WESTERN = [
587
- "cl-04",
588
- "cl-09",
589
- "cl-10",
590
- "cl-11",
591
- "cl-15",
592
- "cl-16",
593
- "cl-19",
594
- "cl-30"
595
- ];
596
- const JAPANESE_AFTER_WESTERN = [
597
- "cl-09",
598
- "cl-10",
599
- "cl-11",
600
- "cl-15",
601
- "cl-16",
602
- "cl-19",
603
- "cl-30"
604
- ];
605
- const NUMERAL_UNIT_OR_WESTERN = [
606
- "cl-24",
607
- "cl-25",
608
- "cl-27"
609
- ];
610
- const PUNCTUATION_TAKING_SPACE_AFTER = [
611
- "cl-02",
612
- "cl-06",
613
- "cl-07"
614
- ];
615
- /**
616
- * 表6 の色付きの小間: the classes a solid pair may be opened up between at stage 3. Neither list holds a
617
- * bracket, a middle dot, a full stop, a comma, a hyphen, an ideographic space or a word space —
618
- * JLReq opens none of those — and only the left-hand list holds a numeral, unit symbol or western
619
- * character, since the space on their Japanese side belongs to stage 2 instead.
620
- */
621
- const EXPANDABLE_BEFORE = [
622
- "cl-08",
623
- "cl-09",
624
- "cl-10",
625
- "cl-11",
626
- "cl-12",
627
- "cl-13",
628
- "cl-15",
629
- "cl-16",
630
- "cl-19",
631
- "cl-24",
632
- "cl-25",
633
- "cl-27",
634
- "cl-30"
635
- ];
636
- const EXPANDABLE_AFTER = [
637
- "cl-08",
638
- "cl-09",
639
- "cl-10",
640
- "cl-11",
641
- "cl-12",
642
- "cl-13",
643
- "cl-15",
644
- "cl-16",
645
- "cl-19",
646
- "cl-30"
647
- ];
648
- /**
649
- * The gray, blue and pink cells of 表6: all pairs that join the fourth and final expansion stage.
650
- * The finite capacities above cover the blue and pink cells at stages three and two; once those
651
- * stages are exhausted, JLReq 3.8.4 d adds the remainder equally across every cell named here.
652
- *
653
- * A cl-08/cl-08 cell is blue only when the two characters are different kinds of mark (注4). This
654
- * profile cannot distinguish the two halves of one 2倍ダッシュ from different marks, so it keeps the
655
- * pair joined, as it already does for the finite third stage.
656
- */
657
- const FINAL_EXPANSION_BASIC_AFTER = /* @__PURE__ */ new Set([
658
- "cl-01",
659
- "cl-08",
660
- "cl-12",
661
- "cl-13",
662
- "cl-14",
663
- "cl-15",
664
- "cl-16",
665
- "cl-19",
666
- "cl-24",
667
- "cl-25",
668
- "cl-26",
669
- "cl-27",
670
- "cl-30"
671
- ]);
672
- const FINAL_EXPANSION_WIDE_AFTER = /* @__PURE__ */ new Set([
673
- ...FINAL_EXPANSION_BASIC_AFTER,
674
- "cl-09",
675
- "cl-10",
676
- "cl-11"
677
- ]);
678
- const FINAL_EXPANSION_AFTER = new Map([
679
- ...[
680
- "cl-02",
681
- "cl-03",
682
- "cl-04",
683
- "cl-05",
684
- "cl-06",
685
- "cl-07",
686
- "cl-14"
687
- ].map((left) => [left, FINAL_EXPANSION_BASIC_AFTER]),
688
- ...[
689
- "cl-09",
690
- "cl-10",
691
- "cl-11",
692
- "cl-13",
693
- "cl-15",
694
- "cl-16",
695
- "cl-19",
696
- "cl-30"
697
- ].map((left) => [left, FINAL_EXPANSION_WIDE_AFTER]),
698
- ["cl-08", new Set([...FINAL_EXPANSION_WIDE_AFTER].filter((characterClass) => characterClass !== "cl-08"))],
699
- ["cl-12", /* @__PURE__ */ new Set([
700
- "cl-08",
701
- "cl-09",
702
- "cl-10",
703
- "cl-11",
704
- "cl-12",
705
- "cl-13",
706
- "cl-15",
707
- "cl-16",
708
- "cl-19",
709
- "cl-25",
710
- "cl-26",
711
- "cl-27",
712
- "cl-30"
713
- ])],
714
- ["cl-24", /* @__PURE__ */ new Set([
715
- "cl-01",
716
- "cl-08",
717
- "cl-09",
718
- "cl-10",
719
- "cl-11",
720
- "cl-12",
721
- "cl-14",
722
- "cl-15",
723
- "cl-16",
724
- "cl-19",
725
- "cl-25",
726
- "cl-26",
727
- "cl-30"
728
- ])],
729
- ["cl-25", /* @__PURE__ */ new Set([
730
- "cl-01",
731
- "cl-08",
732
- "cl-09",
733
- "cl-10",
734
- "cl-11",
735
- "cl-12",
736
- "cl-13",
737
- "cl-14",
738
- "cl-15",
739
- "cl-16",
740
- "cl-19",
741
- "cl-24",
742
- "cl-26",
743
- "cl-30"
744
- ])],
745
- ["cl-26", new Set(japaneseCharacterClasses.filter((characterClass) => characterClass !== "cl-02"))],
746
- ["cl-27", /* @__PURE__ */ new Set([
747
- "cl-01",
748
- "cl-08",
749
- "cl-09",
750
- "cl-10",
751
- "cl-11",
752
- "cl-12",
753
- "cl-13",
754
- "cl-14",
755
- "cl-15",
756
- "cl-16",
757
- "cl-19",
758
- "cl-26",
759
- "cl-30"
760
- ])]
761
- ]);
762
- function canExpandAtFinalStage(left, right) {
763
- if (left === "cl-14" || left === "cl-26" || left === "cl-27" || right === "cl-14" || right === "cl-26" || right === "cl-27") return false;
764
- if (FINAL_EXPANSION_AFTER.get(left)?.has(right) !== true) return false;
765
- return pairSpacing(left, right).naturalWidthEm === 0;
766
- }
767
- /**
768
- * The classes JLReq 3.1.2 sets on a half em rather than a full one.
769
- */
770
- const HALF_EM_CLASSES = /* @__PURE__ */ new Set([
771
- "cl-01",
772
- "cl-02",
773
- "cl-05",
774
- "cl-06",
775
- "cl-07"
776
- ]);
777
- const PAIR_SPACING_TABLE = new Map([
778
- {
779
- left: "any",
780
- right: "any",
781
- spacing: SOLID
782
- },
783
- {
784
- left: EXPANDABLE_BEFORE,
785
- right: EXPANDABLE_AFTER,
786
- spacing: SOLID_EXPANDABLE
787
- },
788
- {
789
- left: [
790
- "cl-08",
791
- "cl-12",
792
- "cl-13"
793
- ],
794
- right: NUMERAL_UNIT_OR_WESTERN,
795
- spacing: SOLID_EXPANDABLE
796
- },
797
- {
798
- left: ["cl-08"],
799
- right: ["cl-24", "cl-25"],
800
- spacing: SOLID
801
- },
802
- {
803
- left: ["cl-12"],
804
- right: ["cl-24"],
805
- spacing: SOLID
806
- },
807
- {
808
- left: ["cl-24"],
809
- right: ["cl-13"],
810
- spacing: SOLID
811
- },
812
- {
813
- left: [
814
- "cl-09",
815
- "cl-10",
816
- "cl-11"
817
- ],
818
- right: ["cl-08"],
819
- spacing: SOLID
820
- },
821
- {
822
- left: "any",
823
- right: ["cl-01"],
824
- spacing: HALF
825
- },
826
- {
827
- left: "any",
828
- right: ["cl-05"],
829
- spacing: QUARTER
830
- },
831
- {
832
- left: "any",
833
- right: PUNCTUATION_TAKING_SPACE_AFTER,
834
- spacing: SOLID
835
- },
836
- {
837
- left: JAPANESE_BEFORE_WESTERN,
838
- right: NUMERAL_UNIT_OR_WESTERN,
839
- spacing: MIXED_TEXT_QUARTER
840
- },
841
- {
842
- left: NUMERAL_UNIT_OR_WESTERN,
843
- right: JAPANESE_AFTER_WESTERN,
844
- spacing: MIXED_TEXT_QUARTER
845
- },
846
- {
847
- left: ["cl-25"],
848
- right: ["cl-24"],
849
- spacing: UNIT_SYMBOL_QUARTER
850
- },
851
- {
852
- left: ["cl-27"],
853
- right: ["cl-25"],
854
- spacing: UNIT_SYMBOL_QUARTER
855
- },
856
- {
857
- left: ["cl-24"],
858
- right: ["cl-25"],
859
- spacing: FIXED_QUARTER
860
- },
861
- {
862
- left: ["cl-02", "cl-07"],
863
- right: "any",
864
- spacing: HALF
865
- },
866
- {
867
- left: ["cl-06"],
868
- right: "any",
869
- spacing: FIXED_HALF
870
- },
871
- {
872
- left: ["cl-02"],
873
- right: ["cl-05"],
874
- spacing: QUARTER
875
- },
876
- {
877
- left: ["cl-06"],
878
- right: ["cl-05"],
879
- spacing: FIXED_HALF_AND_QUARTER
880
- },
881
- {
882
- left: ["cl-07"],
883
- right: ["cl-05"],
884
- spacing: HALF_AND_QUARTER
885
- },
886
- {
887
- left: PUNCTUATION_TAKING_SPACE_AFTER,
888
- right: PUNCTUATION_TAKING_SPACE_AFTER,
889
- spacing: SOLID
890
- },
891
- {
892
- left: ["cl-02"],
893
- right: ["cl-14"],
894
- spacing: SOLID
895
- },
896
- {
897
- left: ["cl-05"],
898
- right: "any",
899
- spacing: QUARTER
900
- },
901
- {
902
- left: ["cl-05"],
903
- right: ["cl-05"],
904
- spacing: TWO_QUARTERS
905
- },
906
- {
907
- left: ["cl-01"],
908
- right: "any",
909
- spacing: SOLID
910
- },
911
- {
912
- left: ["cl-01"],
913
- right: ["cl-05"],
914
- spacing: QUARTER
915
- },
916
- {
917
- left: ["cl-14"],
918
- right: "any",
919
- spacing: SOLID
920
- },
921
- {
922
- left: ["cl-14"],
923
- right: ["cl-05"],
924
- spacing: QUARTER
925
- },
926
- {
927
- left: "any",
928
- right: ["cl-26"],
929
- spacing: SOLID
930
- },
931
- {
932
- left: PUNCTUATION_TAKING_SPACE_AFTER,
933
- right: ["cl-26"],
934
- spacing: FIXED_HALF
935
- },
936
- {
937
- left: ["cl-05"],
938
- right: ["cl-26"],
939
- spacing: FIXED_QUARTER
940
- },
941
- {
942
- left: ["cl-26"],
943
- right: "any",
944
- spacing: SOLID
945
- },
946
- {
947
- left: ["cl-26"],
948
- right: ["cl-01"],
949
- spacing: FIXED_HALF
950
- },
951
- {
952
- left: ["cl-26"],
953
- right: ["cl-05"],
954
- spacing: FIXED_QUARTER
955
- },
956
- {
957
- left: ["cl-08"],
958
- right: ["cl-08"],
959
- spacing: DASH_JOINT
960
- }
961
- ].flatMap(({ left, right, spacing }) => (left === "any" ? japaneseCharacterClasses : left).flatMap((leftClass) => (right === "any" ? japaneseCharacterClasses : right).map((rightClass) => [`${leftClass}/${rightClass}`, spacing]))));
962
- function pairSpacing(left, right) {
963
- return PAIR_SPACING_TABLE.get(`${left}/${right}`) ?? SOLID;
964
- }
965
- /**
966
- * `null` removes the break opportunity outright rather than pricing it, because the three rules
967
- * below are prohibitions and not preferences.
968
- *
969
- * Line-end prohibition (行末禁則, JLReq 3.1.8): an opening bracket or a prefixed abbreviation may not
970
- * be the last character on a line, since it belongs to what follows it.
971
- *
972
- * Line-start prohibition (行頭禁則, JLReq 3.1.7): closing brackets, hyphens, dividing punctuation,
973
- * middle dots, full stops, commas, iteration marks, the prolonged sound mark, small kana and
974
- * postfixed abbreviations may not open a line.
975
- *
976
- * Unbreakable sequences (分割禁止, JLReq 3.1.10): a 分離禁止文字 followed by another one is a single mark set
977
- * over two ems and must not be split.
978
- */
979
- function breakPenalty(left, right) {
980
- if (left === "cl-01" || left === "cl-12") return null;
981
- 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;
982
- if (left === "cl-08" && right === "cl-08") return null;
983
- return 0;
984
- }
985
- const defaultJapaneseTypesettingProfile = {
986
- classify,
987
- /**
988
- * Opening and closing brackets, full stops, commas and middle dots are set on a half em (JLReq
989
- * 3.1.2); the space the pair table puts around them is what brings each back to a full em.
990
- *
991
- * A fullwidth glyph keeps drawing its ink where the font put it, so halving the advance alone
992
- * would leave the ink outside the box. The offset pulls it back: an opening bracket draws in the
993
- * trailing half of its em, a middle dot in the centre, and the rest in the leading half.
994
- */
995
- boxMetrics: (characterClass, measuredAdvanceEm) => {
996
- if (characterClass === "cl-26") return {
997
- advanceEm: 0,
998
- renderOffsetEm: 0
999
- };
1000
- const advanceEm = HALF_EM_CLASSES.has(characterClass) ? Math.min(.5, measuredAdvanceEm) : measuredAdvanceEm;
1001
- const inkOverhangEm = measuredAdvanceEm - advanceEm;
1002
- return {
1003
- advanceEm,
1004
- renderOffsetEm: characterClass === "cl-01" ? -inkOverhangEm : characterClass === "cl-05" ? -inkOverhangEm / 2 : 0
1005
- };
1006
- },
1007
- pairSpacing,
1008
- spacingCharacter: (characterClass, position) => characterClass !== "cl-26" ? null : position === "mid-line" ? WORD_SPACE : EDGE_WORD_SPACE,
1009
- /**
1010
- * The white before an opening bracket at a line head. JLReq 3.1.5 pairs 改行行頭 with 折返し行頭 and lists
1011
- * three schemes for the two together: ① 全角アキ and 天付き, ② 全角半アキ and 二分アキ, ③ 二分アキ and 天付き. This
1012
- * profile takes ③, the scheme 講談社, 新潮社, 文藝春秋, 中央公論新社 and 筑摩書房 set novels with; ① is what JIS X
1013
- * 4051 and 岩波書店 use. A line of dialogue therefore starts one half em in when it opens a
1014
- * paragraph, and flush against the edge when the composer turned it over.
1015
- *
1016
- * The white is not shrinkable. 表3 excludes the line head from line adjustment altogether, and
1017
- * shrinking it is exactly what pushed a line-head bracket flush against the edge in #94.
1018
- */
1019
- lineStartSpacing: (first, lineHead) => first === "cl-01" && lineHead === "paragraph-start" ? {
1020
- kind: "glue",
1021
- naturalWidthEm: .5
1022
- } : null,
1023
- /**
1024
- * The half em trailing a closing bracket, full stop or comma at the line end, and the quarter em
1025
- * trailing a middle dot there (JLReq 3.1.9). Priority 0: spending it changes nothing the reader
1026
- * can see.
1027
- *
1028
- * Both are all-or-nothing. 3.1.9 admits the full アキ or ベタ組 and forbids anything in between, so a
1029
- * line whose overflow is smaller than the アキ has to find the difference somewhere else — or break
1030
- * elsewhere. A line-end middle dot takes the quarter before it into the same amount (表3 注5).
1031
- */
1032
- lineEndSpacing: (last) => last === "cl-02" || last === "cl-06" || last === "cl-07" ? {
1033
- spacing: {
1034
- kind: "glue",
1035
- naturalWidthEm: .5,
1036
- shrink: {
1037
- priority: REDUCTION_STAGE.lineEnd,
1038
- amountEm: .5,
1039
- granularity: "all-or-nothing"
1040
- }
1041
- },
1042
- absorbsPrecedingEm: 0
1043
- } : last === "cl-05" ? {
1044
- spacing: {
1045
- kind: "glue",
1046
- naturalWidthEm: .25,
1047
- shrink: {
1048
- priority: REDUCTION_STAGE.lineEnd,
1049
- amountEm: .25,
1050
- granularity: "all-or-nothing"
1051
- }
1052
- },
1053
- absorbsPrecedingEm: .25
1054
- } : null,
1055
- finalStretchPriority: EXPANSION_STAGE.final,
1056
- canExpandAtFinalStage,
1057
- breakPenalty,
1058
- /**
1059
- * Hanging punctuation (ぶら下げ組, JLReq 3.8.2) lets a full stop or comma sit outside the text area
1060
- * instead of forcing the line to be respaced. JLReq admits no other class for it.
1061
- */
1062
- canHang: (characterClass) => characterClass === "cl-07" || characterClass === "cl-06"
1063
- };
1064
- //#endregion
1065
- //#region src/composer/internal/paragraph-layout.ts
1066
- const EPSILON = 1e-9;
1067
- /**
1068
- * Spending capacity at this priority changes nothing the reader can see.
1069
- */
1070
- const FREE_SHRINK_PRIORITY = 0;
1071
- /**
1072
- * Interleave the two kinds onto one number so the allocator can key a plain `Map` without building
1073
- * a string per boundary — this runs once per adjustment unit of every candidate line.
1074
- */
1075
- function slotKey(slot) {
1076
- return slot.kind === "gap" ? slot.boundary * 2 : slot.index * 2 + 1;
1077
- }
1078
- function skipSourceGaps(atoms, start) {
1079
- let cursor = start;
1080
- while (atoms[cursor]?.sourceGap === true) cursor += 1;
1081
- return cursor;
1082
- }
1083
- function previousVisible(atoms, end) {
1084
- for (let index = end - 1; index >= 0; index -= 1) if (atoms[index]?.sourceGap === false) return index;
1085
- }
1086
- /**
1087
- * Whether a shorter line ending somewhere inside `(contentStart, end)` was ever an option — checked
1088
- * as a plain short-circuiting scan rather than building the boundary list first, since this runs
1089
- * once per over-long candidate the optimizer considers.
1090
- */
1091
- function hasEarlierBoundary(boundaryAllowed, contentStart, end) {
1092
- for (let boundary = contentStart + 1; boundary < end; boundary += 1) if (boundaryAllowed(boundary - 1, boundary)) return true;
1093
- return false;
1094
- }
1095
- function opportunities(atoms, classes, start, end, profile, lineHead) {
1096
- const result = [];
1097
- const firstClass = classes[start];
1098
- const startSpacing = firstClass === void 0 ? null : profile.lineStartSpacing(firstClass, lineHead);
1099
- if (startSpacing !== null) result.push({
1100
- slot: {
1101
- kind: "gap",
1102
- boundary: start
1103
- },
1104
- spacing: startSpacing,
1105
- finalStretch: false,
1106
- absorbsPrecedingEm: 0
1107
- });
1108
- for (let index = start; index < end; index += 1) {
1109
- const characterClass = classes[index];
1110
- if (characterClass === void 0 || atoms[index]?.sourceGap === true) continue;
1111
- const spacing = profile.spacingCharacter(characterClass, index === start || index === end - 1 ? "line-edge" : "mid-line");
1112
- if (spacing !== null) result.push({
1113
- slot: {
1114
- kind: "character",
1115
- index
1116
- },
1117
- spacing,
1118
- finalStretch: false,
1119
- absorbsPrecedingEm: 0
1120
- });
1121
- }
1122
- for (let right = start + 1; right < end; right += 1) {
1123
- const leftAtom = atoms[right - 1];
1124
- const rightAtom = atoms[right];
1125
- const leftClass = classes[right - 1];
1126
- const rightClass = classes[right];
1127
- if (leftAtom === void 0 || rightAtom === void 0 || leftClass === void 0 || rightClass === void 0 || leftAtom.sourceGap || rightAtom.sourceGap || !leftAtom.pairSpacingAfter) continue;
1128
- result.push({
1129
- slot: {
1130
- kind: "gap",
1131
- boundary: right
1132
- },
1133
- spacing: profile.pairSpacing(leftClass, rightClass),
1134
- finalStretch: profile.canExpandAtFinalStage(leftClass, rightClass),
1135
- absorbsPrecedingEm: 0
1136
- });
1137
- }
1138
- const lastClass = classes[end - 1];
1139
- const endSpacing = lastClass === void 0 ? null : profile.lineEndSpacing(lastClass);
1140
- if (endSpacing !== null) result.push({
1141
- slot: {
1142
- kind: "gap",
1143
- boundary: end
1144
- },
1145
- spacing: endSpacing.spacing,
1146
- finalStretch: false,
1147
- absorbsPrecedingEm: endSpacing.absorbsPrecedingEm
1148
- });
1149
- return result;
1150
- }
1151
- /**
1152
- * Break the line's spaces into the indivisible amounts the line adjustment may spend.
1153
- *
1154
- * A line end the profile says absorbs the space before it claims that much of the preceding
1155
- * boundary's capacity, and the two become one unit at the line-end stage — JLReq 3.8.3's third
1156
- * stage, which is free. Whatever the preceding boundary has left over stays an opportunity of its
1157
- * own stage, so the half em of a `、` before a line-end `・` is still spent where 3.8.3 puts it.
1158
- */
1159
- function adjustmentUnits(values, adjustment) {
1160
- const select = (spacing) => adjustment === "shrink" ? spacing.shrink : spacing.stretch;
1161
- const units = [];
1162
- const straddle = values.find(({ absorbsPrecedingEm }) => absorbsPrecedingEm > 0);
1163
- let absorbedBoundary;
1164
- let absorbedEm = 0;
1165
- let absorbingBoundary;
1166
- const straddleSlot = straddle?.slot.kind === "gap" ? straddle.slot : void 0;
1167
- const straddleOwn = straddle === void 0 ? void 0 : select(straddle.spacing);
1168
- if (straddle !== void 0 && straddleSlot !== void 0 && straddleOwn !== void 0) {
1169
- const precedingBoundary = straddleSlot.boundary - 1;
1170
- const preceding = values.find(({ slot }) => slot.kind === "gap" && slot.boundary === precedingBoundary);
1171
- const availableEm = preceding === void 0 ? 0 : select(preceding.spacing)?.amountEm ?? 0;
1172
- absorbingBoundary = straddleSlot.boundary;
1173
- if (availableEm + EPSILON >= straddle.absorbsPrecedingEm) {
1174
- absorbedBoundary = precedingBoundary;
1175
- absorbedEm = straddle.absorbsPrecedingEm;
1176
- units.push({
1177
- parts: [{
1178
- slot: {
1179
- kind: "gap",
1180
- boundary: precedingBoundary
1181
- },
1182
- amountEm: absorbedEm
1183
- }, {
1184
- slot: straddleSlot,
1185
- amountEm: straddleOwn.amountEm
1186
- }],
1187
- priority: straddleOwn.priority,
1188
- amountEm: absorbedEm + straddleOwn.amountEm,
1189
- granularity: straddleOwn.granularity
1190
- });
1191
- }
1192
- }
1193
- for (const { slot, spacing } of values) {
1194
- if (slot.kind === "gap" && slot.boundary === absorbingBoundary) continue;
1195
- const own = select(spacing);
1196
- if (own === void 0) continue;
1197
- const claimedEm = slot.kind === "gap" && slot.boundary === absorbedBoundary ? absorbedEm : 0;
1198
- const amountEm = own.amountEm - claimedEm;
1199
- if (amountEm <= EPSILON) continue;
1200
- units.push({
1201
- parts: [{
1202
- slot,
1203
- amountEm
1204
- }],
1205
- priority: own.priority,
1206
- amountEm,
1207
- granularity: own.granularity
1208
- });
1209
- }
1210
- return units;
1211
- }
1212
- /**
1213
- * Spend `amountEm` over the line's units of adjustment, stage by stage.
1214
- *
1215
- * Every stage of JLReq 3.8.3 and 3.8.4 is stated as 文字サイズ比で均等に — the English text of 3.8.3 a puts
1216
- * it as "The same width reduction is applied to all spaces on the target line at the same time."
1217
- * Within a stage the amount is therefore split in proportion to what each space can give, rather
1218
- * than taken out of the earliest space until it runs dry. Across stages the order stays a
1219
- * waterfall: a later stage is reached only once every stage before it is spent out. The composer
1220
- * sets one character size, so a share of the stage's capacity is a share by character size. JLReq
1221
- * 3.8.4's final stage has no upper bound: after the finite stages are full, the unresolved
1222
- * remainder is added equally to every pair the profile admits, including the earlier-stage pairs.
1223
- *
1224
- * An all-or-nothing unit larger than what is still needed is skipped rather than partly spent, and
1225
- * never revisited: `remaining` only falls, so a unit that did not fit at its own stage cannot fit
1226
- * at a later one. One forward pass is therefore exact and no search over subsets is needed.
1227
- */
1228
- function resolveSpacings(values, units, adjustment, amountEm, finalStretchPriority) {
1229
- const usedBySlot = /* @__PURE__ */ new Map();
1230
- let remaining = amountEm;
1231
- let priorityCost = 0;
1232
- let freeEm = 0;
1233
- let finalStretchPerSlotEm = 0;
1234
- const spend = (unit, fraction) => {
1235
- for (const { slot, amountEm: partEm } of unit.parts) {
1236
- const key = slotKey(slot);
1237
- usedBySlot.set(key, (usedBySlot.get(key) ?? 0) + partEm * fraction);
1238
- }
1239
- const spentEm = unit.amountEm * fraction;
1240
- priorityCost += spentEm * unit.priority;
1241
- if (unit.priority === FREE_SHRINK_PRIORITY) freeEm += spentEm;
1242
- remaining -= spentEm;
1243
- };
1244
- const stages = /* @__PURE__ */ new Map();
1245
- for (const unit of units) {
1246
- const stage = stages.get(unit.priority);
1247
- if (stage === void 0) stages.set(unit.priority, [unit]);
1248
- else stage.push(unit);
1249
- }
1250
- for (const priority of [...stages.keys()].sort((left, right) => left - right)) {
1251
- if (remaining <= EPSILON) break;
1252
- const stage = stages.get(priority) ?? [];
1253
- let stageCapacityEm = 0;
1254
- for (const unit of stage) if (unit.granularity === "continuous") stageCapacityEm += unit.amountEm;
1255
- else if (unit.amountEm <= remaining + EPSILON) spend(unit, 1);
1256
- if (stageCapacityEm <= EPSILON || remaining <= EPSILON) continue;
1257
- const fraction = Math.min(remaining, stageCapacityEm) / stageCapacityEm;
1258
- for (const unit of stage) if (unit.granularity === "continuous") spend(unit, fraction);
1259
- }
1260
- if (adjustment === "stretch" && remaining > EPSILON) {
1261
- let finalSlotCount = 0;
1262
- for (const { finalStretch } of values) if (finalStretch) finalSlotCount += 1;
1263
- if (finalSlotCount > 0) {
1264
- finalStretchPerSlotEm = remaining / finalSlotCount;
1265
- for (const { slot, finalStretch } of values) {
1266
- if (!finalStretch) continue;
1267
- const key = slotKey(slot);
1268
- usedBySlot.set(key, (usedBySlot.get(key) ?? 0) + finalStretchPerSlotEm);
1269
- }
1270
- priorityCost += remaining * finalStretchPriority;
1271
- remaining = 0;
1272
- }
1273
- }
1274
- return {
1275
- spacings: splitBySlot(values, (slot, spacing) => {
1276
- const used = usedBySlot.get(slotKey(slot)) ?? 0;
1277
- return adjustment === "shrink" ? spacing.naturalWidthEm - used : spacing.naturalWidthEm + used;
1278
- }),
1279
- priorityCost,
1280
- freeEm,
1281
- finalStretchPerSlotEm,
1282
- unabsorbedEm: Math.max(0, remaining)
1283
- };
1284
- }
1285
- function splitBySlot(values, widthOf) {
1286
- const pairSpacings = [];
1287
- const characterSpacings = [];
1288
- for (const { slot, spacing } of values) {
1289
- const resolved = {
1290
- kind: spacing.kind,
1291
- naturalWidthEm: spacing.naturalWidthEm,
1292
- widthEm: widthOf(slot, spacing)
1293
- };
1294
- if (slot.kind === "gap") pairSpacings.push({
1295
- boundary: slot.boundary,
1296
- ...resolved
1297
- });
1298
- else characterSpacings.push({
1299
- index: slot.index,
1300
- ...resolved
1301
- });
1302
- }
1303
- return {
1304
- pairSpacings,
1305
- characterSpacings
1306
- };
1307
- }
1308
- /**
1309
- * The same spaces at the width the profile gives them, for a line no adjustment reaches.
1310
- */
1311
- function naturalSpacings(values) {
1312
- return splitBySlot(values, (_slot, spacing) => spacing.naturalWidthEm);
1313
- }
1314
- /**
1315
- * The same, minus the アキ the line end would have taken: a hanging character sits outside the text
1316
- * area and the space that would have followed it goes with it.
1317
- */
1318
- function hangingSpacings(values, end) {
1319
- const { pairSpacings, characterSpacings } = naturalSpacings(values);
1320
- return {
1321
- pairSpacings: pairSpacings.filter(({ boundary }) => boundary !== end),
1322
- characterSpacings
1323
- };
1324
- }
1325
- function candidate(atoms, classes, boxPrefixEm, start, end, lineLengthEm, profile) {
1326
- const contentStart = skipSourceGaps(atoms, start);
1327
- const suppressedIndexes = Array.from({ length: contentStart - start }, (_, index) => start + index);
1328
- const pairValues = opportunities(atoms, classes, contentStart, end, profile, start === 0 ? "paragraph-start" : "turned-over");
1329
- const naturalSizeEm = (boxPrefixEm[end] ?? 0) - (boxPrefixEm[contentStart] ?? 0) + pairValues.reduce((total, value) => total + value.spacing.naturalWidthEm, 0);
1330
- const terminal = skipSourceGaps(atoms, end) === atoms.length;
1331
- const overflow = naturalSizeEm - lineLengthEm;
1332
- const underflow = lineLengthEm - naturalSizeEm;
1333
- const units = adjustmentUnits(pairValues, overflow > 0 ? "shrink" : "stretch");
1334
- const capacity = units.reduce((total, { amountEm }) => total + amountEm, 0);
1335
- const freeShrinkCapacity = units.reduce((total, { priority, amountEm }) => total + (priority === FREE_SHRINK_PRIORITY ? amountEm : 0), 0);
1336
- const lastVisible = previousVisible(atoms, end);
1337
- const lastClass = lastVisible === void 0 ? void 0 : classes[lastVisible];
1338
- const lastAdvance = lastVisible === void 0 ? 0 : atoms[lastVisible]?.boxAdvanceEm ?? 0;
1339
- const trailingSpacing = pairValues.find(({ slot }) => slot.kind === "gap" && slot.boundary === end)?.spacing.naturalWidthEm ?? 0;
1340
- const hasFinalStretch = pairValues.some(({ finalStretch }) => finalStretch);
1341
- if (terminal && overflow <= EPSILON) return {
1342
- start,
1343
- contentStart,
1344
- end,
1345
- suppressedIndexes,
1346
- ...naturalSpacings(pairValues),
1347
- inlineSizeEm: naturalSizeEm,
1348
- break: { kind: "paragraph-end" },
1349
- hangingIndex: null,
1350
- deformationRatio: 0,
1351
- priorityCost: 0
1352
- };
1353
- if (Math.abs(overflow) <= EPSILON) return {
1354
- start,
1355
- contentStart,
1356
- end,
1357
- suppressedIndexes,
1358
- ...naturalSpacings(pairValues),
1359
- inlineSizeEm: naturalSizeEm,
1360
- break: { kind: "natural" },
1361
- hangingIndex: null,
1362
- deformationRatio: 0,
1363
- priorityCost: 0
1364
- };
1365
- const shrunk = overflow > 0 && overflow <= capacity + EPSILON ? resolveSpacings(pairValues, units, "shrink", overflow, profile.finalStretchPriority) : null;
1366
- if (shrunk !== null && shrunk.unabsorbedEm <= EPSILON) {
1367
- const chargeableCapacity = capacity - freeShrinkCapacity;
1368
- const chargeableShrink = Math.max(0, overflow - shrunk.freeEm);
1369
- return {
1370
- start,
1371
- contentStart,
1372
- end,
1373
- suppressedIndexes,
1374
- ...shrunk.spacings,
1375
- inlineSizeEm: lineLengthEm,
1376
- break: { kind: "shrunk" },
1377
- hangingIndex: null,
1378
- deformationRatio: chargeableCapacity <= EPSILON ? 0 : chargeableShrink / chargeableCapacity,
1379
- priorityCost: shrunk.priorityCost
1380
- };
1381
- }
1382
- if (!terminal && overflow > 0 && lastVisible !== void 0 && lastClass !== void 0 && profile.canHang(lastClass) && naturalSizeEm - lastAdvance - trailingSpacing <= lineLengthEm + EPSILON) return {
1383
- start,
1384
- contentStart,
1385
- end,
1386
- suppressedIndexes,
1387
- ...hangingSpacings(pairValues, end),
1388
- inlineSizeEm: naturalSizeEm - lastAdvance - trailingSpacing,
1389
- break: { kind: "hanging" },
1390
- hangingIndex: lastVisible,
1391
- deformationRatio: overflow / Math.max(lastAdvance, EPSILON),
1392
- priorityCost: 0
1393
- };
1394
- const stretched = !terminal && underflow > EPSILON && (underflow <= capacity + EPSILON || hasFinalStretch) ? resolveSpacings(pairValues, units, "stretch", underflow, profile.finalStretchPriority) : null;
1395
- if (stretched !== null && stretched.unabsorbedEm <= EPSILON) return {
1396
- start,
1397
- contentStart,
1398
- end,
1399
- suppressedIndexes,
1400
- ...stretched.spacings,
1401
- inlineSizeEm: lineLengthEm,
1402
- break: { kind: "stretched" },
1403
- hangingIndex: null,
1404
- deformationRatio: stretched.finalStretchPerSlotEm > 0 ? 1 + stretched.finalStretchPerSlotEm : capacity === 0 ? 0 : underflow / capacity,
1405
- priorityCost: stretched.priorityCost
1406
- };
1407
- return {
1408
- start,
1409
- contentStart,
1410
- end,
1411
- suppressedIndexes,
1412
- ...naturalSpacings(pairValues),
1413
- inlineSizeEm: naturalSizeEm,
1414
- break: { kind: "forced" },
1415
- hangingIndex: null,
1416
- deformationRatio: Math.abs(naturalSizeEm - lineLengthEm) / Math.max(lineLengthEm, EPSILON),
1417
- priorityCost: 0
1418
- };
1419
- }
1420
- function scoreFor(line, previousFitness) {
1421
- const mode = line.break.kind;
1422
- const fitness = Math.min(3, Math.floor(line.deformationRatio * 4));
1423
- const transition = Math.abs(previousFitness - fitness) > 1 ? 1 : 0;
1424
- const modeCounts = {
1425
- forced: mode === "forced" ? 1 : 0,
1426
- stretched: mode === "stretched" ? 1 : 0,
1427
- hanging: mode === "hanging" ? 1 : 0,
1428
- shrunk: mode === "shrunk" && line.priorityCost > EPSILON ? 1 : 0
1429
- };
1430
- return {
1431
- score: [
1432
- modeCounts.forced,
1433
- modeCounts.stretched,
1434
- modeCounts.hanging,
1435
- modeCounts.shrunk,
1436
- line.priorityCost,
1437
- line.deformationRatio ** 3,
1438
- transition
1439
- ],
1440
- fitness
1441
- };
1442
- }
1443
- function addScore(left, right) {
1444
- return [
1445
- left[0] + right[0],
1446
- left[1] + right[1],
1447
- left[2] + right[2],
1448
- left[3] + right[3],
1449
- left[4] + right[4],
1450
- left[5] + right[5],
1451
- left[6] + right[6]
1452
- ];
1453
- }
1454
- /**
1455
- * The line-end offsets a state's path took, earliest first. Rebuilt on demand by walking `previous`
1456
- * rather than carried on every `State`: every candidate line attempted would otherwise pay for a
1457
- * copy of its whole path, when `isBetter` only ever reads it on the exact-score tie this comparison
1458
- * breaks — for `boundaryAllowed` predicates permissive enough to fully saturate the optimizer's
1459
- * search window, ties are rare enough that this deferred cost is negligible.
1460
- */
1461
- function breaksOf(state, states) {
1462
- const result = [];
1463
- let cursor = state;
1464
- while (cursor?.line !== null && cursor?.line !== void 0) {
1465
- result.push(cursor.line.end);
1466
- cursor = cursor.previous === null ? void 0 : states.get(cursor.previous.index)?.get(cursor.previous.fitness);
1467
- }
1468
- return result.reverse();
1469
- }
1470
- function compareBreaks(left, right) {
1471
- const length = Math.min(left.length, right.length);
1472
- for (let index = 0; index < length; index += 1) {
1473
- const difference = (right[index] ?? 0) - (left[index] ?? 0);
1474
- if (difference !== 0) return difference;
1475
- }
1476
- return left.length - right.length;
1477
- }
1478
- function isBetter(candidateState, current, states) {
1479
- if (current === void 0) return true;
1480
- for (let index = 0; index < candidateState.score.length; index += 1) {
1481
- const difference = (candidateState.score[index] ?? 0) - (current.score[index] ?? 0);
1482
- if (Math.abs(difference) > EPSILON) return difference < 0;
1483
- }
1484
- return compareBreaks(breaksOf(candidateState, states), breaksOf(current, states)) < 0;
1485
- }
1486
- function layoutParagraph(atoms, lineLengthEm, profile, boundaryAllowed) {
1487
- if (atoms.length === 0) return [];
1488
- const classes = atoms.map(({ characterClass }) => characterClass);
1489
- const boxPrefixEm = Array.from({ length: atoms.length + 1 }).fill(0);
1490
- for (let index = 0; index < atoms.length; index += 1) boxPrefixEm[index + 1] = (boxPrefixEm[index] ?? 0) + (atoms[index]?.boxAdvanceEm ?? 0);
1491
- const states = /* @__PURE__ */ new Map([[0, /* @__PURE__ */ new Map([[0, {
1492
- score: [
1493
- 0,
1494
- 0,
1495
- 0,
1496
- 0,
1497
- 0,
1498
- 0,
1499
- 0
1500
- ],
1501
- fitness: 0,
1502
- previous: null,
1503
- line: null
1504
- }]])]]);
1505
- for (let start = 0; start < atoms.length; start += 1) {
1506
- const activeStates = states.get(start);
1507
- if (activeStates === void 0) continue;
1508
- const contentStart = skipSourceGaps(atoms, start);
1509
- if (contentStart === atoms.length) {
1510
- const terminalStates = states.get(atoms.length) ?? /* @__PURE__ */ new Map();
1511
- for (const state of activeStates.values()) if (isBetter(state, terminalStates.get(state.fitness), states)) terminalStates.set(state.fitness, state);
1512
- states.set(atoms.length, terminalStates);
1513
- continue;
1514
- }
1515
- for (const state of activeStates.values()) for (let end = contentStart + 1; end <= atoms.length; end += 1) {
1516
- const right = skipSourceGaps(atoms, end);
1517
- const left = previousVisible(atoms, end);
1518
- if (atoms[end - 1]?.sourceGap === true) continue;
1519
- if (end < atoms.length && right < atoms.length && (left === void 0 || !boundaryAllowed(left, right))) continue;
1520
- const line = candidate(atoms, classes, boxPrefixEm, start, end, lineLengthEm, profile);
1521
- if (line.break.kind === "forced" && line.inlineSizeEm > lineLengthEm + EPSILON && hasEarlierBoundary(boundaryAllowed, contentStart, end)) {
1522
- if (line.inlineSizeEm > lineLengthEm * 2 && end > contentStart + 1) break;
1523
- continue;
1524
- }
1525
- const lineScore = scoreFor(line, state.fitness);
1526
- const nextState = {
1527
- score: addScore(state.score, lineScore.score),
1528
- fitness: lineScore.fitness,
1529
- previous: {
1530
- index: start,
1531
- fitness: state.fitness
1532
- },
1533
- line
1534
- };
1535
- const statesAtEnd = states.get(end) ?? /* @__PURE__ */ new Map();
1536
- if (isBetter(nextState, statesAtEnd.get(nextState.fitness), states)) {
1537
- statesAtEnd.set(nextState.fitness, nextState);
1538
- states.set(end, statesAtEnd);
1539
- }
1540
- if (line.inlineSizeEm > lineLengthEm * 2 && line.break.kind === "forced" && end > contentStart + 1) break;
1541
- }
1542
- }
1543
- const terminal = [...states.get(atoms.length)?.values() ?? []].reduce((best, state) => isBetter(state, best, states) ? state : best, void 0);
1544
- if (terminal === void 0) return [];
1545
- const lines = [];
1546
- let cursor = terminal;
1547
- while (cursor?.line !== null && cursor?.line !== void 0) {
1548
- lines.push(cursor.line);
1549
- cursor = cursor.previous === null ? void 0 : states.get(cursor.previous.index)?.get(cursor.previous.fitness);
1550
- }
1551
- lines.reverse();
1552
- const last = lines.at(-1);
1553
- if (last !== void 0 && last.end < atoms.length) lines[lines.length - 1] = {
1554
- ...last,
1555
- suppressedIndexes: [...last.suppressedIndexes, ...Array.from({ length: atoms.length - last.end }, (_, index) => last.end + index)]
1556
- };
1557
- return lines;
1558
- }
1559
- //#endregion
1560
- //#region src/appearance/paper-size.ts
1561
- const PAPER_SIZES = {
1562
- a4: {
1563
- id: "a4",
1564
- label: "A4",
1565
- widthMm: 210,
1566
- heightMm: 297
1567
- },
1568
- a5: {
1569
- id: "a5",
1570
- label: "A5",
1571
- widthMm: 148,
1572
- heightMm: 210
1573
- },
1574
- a6: {
1575
- id: "a6",
1576
- label: "A6(文庫)",
1577
- widthMm: 105,
1578
- heightMm: 148
1579
- },
1580
- "jis-b5": {
1581
- id: "jis-b5",
1582
- label: "B5(JIS)",
1583
- widthMm: 182,
1584
- heightMm: 257
1585
- },
1586
- "jis-b6": {
1587
- id: "jis-b6",
1588
- label: "B6(JIS)",
1589
- widthMm: 128,
1590
- heightMm: 182
1591
- },
1592
- shinsho: {
1593
- id: "shinsho",
1594
- label: "新書",
1595
- widthMm: 106,
1596
- heightMm: 173
1597
- }
1598
- };
1599
- const PaperSize = {
1600
- of: (id) => PAPER_SIZES[id],
1601
- all: Object.values(PAPER_SIZES)
1602
- };
1603
- //#endregion
1604
- //#region src/internal/length.ts
1605
- const PT_PER_MM = 72 / 25.4;
1606
- function ptToMm(pt) {
1607
- return pt / PT_PER_MM;
1608
- }
1609
- function mmToPt(mm) {
1610
- return mm * PT_PER_MM;
1611
- }
1612
- //#endregion
1613
- //#region src/composer/manuscript-geometry.ts
1614
- const STAGE_GAP_CELLS = 2;
1615
- const LINE_GAP_CELLS = .5;
1616
- const nonNegativeLength = () => v.pipe(v.number(), v.finite(), v.minValue(0));
1617
- const ManuscriptGeometrySchema = readonlyObject({
1618
- paperWidthMm: nonNegativeLength(),
1619
- paperHeightMm: nonNegativeLength(),
1620
- cellSizeMm: nonNegativeLength(),
1621
- lineGapMm: nonNegativeLength(),
1622
- fontSizePt: FontSizePt.schema,
1623
- contentWidthMm: nonNegativeLength(),
1624
- contentHeightMm: nonNegativeLength(),
1625
- marginInlineMm: v.pipe(v.number(), v.finite()),
1626
- marginBlockMm: v.pipe(v.number(), v.finite()),
1627
- fitsPaper: v.boolean()
1628
- });
1629
- /**
1630
- * Every stage stacked vertically, plus the gap between adjacent stages.
1631
- */
1632
- function contentHeightInEm(flow) {
1633
- return flow.stagesPerPage * flow.lineLengthEm + STAGE_GAP_CELLS * (flow.stagesPerPage - 1);
1634
- }
1635
- /**
1636
- * Every vertical line side by side, plus the gap between adjacent lines.
1637
- */
1638
- function contentWidthInEm(flow) {
1639
- return flow.linesPerStage + LINE_GAP_CELLS * (flow.linesPerStage - 1);
1640
- }
1641
- const ManuscriptGeometry = {
1642
- schema: ManuscriptGeometrySchema,
1643
- of: (flow, appearance) => {
1644
- const paper = PaperSize.of(appearance.paperSize);
1645
- const cellSizeMm = ptToMm(appearance.fontSizePt);
1646
- const contentWidthMm = contentWidthInEm(flow) * cellSizeMm;
1647
- const contentHeightMm = contentHeightInEm(flow) * cellSizeMm;
1648
- const marginInlineMm = (paper.widthMm - contentWidthMm) / 2;
1649
- const marginBlockMm = (paper.heightMm - contentHeightMm) / 2;
1650
- return {
1651
- paperWidthMm: paper.widthMm,
1652
- paperHeightMm: paper.heightMm,
1653
- cellSizeMm,
1654
- lineGapMm: LINE_GAP_CELLS * cellSizeMm,
1655
- fontSizePt: appearance.fontSizePt,
1656
- contentWidthMm,
1657
- contentHeightMm,
1658
- marginInlineMm,
1659
- marginBlockMm,
1660
- fitsPaper: marginInlineMm >= 0 && marginBlockMm >= 0
1661
- };
1662
- },
1663
- /**
1664
- * The largest step-aligned point size whose text area still fits the paper.
1665
- */
1666
- maxFontSizePt: (flow, paperSizeId) => {
1667
- const paper = PaperSize.of(paperSizeId);
1668
- const maxCellSizeMm = Math.min(paper.widthMm / contentWidthInEm(flow), paper.heightMm / contentHeightInEm(flow));
1669
- const steps = Math.floor(mmToPt(maxCellSizeMm) / FontSizePt.range.step + 1e-9);
1670
- const aligned = Math.round(steps * FontSizePt.range.step * 1e3) / 1e3;
1671
- return Math.min(FontSizePt.range.max, Math.max(FontSizePt.range.min, aligned));
1672
- }
1673
- };
1674
- const CompositionStatistics = { schema: readonlyObject({
1675
- chars: nonNegativeInteger(),
1676
- sourceLines: nonNegativeInteger(),
1677
- pages: nonNegativeInteger()
1678
- }) };
1679
- //#endregion
1680
- //#region src/composer/composed-annotation-fragment.ts
1681
- const emLength = () => v.pipe(v.number(), v.finite(), v.minValue(0));
1682
- const continuation = v.union([
1683
- v.literal("whole"),
1684
- v.literal("start"),
1685
- v.literal("middle"),
1686
- v.literal("end")
1687
- ]);
1688
- const PositionedReadingGraphemeSchema = readonlyObject({
1689
- value: v.string(),
1690
- offsetEm: v.pipe(v.number(), v.finite()),
1691
- advanceEm: emLength()
1692
- });
1693
- const common$1 = {
1694
- annotationRange: ManuscriptRange.schema,
1695
- fragmentRange: ManuscriptRange.schema,
1696
- continuation
1697
- };
1698
- const ComposedAnnotationFragment = { schema: v.variant("kind", [
1699
- readonlyObject({
1700
- kind: v.literal("bold"),
1701
- ...common$1
1702
- }),
1703
- readonlyObject({
1704
- kind: v.literal("italic"),
1705
- ...common$1
1706
- }),
1707
- readonlyObject({
1708
- kind: v.literal("emphasis"),
1709
- mark: v.string(),
1710
- ...common$1
1711
- }),
1712
- readonlyObject({
1713
- kind: v.literal("ruby"),
1714
- rubyKind: RubyKind.schema,
1715
- reading: v.string(),
1716
- baseOffsetEm: emLength(),
1717
- baseAdvanceEm: emLength(),
1718
- readingGraphemes: readonlyArray(PositionedReadingGraphemeSchema),
1719
- ...common$1
1720
- })
1721
- ]) };
1722
- const InlineSpan = { schema: readonlyObject({
1723
- offsetEm: v.pipe(v.number(), v.finite()),
1724
- advanceEm: v.pipe(v.number(), v.finite(), v.minValue(0))
1725
- }) };
1726
- //#endregion
1727
- //#region src/composer/vertical-text-presentation.ts
1728
- const common = { groupRange: ManuscriptRange.schema };
1729
- const VerticalTextPresentation = { schema: v.variant("kind", [
1730
- readonlyObject({
1731
- kind: v.literal("mixed"),
1732
- ...common
1733
- }),
1734
- readonlyObject({
1735
- kind: v.literal("upright"),
1736
- ...common
1737
- }),
1738
- readonlyObject({
1739
- kind: v.literal("sideways"),
1740
- ...common
1741
- }),
1742
- readonlyObject({
1743
- kind: v.literal("tate-chu-yoko"),
1744
- ...common
1745
- })
1746
- ]) };
1747
- //#endregion
1748
- //#region src/composer/composed-inline-item.ts
1749
- const signedEm = () => v.pipe(v.number(), v.finite());
1750
- const GlyphItemSchema = readonlyObject({
1751
- kind: v.literal("glyph"),
1752
- value: v.string(),
1753
- range: ManuscriptRange.schema,
1754
- layoutSpan: InlineSpan.schema,
1755
- renderSpan: InlineSpan.schema,
1756
- disposition: v.union([v.literal("placed"), v.literal("hanging")]),
1757
- presentation: VerticalTextPresentation.schema
1758
- });
1759
- const GeneratedGlueItemSchema = readonlyObject({
1760
- kind: v.literal("glue"),
1761
- origin: v.literal("generated"),
1762
- offsetEm: signedEm(),
1763
- widthEm: signedEm(),
1764
- naturalWidthEm: signedEm(),
1765
- adjustment: v.union([
1766
- v.literal("natural"),
1767
- v.literal("shrunk"),
1768
- v.literal("stretched")
1769
- ])
1770
- });
1771
- const SourceGlueItemSchema = readonlyObject({
1772
- kind: v.literal("glue"),
1773
- origin: v.literal("source"),
1774
- value: v.string(),
1775
- range: ManuscriptRange.schema,
1776
- offsetEm: signedEm(),
1777
- widthEm: signedEm(),
1778
- naturalWidthEm: signedEm(),
1779
- adjustment: v.union([
1780
- v.literal("natural"),
1781
- v.literal("shrunk"),
1782
- v.literal("stretched")
1783
- ])
1784
- });
1785
- const KernItemSchema = readonlyObject({
1786
- kind: v.literal("kern"),
1787
- offsetEm: signedEm(),
1788
- widthEm: signedEm()
1789
- });
1790
- const SuppressedItemSchema = readonlyObject({
1791
- kind: v.literal("suppressed"),
1792
- value: v.string(),
1793
- range: ManuscriptRange.schema,
1794
- reason: v.literal("question-or-exclamation-gap")
1795
- });
1796
- const ComposedInlineItem = { schema: v.union([
1797
- GlyphItemSchema,
1798
- GeneratedGlueItemSchema,
1799
- SourceGlueItemSchema,
1800
- KernItemSchema,
1801
- SuppressedItemSchema
1802
- ]) };
1803
- const LineBreakResult = { schema: v.variant("kind", [
1804
- readonlyObject({ kind: v.literal("natural") }),
1805
- readonlyObject({ kind: v.literal("shrunk") }),
1806
- readonlyObject({ kind: v.literal("stretched") }),
1807
- readonlyObject({ kind: v.literal("hanging") }),
1808
- readonlyObject({ kind: v.literal("forced") }),
1809
- readonlyObject({ kind: v.literal("paragraph-end") })
1810
- ]) };
1811
- const NovelLine = {
1812
- schema: readonlyObject({
1813
- range: v.nullable(ManuscriptRange.schema),
1814
- inlineSizeEm: v.pipe(v.number(), v.finite(), v.minValue(0)),
1815
- items: readonlyArray(ComposedInlineItem.schema),
1816
- break: LineBreakResult.schema,
1817
- annotations: readonlyArray(ComposedAnnotationFragment.schema)
1818
- }),
1819
- empty: () => ({
1820
- range: null,
1821
- inlineSizeEm: 0,
1822
- items: [],
1823
- break: { kind: "paragraph-end" },
1824
- annotations: []
1825
- })
1826
- };
1827
- const NovelStage = {
1828
- schema: readonlyObject({
1829
- range: v.nullable(ManuscriptRange.schema),
1830
- lines: readonlyArray(NovelLine.schema)
1831
- }),
1832
- of: (lines) => ({
1833
- lines,
1834
- range: ManuscriptRange.merge(lines.map(({ range }) => range))
1835
- })
1836
- };
1837
- const NovelPage = {
1838
- schema: readonlyObject({
1839
- range: v.nullable(ManuscriptRange.schema),
1840
- stages: readonlyArray(NovelStage.schema)
1841
- }),
1842
- of: (stages) => ({
1843
- stages,
1844
- range: ManuscriptRange.merge(stages.map(({ range }) => range))
1845
- })
1846
- };
1847
- const NovelLayout = { schema: readonlyObject({
1848
- pages: readonlyArray(NovelPage.schema),
1849
- geometry: ManuscriptGeometry.schema,
1850
- stats: CompositionStatistics.schema
1851
- }) };
1852
- //#endregion
1853
- //#region src/composer/novel-composer.ts
1854
- const COMPOSER_ID = NamespacedId.of("kg/novel");
1855
- const TYPESETTING_PROFILE = defaultJapaneseTypesettingProfile;
1856
- const ASCII_ALPHANUMERIC = /^[A-Za-z0-9]$/u;
1857
- const ASCII_TWO_DIGITS = /^[0-9]{2}$/u;
1858
- const UPRIGHT_LATIN_ABBREVIATION = /^(?:[A-Z]+|[A-Z][a-z]{1,2})$/u;
1859
- const FULLWIDTH_ALPHANUMERIC = /^[A-Za-z0-9]$/u;
1860
- function presentation(kind, graphemes) {
1861
- const groupRange = ManuscriptRange.merge(graphemes.map(({ range }) => range));
1862
- return groupRange === null ? void 0 : {
1863
- kind,
1864
- groupRange
1865
- };
1866
- }
1867
- function presentedSourceLine(sourceLine) {
1868
- const presented = [];
1869
- let cursor = 0;
1870
- while (cursor < sourceLine.length) {
1871
- const first = sourceLine[cursor];
1872
- if (first === void 0) break;
1873
- if (!ASCII_ALPHANUMERIC.test(first.value)) {
1874
- const itemPresentation = presentation(FULLWIDTH_ALPHANUMERIC.test(first.value) ? "upright" : "mixed", [first]);
1875
- if (itemPresentation !== void 0) presented.push({
1876
- grapheme: first,
1877
- presentation: itemPresentation
1878
- });
1879
- cursor += 1;
1880
- continue;
1881
- }
1882
- const start = cursor;
1883
- while (ASCII_ALPHANUMERIC.test(sourceLine[cursor]?.value ?? "")) cursor += 1;
1884
- const run = sourceLine.slice(start, cursor);
1885
- const text = run.map(({ value }) => value).join("");
1886
- const runPresentation = presentation(ASCII_TWO_DIGITS.test(text) ? "tate-chu-yoko" : text.length === 1 || UPRIGHT_LATIN_ABBREVIATION.test(text) ? "upright" : "sideways", run);
1887
- if (runPresentation !== void 0) presented.push(...run.map((grapheme) => ({
1888
- grapheme,
1889
- presentation: runPresentation
1890
- })));
1891
- }
1892
- return presented;
1893
- }
1894
- function displayedLines(graphemes) {
1895
- const lines = [[]];
1896
- let endedWithLineBreak = false;
1897
- for (const grapheme of graphemes) {
1898
- if (grapheme.value === "\n" || grapheme.value === "\r" || grapheme.value === "\r\n") {
1899
- lines.push([]);
1900
- endedWithLineBreak = true;
1901
- continue;
1902
- }
1903
- lines.at(-1)?.push(grapheme);
1904
- endedWithLineBreak = false;
1905
- }
1906
- if (lines.length > 1 && endedWithLineBreak) lines.pop();
1907
- return lines;
1908
- }
1909
- function validGapIndexes(sourceLine) {
1910
- const line = sourceLine.map(({ value }) => value).join("");
1911
- const gapStarts = new Set(questionOrExclamationSpacings(line).filter((spacing) => spacing.kind === "valid").map(({ gap }) => gap.start));
1912
- const indexes = /* @__PURE__ */ new Set();
1913
- let offset = 0;
1914
- for (const [index, grapheme] of sourceLine.entries()) {
1915
- if (gapStarts.has(offset)) indexes.add(index);
1916
- offset += grapheme.value.length;
1917
- }
1918
- return indexes;
1919
- }
1920
- function indicesInside(atoms, annotation) {
1921
- return atoms.flatMap(({ grapheme }, index) => ManuscriptRange.overlaps(grapheme.range, annotation.range) ? [index] : []);
1922
- }
1923
- function measure(measurer, text, role, settings, presentationKind) {
1924
- const context = {
1925
- text,
1926
- fontPreset: settings.appearance.fontPreset,
1927
- writingMode: "vertical-rl"
1928
- };
1929
- const measurement = role === "base" ? measurer({
1930
- ...context,
1931
- role,
1932
- presentation: presentationKind ?? "mixed"
1933
- }) : measurer({
1934
- ...context,
1935
- role
1936
- });
1937
- const parsed = v.safeParse(InlineMeasurement.schema, measurement);
1938
- if (!parsed.success) return void 0;
1939
- const { advanceEm } = parsed.output;
1940
- return advanceEm;
1941
- }
1942
- /**
1943
- * Widen a base character by `extraEm` so a reading longer than it has room, keeping the character's
1944
- * ink in the middle of the widened box.
1945
- *
1946
- * JLReq 3.3.6 sets such a reading solid and spends the surplus on the base instead: two units
1947
- * between the base characters for one unit before the first and after the last. Handing every base
1948
- * character an equal share of the surplus and centring it inside that share produces exactly those
1949
- * proportions, and it is also the 中付き position JLReq 3.3.5 asks of a mono ruby whose reading
1950
- * outruns its single base character.
1951
- */
1952
- function widenForReading(atom, extraEm) {
1953
- if (extraEm <= 0) return;
1954
- atom.boxAdvanceEm += extraEm;
1955
- atom.renderOffsetEm += extraEm / 2;
1956
- }
1957
- function measureSourceLine(sourceLine, annotations, settings, measurer) {
1958
- const mutable = presentedSourceLine(sourceLine).map(({ grapheme, presentation }) => {
1959
- const renderAdvanceEm = measure(measurer, grapheme.value, "base", settings, presentation.kind);
1960
- if (renderAdvanceEm === void 0) return void 0;
1961
- const characterClass = TYPESETTING_PROFILE.classify({
1962
- value: grapheme.value,
1963
- presentation: presentation.kind
1964
- });
1965
- const metrics = TYPESETTING_PROFILE.boxMetrics(characterClass, renderAdvanceEm);
1966
- return {
1967
- grapheme,
1968
- boxAdvanceEm: metrics.advanceEm,
1969
- renderAdvanceEm,
1970
- renderOffsetEm: metrics.renderOffsetEm,
1971
- presentation
1972
- };
1973
- });
1974
- if (mutable.some((atom) => atom === void 0)) return void 0;
1975
- const atoms = mutable.flatMap((atom) => atom === void 0 ? [] : [atom]);
1976
- for (const annotation of annotations) {
1977
- if (annotation.kind !== "ruby") continue;
1978
- const indexes = indicesInside(atoms, annotation);
1979
- if (indexes.length === 0) continue;
1980
- if ((annotation.reading.kind === "group" ? [annotation.reading.text] : annotation.reading.segments).some((text) => [...graphemeSegmenter.segment(text)].some(({ segment }) => measure(measurer, segment, "ruby", settings) === void 0))) return void 0;
1981
- if (annotation.reading.kind === "group") {
1982
- const readingAdvance = measure(measurer, annotation.reading.text, "ruby", settings);
1983
- if (readingAdvance === void 0) return void 0;
1984
- const baseAdvance = indexes.reduce((total, index) => total + (atoms[index]?.boxAdvanceEm ?? 0), 0);
1985
- const extra = Math.max(0, readingAdvance - baseAdvance) / indexes.length;
1986
- for (const index of indexes) {
1987
- const atom = atoms[index];
1988
- if (atom !== void 0) widenForReading(atom, extra);
1989
- }
1990
- continue;
1991
- }
1992
- for (const [segmentIndex, index] of indexes.entries()) {
1993
- const segment = annotation.reading.segments[segmentIndex];
1994
- const atom = atoms[index];
1995
- if (segment === void 0 || atom === void 0) continue;
1996
- const readingAdvance = measure(measurer, segment, "ruby", settings);
1997
- if (readingAdvance === void 0) return void 0;
1998
- widenForReading(atom, readingAdvance - atom.boxAdvanceEm);
1999
- }
2000
- }
2001
- return {
2002
- atoms,
2003
- suppressedIndexes: validGapIndexes(sourceLine)
2004
- };
2005
- }
2006
- function breakInsideFittableGroupRuby(atoms, annotations, boundary, lineLengthEm) {
2007
- const right = atoms[boundary];
2008
- if (right === void 0) return false;
2009
- const graphemeIndex = right.grapheme.range.graphemes.start;
2010
- return annotations.some((annotation) => {
2011
- if (annotation.kind !== "ruby" || annotation.reading.kind !== "group") return false;
2012
- if (graphemeIndex <= annotation.range.graphemes.start || graphemeIndex >= annotation.range.graphemes.end) return false;
2013
- return indicesInside(atoms, annotation).reduce((total, index) => total + (atoms[index]?.boxAdvanceEm ?? 0), 0) <= lineLengthEm;
2014
- });
2015
- }
2016
- function samePresentationGroup(left, right) {
2017
- return left.presentation.groupRange.graphemes.start === right.presentation.groupRange.graphemes.start && left.presentation.groupRange.graphemes.end === right.presentation.groupRange.graphemes.end;
2018
- }
2019
- function legalBoundary(atoms, annotations, leftIndex, rightIndex, lineLengthEm, profile) {
2020
- const left = atoms[leftIndex];
2021
- const right = atoms[rightIndex];
2022
- if (left === void 0 || right === void 0) return true;
2023
- return !samePresentationGroup(left, right) && profile.breakPenalty(profile.classify({
2024
- value: left.grapheme.value,
2025
- presentation: left.presentation.kind
2026
- }), profile.classify({
2027
- value: right.grapheme.value,
2028
- presentation: right.presentation.kind
2029
- })) !== null && !breakInsideFittableGroupRuby(atoms, annotations, rightIndex, lineLengthEm);
2030
- }
2031
- function continuationFor(annotation, range) {
2032
- const starts = range.graphemes.start === annotation.range.graphemes.start;
2033
- const ends = range.graphemes.end === annotation.range.graphemes.end;
2034
- if (starts && ends) return "whole";
2035
- if (starts) return "start";
2036
- if (ends) return "end";
2037
- return "middle";
2038
- }
2039
- function groupReadingsForFragments(annotation, fragmentRanges, baseAdvances) {
2040
- const baseLength = annotation.range.graphemes.end - annotation.range.graphemes.start;
2041
- if (annotation.reading.kind !== "group") return [];
2042
- const reading = [...graphemeSegmenter.segment(annotation.reading.text)].map(({ segment }) => segment);
2043
- const advances = Array.from({ length: baseLength }, (_, index) => baseAdvances.get(annotation.range.graphemes.start + index));
2044
- const measured = advances.every((advance) => advance !== void 0);
2045
- const advanceFor = (fragment) => {
2046
- const fragmentStart = fragment.graphemes.start - annotation.range.graphemes.start;
2047
- const fragmentLength = fragment.graphemes.end - fragment.graphemes.start;
2048
- return measured ? advances.slice(fragmentStart, fragmentStart + fragmentLength).reduce((total, advance) => total + advance, 0) : fragmentLength;
2049
- };
2050
- const weights = fragmentRanges.map(advanceFor);
2051
- const effectiveWeights = weights.reduce((total, weight) => total + weight, 0) > 0 ? weights : fragmentRanges.map(({ graphemes }) => graphemes.end - graphemes.start);
2052
- const totalWeight = effectiveWeights.reduce((total, weight) => total + weight, 0);
2053
- const reserveEveryFragment = reading.length >= fragmentRanges.length;
2054
- const fragments = [];
2055
- let readingStart = 0;
2056
- let accumulatedWeight = 0;
2057
- for (const [index, weight] of effectiveWeights.entries()) {
2058
- accumulatedWeight += weight;
2059
- const remainingFragments = effectiveWeights.length - index - 1;
2060
- const proportionalEnd = index === effectiveWeights.length - 1 ? reading.length : Math.round(accumulatedWeight * reading.length / totalWeight);
2061
- const minimumEnd = reserveEveryFragment ? readingStart + 1 : readingStart;
2062
- const maximumEnd = reserveEveryFragment ? reading.length - remainingFragments : reading.length;
2063
- const readingEnd = Math.min(maximumEnd, Math.max(minimumEnd, proportionalEnd));
2064
- fragments.push(reading.slice(readingStart, readingEnd).join(""));
2065
- readingStart = readingEnd;
2066
- }
2067
- return fragments;
2068
- }
2069
- function readingForFragment(annotation, range, fragmentPlan) {
2070
- const start = range.graphemes.start - annotation.range.graphemes.start;
2071
- const length = range.graphemes.end - range.graphemes.start;
2072
- if (annotation.reading.kind !== "group") return {
2073
- kind: annotation.reading.kind,
2074
- text: annotation.reading.segments.slice(start, start + length).join("")
2075
- };
2076
- const fragmentIndex = fragmentPlan.ranges.findIndex(({ graphemes }) => graphemes.start === range.graphemes.start && graphemes.end === range.graphemes.end);
2077
- return {
2078
- kind: "group",
2079
- text: fragmentPlan.groupReadings[fragmentIndex] ?? ""
2080
- };
2081
- }
2082
- function positionReading(text, baseOffsetEm, baseAdvanceEm, settings, measurer) {
2083
- const measured = [...graphemeSegmenter.segment(text)].map(({ segment }) => ({
2084
- value: segment,
2085
- advanceEm: measure(measurer, segment, "ruby", settings) ?? 0
2086
- }));
2087
- const total = measured.reduce((sum, grapheme) => sum + grapheme.advanceEm, 0);
2088
- const edgeGap = measured.length > 0 && total < baseAdvanceEm ? (baseAdvanceEm - total) / (2 * measured.length) : 0;
2089
- let offsetEm = baseOffsetEm + (total > baseAdvanceEm ? (baseAdvanceEm - total) / 2 : edgeGap);
2090
- return measured.map((grapheme, index) => {
2091
- const positioned = {
2092
- value: grapheme.value,
2093
- advanceEm: grapheme.advanceEm,
2094
- offsetEm
2095
- };
2096
- offsetEm += grapheme.advanceEm;
2097
- if (index < measured.length - 1) offsetEm += edgeGap * 2;
2098
- return positioned;
2099
- });
2100
- }
2101
- function annotationFragmentRange(graphemes, suppressed, sourceGlues, annotation) {
2102
- return ManuscriptRange.merge([
2103
- ...graphemes.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range),
2104
- ...suppressed.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range),
2105
- ...sourceGlues.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range)
2106
- ]);
2107
- }
2108
- function annotationFragments(graphemes, suppressed, sourceGlues, annotations, settings, measurer, fragmentPlansByAnnotation) {
2109
- const fragments = [];
2110
- for (const annotation of annotations) {
2111
- const covered = graphemes.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range));
2112
- const fragmentRange = annotationFragmentRange(graphemes, suppressed, sourceGlues, annotation);
2113
- const first = covered[0];
2114
- const last = covered.at(-1);
2115
- if (fragmentRange === null || first === void 0 || last === void 0) continue;
2116
- const common = {
2117
- annotationRange: annotation.range,
2118
- fragmentRange,
2119
- continuation: continuationFor(annotation, fragmentRange)
2120
- };
2121
- switch (annotation.kind) {
2122
- case "bold":
2123
- case "italic":
2124
- fragments.push({
2125
- kind: annotation.kind,
2126
- ...common
2127
- });
2128
- break;
2129
- case "emphasis":
2130
- fragments.push({
2131
- kind: "emphasis",
2132
- mark: annotation.mark,
2133
- ...common
2134
- });
2135
- break;
2136
- case "ruby": {
2137
- const fragmentPlan = fragmentPlansByAnnotation.get(annotation);
2138
- if (fragmentPlan === void 0) break;
2139
- const baseOffsetEm = first.layoutSpan.offsetEm;
2140
- const baseAdvanceEm = last.layoutSpan.offsetEm + last.layoutSpan.advanceEm - baseOffsetEm;
2141
- const reading = readingForFragment(annotation, fragmentRange, fragmentPlan);
2142
- fragments.push({
2143
- kind: "ruby",
2144
- rubyKind: reading.kind,
2145
- reading: reading.text,
2146
- baseOffsetEm,
2147
- baseAdvanceEm,
2148
- readingGraphemes: positionReading(reading.text, baseOffsetEm, baseAdvanceEm, settings, measurer),
2149
- ...common
2150
- });
2151
- break;
2152
- }
2153
- }
2154
- }
2155
- return fragments;
2156
- }
2157
- function positionedLine(atoms, sourceGapIndexes, plan) {
2158
- const items = plan.suppressedIndexes.flatMap((index) => {
2159
- const atom = atoms[index];
2160
- return atom === void 0 ? [] : [{
2161
- kind: "suppressed",
2162
- value: atom.grapheme.value,
2163
- range: atom.grapheme.range,
2164
- reason: "question-or-exclamation-gap"
2165
- }];
2166
- });
2167
- const spacings = new Map(plan.pairSpacings.map((spacing) => [spacing.boundary, spacing]));
2168
- const characterSpacings = new Map(plan.characterSpacings.map((spacing) => [spacing.index, spacing]));
2169
- let offsetEm = 0;
2170
- const adjustmentOf = (widthEm, naturalWidthEm) => widthEm < naturalWidthEm ? "shrunk" : widthEm > naturalWidthEm ? "stretched" : "natural";
2171
- const positionSpacing = (spacing) => {
2172
- if (spacing.kind === "kern") items.push({
2173
- kind: "kern",
2174
- offsetEm,
2175
- widthEm: spacing.widthEm
2176
- });
2177
- else items.push({
2178
- kind: "glue",
2179
- origin: "generated",
2180
- offsetEm,
2181
- widthEm: spacing.widthEm,
2182
- naturalWidthEm: spacing.naturalWidthEm,
2183
- adjustment: adjustmentOf(spacing.widthEm, spacing.naturalWidthEm)
2184
- });
2185
- offsetEm += spacing.widthEm;
2186
- };
2187
- for (let index = plan.contentStart; index < plan.end; index += 1) {
2188
- const spacing = spacings.get(index);
2189
- if (spacing !== void 0) positionSpacing(spacing);
2190
- const atom = atoms[index];
2191
- if (atom === void 0) continue;
2192
- const characterSpacing = characterSpacings.get(index);
2193
- if (sourceGapIndexes.has(index) || characterSpacing !== void 0) {
2194
- const naturalWidthEm = characterSpacing?.naturalWidthEm ?? atom.boxAdvanceEm;
2195
- const widthEm = characterSpacing?.widthEm ?? atom.boxAdvanceEm;
2196
- items.push({
2197
- kind: "glue",
2198
- origin: "source",
2199
- value: atom.grapheme.value,
2200
- range: atom.grapheme.range,
2201
- offsetEm,
2202
- widthEm,
2203
- naturalWidthEm,
2204
- adjustment: adjustmentOf(widthEm, naturalWidthEm)
2205
- });
2206
- offsetEm += widthEm;
2207
- continue;
2208
- }
2209
- const hanging = plan.hangingIndex === index;
2210
- const layoutAdvanceEm = hanging ? 0 : atom.boxAdvanceEm;
2211
- items.push({
2212
- kind: "glyph",
2213
- value: atom.grapheme.value,
2214
- range: atom.grapheme.range,
2215
- layoutSpan: {
2216
- offsetEm,
2217
- advanceEm: layoutAdvanceEm
2218
- },
2219
- renderSpan: {
2220
- offsetEm: offsetEm + atom.renderOffsetEm,
2221
- advanceEm: atom.renderAdvanceEm
2222
- },
2223
- disposition: hanging ? "hanging" : "placed",
2224
- presentation: atom.presentation
2225
- });
2226
- offsetEm += layoutAdvanceEm;
2227
- }
2228
- const trailingSpacing = spacings.get(plan.end);
2229
- if (trailingSpacing !== void 0) positionSpacing(trailingSpacing);
2230
- const ranges = items.flatMap((item) => item.kind === "glyph" || item.kind === "suppressed" || item.kind === "glue" && item.origin === "source" ? [item.range] : []);
2231
- return {
2232
- range: ManuscriptRange.merge(ranges),
2233
- inlineSizeEm: Math.max(0, plan.inlineSizeEm),
2234
- items,
2235
- break: plan.break,
2236
- annotations: []
2237
- };
2238
- }
2239
- function wrapSourceLine(sourceLine, annotations, settings, measurer, baseAdvances) {
2240
- if (sourceLine.atoms.length === 0) return [NovelLine.empty()];
2241
- const profile = TYPESETTING_PROFILE;
2242
- const lines = layoutParagraph(sourceLine.atoms.map(({ grapheme, boxAdvanceEm, presentation }, index) => {
2243
- const current = sourceLine.atoms[index];
2244
- const next = sourceLine.atoms[index + 1];
2245
- return {
2246
- value: grapheme.value,
2247
- boxAdvanceEm,
2248
- sourceGap: sourceLine.suppressedIndexes.has(index),
2249
- characterClass: profile.classify({
2250
- value: grapheme.value,
2251
- presentation: presentation.kind
2252
- }),
2253
- pairSpacingAfter: current === void 0 || next === void 0 || !samePresentationGroup(current, next) && !breakInsideFittableGroupRuby(sourceLine.atoms, annotations, index + 1, settings.flow.lineLengthEm)
2254
- };
2255
- }), settings.flow.lineLengthEm, profile, (leftIndex, rightIndex) => legalBoundary(sourceLine.atoms, annotations, leftIndex, rightIndex, settings.flow.lineLengthEm, profile)).map((plan) => positionedLine(sourceLine.atoms, sourceLine.suppressedIndexes, plan));
2256
- const fragmentPlansByAnnotation = new Map(annotations.map((annotation) => {
2257
- const ranges = lines.flatMap((line) => {
2258
- const range = annotationFragmentRange(line.items.filter((item) => item.kind === "glyph"), line.items.filter((item) => item.kind === "suppressed"), line.items.flatMap((item) => item.kind === "glue" && item.origin === "source" ? [item] : []), annotation);
2259
- return range === null ? [] : [range];
2260
- });
2261
- return [annotation, {
2262
- ranges,
2263
- groupReadings: annotation.kind === "ruby" ? groupReadingsForFragments(annotation, ranges, baseAdvances) : []
2264
- }];
2265
- }));
2266
- return lines.map((line) => ({
2267
- range: line.range,
2268
- inlineSizeEm: line.inlineSizeEm,
2269
- items: line.items,
2270
- break: line.break,
2271
- annotations: annotationFragments(line.items.filter((item) => item.kind === "glyph"), line.items.filter((item) => item.kind === "suppressed"), line.items.flatMap((item) => item.kind === "glue" && item.origin === "source" ? [item] : []), annotations, settings, measurer, fragmentPlansByAnnotation)
2272
- }));
2273
- }
2274
- function blankLines(count) {
2275
- return Array.from({ length: count }, NovelLine.empty);
2276
- }
2277
- function buildPages(contentLines, settings) {
2278
- const { linesPerStage, stagesPerPage } = settings.flow;
2279
- const { stage: stageOffset, page: pageOffset } = settings.offsets;
2280
- const usablePerStage = linesPerStage - LineOffset.total(stageOffset);
2281
- const usablePerPage = usablePerStage * stagesPerPage;
2282
- const pages = [];
2283
- let contentIndex = 0;
2284
- while (contentIndex < contentLines.length || pages.length === 0) {
2285
- const stages = [];
2286
- for (let stageIndex = 0; stageIndex < stagesPerPage; stageIndex += 1) {
2287
- const lines = [];
2288
- for (let position = 0; position < linesPerStage; position += 1) {
2289
- const usableIndex = stageIndex * usablePerStage + position - stageOffset.leading;
2290
- const withinStage = position >= stageOffset.leading && position < linesPerStage - stageOffset.trailing;
2291
- const withinPage = usableIndex >= pageOffset.leading && usableIndex < usablePerPage - pageOffset.trailing;
2292
- const next = contentLines[contentIndex];
2293
- if (!withinStage || !withinPage || next === void 0) {
2294
- lines.push(NovelLine.empty());
2295
- continue;
2296
- }
2297
- lines.push(next);
2298
- contentIndex += 1;
2299
- }
2300
- stages.push(NovelStage.of(lines));
2301
- }
2302
- pages.push(NovelPage.of(stages));
2303
- }
2304
- return pages;
2305
- }
2306
- function createCompose(measurer) {
2307
- return (manuscript, settings) => {
2308
- const sourceLines = displayedLines(manuscript.graphemes);
2309
- const measured = sourceLines.map((line) => measureSourceLine(line, manuscript.annotations, settings, measurer));
2310
- if (measured.some((line) => line === void 0)) return ManuscriptResult.fail({ reason: "inline measurer returned a negative or non-finite value" });
2311
- const baseAdvances = /* @__PURE__ */ new Map();
2312
- for (const line of measured) for (const atom of line?.atoms ?? []) baseAdvances.set(atom.grapheme.range.graphemes.start, atom.boxAdvanceEm);
2313
- const manuscriptLines = measured.flatMap((line) => line === void 0 ? [] : wrapSourceLine(line, manuscript.annotations, settings, measurer, baseAdvances));
2314
- const pages = buildPages([
2315
- ...blankLines(settings.offsets.document.leading),
2316
- ...manuscriptLines,
2317
- ...blankLines(settings.offsets.document.trailing)
2318
- ], settings);
2319
- return ManuscriptResult.succeed({
2320
- pages,
2321
- geometry: ManuscriptGeometry.of(settings.flow, settings.appearance),
2322
- stats: {
2323
- chars: sourceLines.reduce((total, line) => total + line.length, 0),
2324
- sourceLines: sourceLines.length,
2325
- pages: pages.length
2326
- }
2327
- });
2328
- };
2329
- }
2330
- function createNovelComposer(options) {
2331
- return {
2332
- id: COMPOSER_ID,
2333
- settingsSchema: NovelCompositionSettings.schema,
2334
- layoutSchema: NovelLayout.schema,
2335
- compose: createCompose(options.measurer)
2336
- };
2337
- }
2338
- const novelComposer = createNovelComposer({ measurer: logicalInlineMeasurer });
2339
- //#endregion
2340
- export { logicalInlineMeasurer as a, ManuscriptOffsets as c, FontSizePt as d, FontPresetId as f, PaperSize as i, ManuscriptAppearanceSettings as l, novelComposer as n, NovelCompositionSettings as o, ParsedManuscript as p, ManuscriptGeometry as r, NovelFlowSettings as s, createNovelComposer as t, PaperSizeId as u };