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