@sushichan044/kg-core 0.9.5 → 0.10.1

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