@sushichan044/kg-core 0.6.5 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -13
- package/dist/composed-manuscript-QgIdae8p.d.mts +1740 -0
- package/dist/describe-cause-BV8mZ3Um.mjs +18 -0
- package/dist/index.d.mts +6481 -18298
- package/dist/index.mjs +50 -2475
- package/dist/lint.d.mts +461 -0
- package/dist/lint.mjs +812 -0
- package/dist/novel-composer-BCt9g172.d.mts +9781 -0
- package/dist/novel-composer-CROZLBqt.mjs +1929 -0
- package/dist/plugin.d.mts +3 -0
- package/dist/plugin.mjs +3 -0
- package/dist/question-or-exclamation-spacing-B5o23ZLf.mjs +251 -0
- package/package.json +11 -4
|
@@ -0,0 +1,1929 @@
|
|
|
1
|
+
import { c as ManuscriptResult, d as NamespacedId, f as ManuscriptRange, h as readonlyObject, i as OPENING_BRACKETS, m as readonlyArray, o as graphemeSegmenter, p as nonNegativeInteger, r as CLOSING_BRACKETS, t as questionOrExclamationSpacings } from "./question-or-exclamation-spacing-B5o23ZLf.mjs";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
import { eastAsianWidth } from "get-east-asian-width";
|
|
4
|
+
const BoldAnnotation = { schema: readonlyObject({
|
|
5
|
+
kind: v.literal("bold"),
|
|
6
|
+
range: ManuscriptRange.schema
|
|
7
|
+
}) };
|
|
8
|
+
const EmphasisAnnotation = { schema: readonlyObject({
|
|
9
|
+
kind: v.literal("emphasis"),
|
|
10
|
+
range: ManuscriptRange.schema,
|
|
11
|
+
mark: v.string()
|
|
12
|
+
}) };
|
|
13
|
+
const ItalicAnnotation = { schema: readonlyObject({
|
|
14
|
+
kind: v.literal("italic"),
|
|
15
|
+
range: ManuscriptRange.schema
|
|
16
|
+
}) };
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/parser/annotation/ruby-annotation.ts
|
|
19
|
+
const GroupRubyKindSchema = v.literal("group");
|
|
20
|
+
const MonoRubyKindSchema = v.literal("mono");
|
|
21
|
+
const JukugoRubyKindSchema = v.literal("jukugo");
|
|
22
|
+
const RubyKindSchema = v.union([
|
|
23
|
+
GroupRubyKindSchema,
|
|
24
|
+
MonoRubyKindSchema,
|
|
25
|
+
JukugoRubyKindSchema
|
|
26
|
+
]);
|
|
27
|
+
const RubyReadingSchema = v.variant("kind", [
|
|
28
|
+
readonlyObject({
|
|
29
|
+
kind: GroupRubyKindSchema,
|
|
30
|
+
text: v.pipe(v.string(), v.nonEmpty())
|
|
31
|
+
}),
|
|
32
|
+
readonlyObject({
|
|
33
|
+
kind: MonoRubyKindSchema,
|
|
34
|
+
segments: v.pipe(v.array(v.pipe(v.string(), v.nonEmpty())), v.nonEmpty(), v.readonly())
|
|
35
|
+
}),
|
|
36
|
+
readonlyObject({
|
|
37
|
+
kind: JukugoRubyKindSchema,
|
|
38
|
+
segments: v.pipe(v.array(v.pipe(v.string(), v.nonEmpty())), v.nonEmpty(), v.readonly())
|
|
39
|
+
})
|
|
40
|
+
]);
|
|
41
|
+
const RubyAnnotationSchema = readonlyObject({
|
|
42
|
+
kind: v.literal("ruby"),
|
|
43
|
+
range: ManuscriptRange.schema,
|
|
44
|
+
reading: RubyReadingSchema
|
|
45
|
+
});
|
|
46
|
+
const RubyKind = { schema: RubyKindSchema };
|
|
47
|
+
const RubyAnnotation = { schema: RubyAnnotationSchema };
|
|
48
|
+
const ManuscriptAnnotation = {
|
|
49
|
+
schema: v.variant("kind", [
|
|
50
|
+
RubyAnnotation.schema,
|
|
51
|
+
BoldAnnotation.schema,
|
|
52
|
+
ItalicAnnotation.schema,
|
|
53
|
+
EmphasisAnnotation.schema
|
|
54
|
+
]),
|
|
55
|
+
/**
|
|
56
|
+
* Stable order for rendering: outermost span first, then by variant name.
|
|
57
|
+
*/
|
|
58
|
+
compare: (left, right) => left.range.source.start - right.range.source.start || left.range.source.end - right.range.source.end || left.kind.localeCompare(right.kind)
|
|
59
|
+
};
|
|
60
|
+
const ParsedGrapheme = { schema: readonlyObject({
|
|
61
|
+
value: v.string(),
|
|
62
|
+
range: ManuscriptRange.schema
|
|
63
|
+
}) };
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/parser/parsed-manuscript.ts
|
|
66
|
+
const ParsedManuscriptSchema = readonlyObject({
|
|
67
|
+
source: v.string(),
|
|
68
|
+
displayText: v.string(),
|
|
69
|
+
graphemes: readonlyArray(ParsedGrapheme.schema),
|
|
70
|
+
annotations: readonlyArray(ManuscriptAnnotation.schema)
|
|
71
|
+
});
|
|
72
|
+
function rangeFits(range, manuscript) {
|
|
73
|
+
return range.source.end <= manuscript.source.length && range.display.end <= manuscript.displayText.length && range.graphemes.end <= manuscript.graphemes.length;
|
|
74
|
+
}
|
|
75
|
+
function hasValidRubyAssociations(manuscript) {
|
|
76
|
+
const ruby = manuscript.annotations.filter((annotation) => annotation.kind === "ruby");
|
|
77
|
+
return ruby.every((annotation, index) => {
|
|
78
|
+
const baseLength = annotation.range.graphemes.end - annotation.range.graphemes.start;
|
|
79
|
+
if (baseLength <= 0) return false;
|
|
80
|
+
if (annotation.reading.kind !== "group" && annotation.reading.segments.length !== baseLength) return false;
|
|
81
|
+
return ruby.slice(index + 1).every((other) => !ManuscriptRange.overlaps(annotation.range, other.range));
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function isConsistent(manuscript, source) {
|
|
85
|
+
const joined = manuscript.graphemes.map(({ value }) => value).join("");
|
|
86
|
+
const sequential = manuscript.graphemes.every(({ range }, index) => range.graphemes.start === index && range.graphemes.end === index + 1 && rangeFits(range, manuscript));
|
|
87
|
+
return manuscript.source === source && manuscript.displayText === joined && sequential && manuscript.annotations.every(({ range }) => rangeFits(range, manuscript)) && hasValidRubyAssociations(manuscript);
|
|
88
|
+
}
|
|
89
|
+
const CONTRACT_MESSAGE = "parser output must contain consistent source, display, grapheme, and annotation ranges and valid ruby associations";
|
|
90
|
+
const ParsedManuscript = {
|
|
91
|
+
schema: ParsedManuscriptSchema,
|
|
92
|
+
/**
|
|
93
|
+
* The parser contract as a schema: shape, plus the cross-field invariants that make the three
|
|
94
|
+
* coordinate systems line up. Annotations come out in a stable order regardless of parser.
|
|
95
|
+
*/
|
|
96
|
+
contractFor: (source) => v.pipe(ParsedManuscriptSchema, v.check((manuscript) => isConsistent(manuscript, source), CONTRACT_MESSAGE), v.transform((manuscript) => ({
|
|
97
|
+
...manuscript,
|
|
98
|
+
annotations: [...manuscript.annotations].sort(ManuscriptAnnotation.compare)
|
|
99
|
+
})))
|
|
100
|
+
};
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/appearance/font-preset-id.ts
|
|
103
|
+
const FontPresetIdSchema = v.picklist(["mincho", "gothic"]);
|
|
104
|
+
const FontPresetId = {
|
|
105
|
+
schema: FontPresetIdSchema,
|
|
106
|
+
is: (value) => v.is(FontPresetIdSchema, value)
|
|
107
|
+
};
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/appearance/font-size-pt.ts
|
|
110
|
+
const RANGE = {
|
|
111
|
+
min: 6,
|
|
112
|
+
max: 24,
|
|
113
|
+
step: .5
|
|
114
|
+
};
|
|
115
|
+
const FontSizePtSchema = v.pipe(v.number(), v.finite(), v.minValue(RANGE.min), v.maxValue(RANGE.max), v.multipleOf(RANGE.step));
|
|
116
|
+
const FontSizePt = {
|
|
117
|
+
schema: FontSizePtSchema,
|
|
118
|
+
range: RANGE,
|
|
119
|
+
is: (value) => v.is(FontSizePtSchema, value)
|
|
120
|
+
};
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/appearance/paper-size-id.ts
|
|
123
|
+
const PaperSizeIdSchema = v.picklist([
|
|
124
|
+
"a4",
|
|
125
|
+
"a5",
|
|
126
|
+
"a6",
|
|
127
|
+
"jis-b5",
|
|
128
|
+
"jis-b6",
|
|
129
|
+
"shinsho"
|
|
130
|
+
]);
|
|
131
|
+
const PaperSizeId = {
|
|
132
|
+
schema: PaperSizeIdSchema,
|
|
133
|
+
is: (value) => v.is(PaperSizeIdSchema, value)
|
|
134
|
+
};
|
|
135
|
+
const ManuscriptAppearanceSettings = {
|
|
136
|
+
schema: readonlyObject({
|
|
137
|
+
paperSize: PaperSizeId.schema,
|
|
138
|
+
fontSizePt: FontSizePt.schema,
|
|
139
|
+
fontPreset: FontPresetId.schema
|
|
140
|
+
}),
|
|
141
|
+
defaults: {
|
|
142
|
+
paperSize: "a5",
|
|
143
|
+
fontSizePt: 9,
|
|
144
|
+
fontPreset: "mincho"
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const LineOffset = {
|
|
148
|
+
schema: readonlyObject({
|
|
149
|
+
leading: nonNegativeInteger(),
|
|
150
|
+
trailing: nonNegativeInteger()
|
|
151
|
+
}),
|
|
152
|
+
total: (offset) => offset.leading + offset.trailing
|
|
153
|
+
};
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/composer/manuscript-offsets.ts
|
|
156
|
+
const MAX_DOCUMENT_OFFSET = 1e4;
|
|
157
|
+
const ManuscriptOffsets = {
|
|
158
|
+
schema: readonlyObject({
|
|
159
|
+
document: readonlyObject({
|
|
160
|
+
leading: nonNegativeInteger(MAX_DOCUMENT_OFFSET),
|
|
161
|
+
trailing: nonNegativeInteger(MAX_DOCUMENT_OFFSET)
|
|
162
|
+
}),
|
|
163
|
+
page: LineOffset.schema,
|
|
164
|
+
stage: LineOffset.schema
|
|
165
|
+
}),
|
|
166
|
+
defaults: {
|
|
167
|
+
document: {
|
|
168
|
+
leading: 0,
|
|
169
|
+
trailing: 0
|
|
170
|
+
},
|
|
171
|
+
page: {
|
|
172
|
+
leading: 0,
|
|
173
|
+
trailing: 0
|
|
174
|
+
},
|
|
175
|
+
stage: {
|
|
176
|
+
leading: 0,
|
|
177
|
+
trailing: 0
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
/**
|
|
181
|
+
* Stage offsets must leave at least one usable line in the stage.
|
|
182
|
+
*/
|
|
183
|
+
maxStageTotal: (flow) => Math.max(0, flow.linesPerStage - 1),
|
|
184
|
+
/**
|
|
185
|
+
* Page offsets must leave at least one usable line on the page, after stage offsets apply.
|
|
186
|
+
*/
|
|
187
|
+
maxPageTotal: (flow, stage) => Math.max(0, (flow.linesPerStage - LineOffset.total(stage)) * flow.stagesPerPage - 1)
|
|
188
|
+
};
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/composer/novel-flow-settings.ts
|
|
191
|
+
const RANGES = {
|
|
192
|
+
lineLengthEm: {
|
|
193
|
+
min: 10,
|
|
194
|
+
max: 60
|
|
195
|
+
},
|
|
196
|
+
linesPerStage: {
|
|
197
|
+
min: 10,
|
|
198
|
+
max: 60
|
|
199
|
+
},
|
|
200
|
+
stagesPerPage: {
|
|
201
|
+
min: 1,
|
|
202
|
+
max: 3
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const boundedCount = (bounds) => v.pipe(v.number(), v.finite(), v.integer(), v.minValue(bounds.min), v.maxValue(bounds.max));
|
|
206
|
+
const NovelFlowSettings = {
|
|
207
|
+
schema: readonlyObject({
|
|
208
|
+
lineLengthEm: boundedCount(RANGES.lineLengthEm),
|
|
209
|
+
linesPerStage: boundedCount(RANGES.linesPerStage),
|
|
210
|
+
stagesPerPage: boundedCount(RANGES.stagesPerPage)
|
|
211
|
+
}),
|
|
212
|
+
ranges: RANGES,
|
|
213
|
+
defaults: {
|
|
214
|
+
lineLengthEm: 27,
|
|
215
|
+
linesPerStage: 23,
|
|
216
|
+
stagesPerPage: 2
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
//#endregion
|
|
220
|
+
//#region src/composer/composition-settings.ts
|
|
221
|
+
const NovelCompositionSettingsSchema = v.pipe(readonlyObject({
|
|
222
|
+
flow: NovelFlowSettings.schema,
|
|
223
|
+
offsets: ManuscriptOffsets.schema,
|
|
224
|
+
appearance: ManuscriptAppearanceSettings.schema
|
|
225
|
+
}), v.check(({ flow, offsets }) => LineOffset.total(offsets.stage) <= ManuscriptOffsets.maxStageTotal(flow) && LineOffset.total(offsets.page) <= ManuscriptOffsets.maxPageTotal(flow, offsets.stage), "page or stage offsets leave no usable novel lines"));
|
|
226
|
+
const NovelCompositionSettings = {
|
|
227
|
+
schema: NovelCompositionSettingsSchema,
|
|
228
|
+
defaults: {
|
|
229
|
+
flow: NovelFlowSettings.defaults,
|
|
230
|
+
offsets: ManuscriptOffsets.defaults,
|
|
231
|
+
appearance: ManuscriptAppearanceSettings.defaults
|
|
232
|
+
},
|
|
233
|
+
is: (value) => v.is(NovelCompositionSettingsSchema, value)
|
|
234
|
+
};
|
|
235
|
+
const InlineMeasurement = { schema: readonlyObject({ advanceEm: v.pipe(v.number(), v.finite(), v.minValue(0)) }) };
|
|
236
|
+
function graphemeWidth(grapheme) {
|
|
237
|
+
let width = 1;
|
|
238
|
+
for (const character of grapheme) {
|
|
239
|
+
const codePoint = character.codePointAt(0);
|
|
240
|
+
if (codePoint !== void 0) width = Math.max(width, eastAsianWidth(codePoint, { ambiguousAsWide: true }));
|
|
241
|
+
}
|
|
242
|
+
return width / 2;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Deterministic Japanese-context measurement in logical em units.
|
|
246
|
+
*/
|
|
247
|
+
const logicalInlineMeasurer = (request) => {
|
|
248
|
+
const { text } = request;
|
|
249
|
+
const width = [...graphemeSegmenter.segment(text)].reduce((total, { segment }) => total + graphemeWidth(segment), 0);
|
|
250
|
+
if (request.role === "ruby") return { advanceEm: width / 2 };
|
|
251
|
+
return { advanceEm: request.presentation === "upright" ? [...graphemeSegmenter.segment(text)].length : width };
|
|
252
|
+
};
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/composer/internal/japanese-typesetting-profile.ts
|
|
255
|
+
/**
|
|
256
|
+
* The JLReq character classes (文字クラス) this profile distinguishes. The bare `cl-NN` identifiers are
|
|
257
|
+
* kept rather than descriptive names so the spacing and penalty tables below can be read side by
|
|
258
|
+
* side with the tables in the specification.
|
|
259
|
+
*
|
|
260
|
+
* JLReq defines thirty classes. Math (cl-17, cl-18) and warichu brackets (cl-28, cl-29) are absent
|
|
261
|
+
* because a novel is not set with them. The base-group classes are absent for a different reason: a
|
|
262
|
+
* novel does carry ruby (cl-22, cl-23), but ruby is an annotation the composer positions
|
|
263
|
+
* separately, and `JapaneseCharacter` cannot describe a base group at all — nor a reference mark
|
|
264
|
+
* (cl-20) or a subscripted base (cl-21). A base character is therefore classified here as whatever
|
|
265
|
+
* it is on its own, which for the kanji that usually carries ruby means cl-19.
|
|
266
|
+
*
|
|
267
|
+
* @see https://www.w3.org/TR/jlreq/#character_classes
|
|
268
|
+
*/
|
|
269
|
+
const japaneseCharacterClasses = [
|
|
270
|
+
"cl-01",
|
|
271
|
+
"cl-02",
|
|
272
|
+
"cl-03",
|
|
273
|
+
"cl-04",
|
|
274
|
+
"cl-05",
|
|
275
|
+
"cl-06",
|
|
276
|
+
"cl-07",
|
|
277
|
+
"cl-08",
|
|
278
|
+
"cl-09",
|
|
279
|
+
"cl-10",
|
|
280
|
+
"cl-11",
|
|
281
|
+
"cl-12",
|
|
282
|
+
"cl-13",
|
|
283
|
+
"cl-14",
|
|
284
|
+
"cl-15",
|
|
285
|
+
"cl-16",
|
|
286
|
+
"cl-19",
|
|
287
|
+
"cl-24",
|
|
288
|
+
"cl-25",
|
|
289
|
+
"cl-26",
|
|
290
|
+
"cl-27",
|
|
291
|
+
"cl-30"
|
|
292
|
+
];
|
|
293
|
+
const OPENING = /* @__PURE__ */ new Set(`${OPENING_BRACKETS}【〘〝⦅«`);
|
|
294
|
+
const CLOSING = /* @__PURE__ */ new Set(`${CLOSING_BRACKETS}】〙〟⦆»`);
|
|
295
|
+
const HYPHENS = /* @__PURE__ */ new Set("‐‑⁃–");
|
|
296
|
+
const DIVIDING_PUNCTUATION = /* @__PURE__ */ new Set("!?‼⁇⁈⁉");
|
|
297
|
+
const MIDDLE_DOTS = /* @__PURE__ */ new Set("・:;");
|
|
298
|
+
const SMALL_KANA = /* @__PURE__ */ new Set("ァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖ");
|
|
299
|
+
const ITERATION_MARKS = /* @__PURE__ */ new Set("ヽヾゝゞ々〻");
|
|
300
|
+
const INSEPARABLE = /* @__PURE__ */ new Set("—―…‥─〳〵");
|
|
301
|
+
const PREFIXED_ABBREVIATIONS = /* @__PURE__ */ new Set("¥$£##");
|
|
302
|
+
const POSTFIXED_ABBREVIATIONS = /* @__PURE__ */ new Set("°′″%‰");
|
|
303
|
+
const UNIT_SYMBOLS = /* @__PURE__ */ new Set("℃℉㎀㎁㎂㎃㎄㎅㎆㎇㎈㎉㎊㎋㎌㎍㎎㎏㎐㎑㎒㎓㎔㎕㎖㎗㎘㎙㎚㎛㎜㎝㎞㎟㎠㎡㎢");
|
|
304
|
+
const GROUPED_NUMERAL = /^[0-90-9]$/u;
|
|
305
|
+
const HIRAGANA = /^\p{Script=Hiragana}$/u;
|
|
306
|
+
const KATAKANA = /^\p{Script=Katakana}$/u;
|
|
307
|
+
const WESTERN = /^[\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}]$/u;
|
|
308
|
+
/**
|
|
309
|
+
* Order matters: a character inside a tate-chu-yoko group is cl-30 whatever it would be on its own,
|
|
310
|
+
* and anything left unmatched is cl-19, the class JLReq reserves for kanji and the like.
|
|
311
|
+
*/
|
|
312
|
+
function classify({ value, presentation }) {
|
|
313
|
+
if (presentation === "tate-chu-yoko") return "cl-30";
|
|
314
|
+
if (OPENING.has(value)) return "cl-01";
|
|
315
|
+
if (CLOSING.has(value)) return "cl-02";
|
|
316
|
+
if (HYPHENS.has(value)) return "cl-03";
|
|
317
|
+
if (DIVIDING_PUNCTUATION.has(value)) return "cl-04";
|
|
318
|
+
if (MIDDLE_DOTS.has(value)) return "cl-05";
|
|
319
|
+
if (value === "。" || value === ".") return "cl-06";
|
|
320
|
+
if (value === "、" || value === ",") return "cl-07";
|
|
321
|
+
if (INSEPARABLE.has(value)) return "cl-08";
|
|
322
|
+
if (ITERATION_MARKS.has(value)) return "cl-09";
|
|
323
|
+
if (value === "ー") return "cl-10";
|
|
324
|
+
if (SMALL_KANA.has(value)) return "cl-11";
|
|
325
|
+
if (PREFIXED_ABBREVIATIONS.has(value)) return "cl-12";
|
|
326
|
+
if (POSTFIXED_ABBREVIATIONS.has(value)) return "cl-13";
|
|
327
|
+
if (value === " ") return "cl-14";
|
|
328
|
+
if (HIRAGANA.test(value)) return "cl-15";
|
|
329
|
+
if (KATAKANA.test(value)) return "cl-16";
|
|
330
|
+
if (GROUPED_NUMERAL.test(value)) return "cl-24";
|
|
331
|
+
if (UNIT_SYMBOLS.has(value)) return "cl-25";
|
|
332
|
+
if (value === " ") return "cl-26";
|
|
333
|
+
if (WESTERN.test(value)) return "cl-27";
|
|
334
|
+
return "cl-19";
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* The stages JLReq 3.8.3 spends inter-character space in, as glue priorities: the lower the number,
|
|
338
|
+
* the earlier the paragraph optimizer takes it.
|
|
339
|
+
*
|
|
340
|
+
* JLReq's own first stage is the western word space (cl-26), which is absent here because reducing
|
|
341
|
+
* it means resizing the space's own advance rather than a space between two characters.
|
|
342
|
+
*
|
|
343
|
+
* The line-end stage is priority 0, which the optimizer also charges nothing for. JLReq 3.1.9 is
|
|
344
|
+
* what allows that: the half em after a closing bracket, full stop or comma — and the quarter em
|
|
345
|
+
* after a middle dot — cannot be seen once that character sits at the line end. JLReq orders it
|
|
346
|
+
* second rather than first, but only after the word space this profile does not adjust at all.
|
|
347
|
+
* Stages two and three are one priority here because a line has a single line end, so the two never
|
|
348
|
+
* compete.
|
|
349
|
+
*/
|
|
350
|
+
const REDUCTION_STAGE = {
|
|
351
|
+
/**
|
|
352
|
+
* Stages 2 and 3: the space after a closing bracket, full stop, comma or middle dot at line end.
|
|
353
|
+
*/
|
|
354
|
+
lineEnd: 0,
|
|
355
|
+
/**
|
|
356
|
+
* Stage 4: the quarter em on either side of a mid-line middle dot.
|
|
357
|
+
*/
|
|
358
|
+
middleDot: 1,
|
|
359
|
+
/**
|
|
360
|
+
* Stage 5: the half em before an opening bracket and after a closing bracket or comma.
|
|
361
|
+
*/
|
|
362
|
+
punctuation: 2,
|
|
363
|
+
/**
|
|
364
|
+
* Stage 6: the quarter em between Japanese and a numeral, unit symbol or western character.
|
|
365
|
+
*/
|
|
366
|
+
mixedText: 3
|
|
367
|
+
};
|
|
368
|
+
/**
|
|
369
|
+
* The stages JLReq 3.8.4 adds inter-character space in, as glue priorities on the same reading as
|
|
370
|
+
* {@link REDUCTION_STAGE}: the lower the number, the earlier the paragraph optimizer takes it. A
|
|
371
|
+
* line is either reduced or expanded, never both, so the two scales never meet.
|
|
372
|
+
*
|
|
373
|
+
* Stage 1 is the western word space (cl-26), absent for the same reason it is absent from the
|
|
374
|
+
* reduction stages. Stage 4 spreads whatever a、b and c could not absorb across every character gap
|
|
375
|
+
* that is not unbreakable, and is deliberately not implemented: it would justify every line that
|
|
376
|
+
* currently falls short, which reads as prose combed out to the margin rather than a novel.
|
|
377
|
+
*/
|
|
378
|
+
const EXPANSION_STAGE = {
|
|
379
|
+
/**
|
|
380
|
+
* Stage 2: the quarter em between Japanese and a numeral, unit symbol or western character.
|
|
381
|
+
*/
|
|
382
|
+
mixedText: 1,
|
|
383
|
+
/**
|
|
384
|
+
* Stage 3: everywhere else 表6 admits, from solid up to a quarter em.
|
|
385
|
+
*/
|
|
386
|
+
solid: 2
|
|
387
|
+
};
|
|
388
|
+
/**
|
|
389
|
+
* 無印 in 表1 with no colour in 表6: the pair is set solid and line adjustment leaves it alone.
|
|
390
|
+
*/
|
|
391
|
+
const SOLID = {
|
|
392
|
+
kind: "glue",
|
|
393
|
+
naturalWidthEm: 0
|
|
394
|
+
};
|
|
395
|
+
/**
|
|
396
|
+
* `1/4` in 表6: set solid, but line adjustment may open it up to a quarter em.
|
|
397
|
+
*/
|
|
398
|
+
const SOLID_EXPANDABLE = {
|
|
399
|
+
kind: "glue",
|
|
400
|
+
naturalWidthEm: 0,
|
|
401
|
+
stretch: {
|
|
402
|
+
priority: EXPANSION_STAGE.solid,
|
|
403
|
+
amountEm: .25
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
/**
|
|
407
|
+
* `1/2` in 表1 with `1/2-0` in 表3: a half em that line adjustment may take down to solid.
|
|
408
|
+
*/
|
|
409
|
+
const HALF = {
|
|
410
|
+
kind: "glue",
|
|
411
|
+
naturalWidthEm: .5,
|
|
412
|
+
shrink: {
|
|
413
|
+
priority: REDUCTION_STAGE.punctuation,
|
|
414
|
+
amountEm: .5
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
/**
|
|
418
|
+
* `1/2` in both tables: the half em exists but is not a reduction opportunity.
|
|
419
|
+
*/
|
|
420
|
+
const FIXED_HALF = {
|
|
421
|
+
kind: "glue",
|
|
422
|
+
naturalWidthEm: .5
|
|
423
|
+
};
|
|
424
|
+
/**
|
|
425
|
+
* `1/4` in 表1 with `1/4-0` in 表3: a quarter em that may go down to solid.
|
|
426
|
+
*/
|
|
427
|
+
const QUARTER = {
|
|
428
|
+
kind: "glue",
|
|
429
|
+
naturalWidthEm: .25,
|
|
430
|
+
shrink: {
|
|
431
|
+
priority: REDUCTION_STAGE.middleDot,
|
|
432
|
+
amountEm: .25
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
/**
|
|
436
|
+
* `1/4` in both tables: the quarter em exists but is not a reduction opportunity.
|
|
437
|
+
*/
|
|
438
|
+
const FIXED_QUARTER = {
|
|
439
|
+
kind: "glue",
|
|
440
|
+
naturalWidthEm: .25
|
|
441
|
+
};
|
|
442
|
+
/**
|
|
443
|
+
* `1/4-1/8` in 表3 with `1/4-1/2` in 表6: the Japanese-to-western quarter em, reducible to an eighth
|
|
444
|
+
* and expandable to a half.
|
|
445
|
+
*/
|
|
446
|
+
const MIXED_TEXT_QUARTER = {
|
|
447
|
+
kind: "glue",
|
|
448
|
+
naturalWidthEm: .25,
|
|
449
|
+
shrink: {
|
|
450
|
+
priority: REDUCTION_STAGE.mixedText,
|
|
451
|
+
amountEm: .125
|
|
452
|
+
},
|
|
453
|
+
stretch: {
|
|
454
|
+
priority: EXPANSION_STAGE.mixedText,
|
|
455
|
+
amountEm: .25
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
/**
|
|
459
|
+
* The same quarter em where 表6 leaves it out of the expansion: between a unit symbol and the
|
|
460
|
+
* quantity on either side of it.
|
|
461
|
+
*/
|
|
462
|
+
const UNIT_SYMBOL_QUARTER = {
|
|
463
|
+
kind: "glue",
|
|
464
|
+
naturalWidthEm: .25,
|
|
465
|
+
shrink: {
|
|
466
|
+
priority: REDUCTION_STAGE.mixedText,
|
|
467
|
+
amountEm: .125
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
/**
|
|
471
|
+
* 表1 注3 with 表3 注1: two middle dots meet, and their quarters go solid together.
|
|
472
|
+
*/
|
|
473
|
+
const TWO_QUARTERS = {
|
|
474
|
+
kind: "glue",
|
|
475
|
+
naturalWidthEm: .5,
|
|
476
|
+
shrink: {
|
|
477
|
+
priority: REDUCTION_STAGE.middleDot,
|
|
478
|
+
amountEm: .5
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
/**
|
|
482
|
+
* 表1 注5 with 表3 注2: a full stop's fixed half em plus the following middle dot's quarter.
|
|
483
|
+
*/
|
|
484
|
+
const FIXED_HALF_AND_QUARTER = {
|
|
485
|
+
kind: "glue",
|
|
486
|
+
naturalWidthEm: .75,
|
|
487
|
+
shrink: {
|
|
488
|
+
priority: REDUCTION_STAGE.middleDot,
|
|
489
|
+
amountEm: .25
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
/**
|
|
493
|
+
* 表1 注5 with 表3 注3: a comma's half em plus the following middle dot's quarter.
|
|
494
|
+
*
|
|
495
|
+
* 表3 spends the quarter at stage 4 and the half at stage 5, which one capacity cannot say. Both are
|
|
496
|
+
* reducible to solid, so the total is right and only the order collapses — and `、・` is rare enough
|
|
497
|
+
* that the order never shows.
|
|
498
|
+
*/
|
|
499
|
+
const HALF_AND_QUARTER = {
|
|
500
|
+
kind: "glue",
|
|
501
|
+
naturalWidthEm: .75,
|
|
502
|
+
shrink: {
|
|
503
|
+
priority: REDUCTION_STAGE.punctuation,
|
|
504
|
+
amountEm: .75
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
/**
|
|
508
|
+
* 分離禁止文字 repeated (a 2倍ダッシュ, a 2倍リーダ) must read as one continuous rule (3.1.10).
|
|
509
|
+
*/
|
|
510
|
+
const DASH_JOINT = {
|
|
511
|
+
kind: "kern",
|
|
512
|
+
naturalWidthEm: 0
|
|
513
|
+
};
|
|
514
|
+
/**
|
|
515
|
+
* The classes that take a quarter em against a numeral, unit symbol or western character (3.2.6).
|
|
516
|
+
* Dividing punctuation is on the list in that direction only, because the space before a `?` or `!`
|
|
517
|
+
* is solid (3.1.6).
|
|
518
|
+
*/
|
|
519
|
+
const JAPANESE_BEFORE_WESTERN = [
|
|
520
|
+
"cl-04",
|
|
521
|
+
"cl-09",
|
|
522
|
+
"cl-10",
|
|
523
|
+
"cl-11",
|
|
524
|
+
"cl-15",
|
|
525
|
+
"cl-16",
|
|
526
|
+
"cl-19",
|
|
527
|
+
"cl-30"
|
|
528
|
+
];
|
|
529
|
+
const JAPANESE_AFTER_WESTERN = [
|
|
530
|
+
"cl-09",
|
|
531
|
+
"cl-10",
|
|
532
|
+
"cl-11",
|
|
533
|
+
"cl-15",
|
|
534
|
+
"cl-16",
|
|
535
|
+
"cl-19",
|
|
536
|
+
"cl-30"
|
|
537
|
+
];
|
|
538
|
+
const NUMERAL_UNIT_OR_WESTERN = [
|
|
539
|
+
"cl-24",
|
|
540
|
+
"cl-25",
|
|
541
|
+
"cl-27"
|
|
542
|
+
];
|
|
543
|
+
const PUNCTUATION_TAKING_SPACE_AFTER = [
|
|
544
|
+
"cl-02",
|
|
545
|
+
"cl-06",
|
|
546
|
+
"cl-07"
|
|
547
|
+
];
|
|
548
|
+
/**
|
|
549
|
+
* 表6 の色付きの小間: the classes a solid pair may be opened up between at stage 3. Neither list holds a
|
|
550
|
+
* bracket, a middle dot, a full stop, a comma, a hyphen, an ideographic space or a word space —
|
|
551
|
+
* JLReq opens none of those — and only the left-hand list holds a numeral, unit symbol or western
|
|
552
|
+
* character, since the space on their Japanese side belongs to stage 2 instead.
|
|
553
|
+
*/
|
|
554
|
+
const EXPANDABLE_BEFORE = [
|
|
555
|
+
"cl-08",
|
|
556
|
+
"cl-09",
|
|
557
|
+
"cl-10",
|
|
558
|
+
"cl-11",
|
|
559
|
+
"cl-12",
|
|
560
|
+
"cl-13",
|
|
561
|
+
"cl-15",
|
|
562
|
+
"cl-16",
|
|
563
|
+
"cl-19",
|
|
564
|
+
"cl-24",
|
|
565
|
+
"cl-25",
|
|
566
|
+
"cl-27",
|
|
567
|
+
"cl-30"
|
|
568
|
+
];
|
|
569
|
+
const EXPANDABLE_AFTER = [
|
|
570
|
+
"cl-08",
|
|
571
|
+
"cl-09",
|
|
572
|
+
"cl-10",
|
|
573
|
+
"cl-11",
|
|
574
|
+
"cl-12",
|
|
575
|
+
"cl-13",
|
|
576
|
+
"cl-15",
|
|
577
|
+
"cl-16",
|
|
578
|
+
"cl-19",
|
|
579
|
+
"cl-30"
|
|
580
|
+
];
|
|
581
|
+
/**
|
|
582
|
+
* The classes JLReq 3.1.2 sets on a half em rather than a full one.
|
|
583
|
+
*/
|
|
584
|
+
const HALF_EM_CLASSES = /* @__PURE__ */ new Set([
|
|
585
|
+
"cl-01",
|
|
586
|
+
"cl-02",
|
|
587
|
+
"cl-05",
|
|
588
|
+
"cl-06",
|
|
589
|
+
"cl-07"
|
|
590
|
+
]);
|
|
591
|
+
const PAIR_SPACING_TABLE = new Map([
|
|
592
|
+
{
|
|
593
|
+
left: "any",
|
|
594
|
+
right: "any",
|
|
595
|
+
spacing: SOLID
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
left: EXPANDABLE_BEFORE,
|
|
599
|
+
right: EXPANDABLE_AFTER,
|
|
600
|
+
spacing: SOLID_EXPANDABLE
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
left: [
|
|
604
|
+
"cl-08",
|
|
605
|
+
"cl-12",
|
|
606
|
+
"cl-13"
|
|
607
|
+
],
|
|
608
|
+
right: NUMERAL_UNIT_OR_WESTERN,
|
|
609
|
+
spacing: SOLID_EXPANDABLE
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
left: ["cl-08"],
|
|
613
|
+
right: ["cl-24", "cl-25"],
|
|
614
|
+
spacing: SOLID
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
left: ["cl-12"],
|
|
618
|
+
right: ["cl-24"],
|
|
619
|
+
spacing: SOLID
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
left: ["cl-24"],
|
|
623
|
+
right: ["cl-13"],
|
|
624
|
+
spacing: SOLID
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
left: [
|
|
628
|
+
"cl-09",
|
|
629
|
+
"cl-10",
|
|
630
|
+
"cl-11"
|
|
631
|
+
],
|
|
632
|
+
right: ["cl-08"],
|
|
633
|
+
spacing: SOLID
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
left: "any",
|
|
637
|
+
right: ["cl-01"],
|
|
638
|
+
spacing: HALF
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
left: "any",
|
|
642
|
+
right: ["cl-05"],
|
|
643
|
+
spacing: QUARTER
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
left: "any",
|
|
647
|
+
right: PUNCTUATION_TAKING_SPACE_AFTER,
|
|
648
|
+
spacing: SOLID
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
left: JAPANESE_BEFORE_WESTERN,
|
|
652
|
+
right: NUMERAL_UNIT_OR_WESTERN,
|
|
653
|
+
spacing: MIXED_TEXT_QUARTER
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
left: NUMERAL_UNIT_OR_WESTERN,
|
|
657
|
+
right: JAPANESE_AFTER_WESTERN,
|
|
658
|
+
spacing: MIXED_TEXT_QUARTER
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
left: ["cl-25"],
|
|
662
|
+
right: ["cl-24"],
|
|
663
|
+
spacing: UNIT_SYMBOL_QUARTER
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
left: ["cl-27"],
|
|
667
|
+
right: ["cl-25"],
|
|
668
|
+
spacing: UNIT_SYMBOL_QUARTER
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
left: ["cl-24"],
|
|
672
|
+
right: ["cl-25"],
|
|
673
|
+
spacing: FIXED_QUARTER
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
left: ["cl-02", "cl-07"],
|
|
677
|
+
right: "any",
|
|
678
|
+
spacing: HALF
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
left: ["cl-06"],
|
|
682
|
+
right: "any",
|
|
683
|
+
spacing: FIXED_HALF
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
left: ["cl-02"],
|
|
687
|
+
right: ["cl-05"],
|
|
688
|
+
spacing: QUARTER
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
left: ["cl-06"],
|
|
692
|
+
right: ["cl-05"],
|
|
693
|
+
spacing: FIXED_HALF_AND_QUARTER
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
left: ["cl-07"],
|
|
697
|
+
right: ["cl-05"],
|
|
698
|
+
spacing: HALF_AND_QUARTER
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
left: PUNCTUATION_TAKING_SPACE_AFTER,
|
|
702
|
+
right: PUNCTUATION_TAKING_SPACE_AFTER,
|
|
703
|
+
spacing: SOLID
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
left: ["cl-02"],
|
|
707
|
+
right: ["cl-14"],
|
|
708
|
+
spacing: SOLID
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
left: ["cl-05"],
|
|
712
|
+
right: "any",
|
|
713
|
+
spacing: QUARTER
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
left: ["cl-05"],
|
|
717
|
+
right: ["cl-05"],
|
|
718
|
+
spacing: TWO_QUARTERS
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
left: ["cl-01"],
|
|
722
|
+
right: "any",
|
|
723
|
+
spacing: SOLID
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
left: ["cl-01"],
|
|
727
|
+
right: ["cl-05"],
|
|
728
|
+
spacing: QUARTER
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
left: ["cl-14"],
|
|
732
|
+
right: "any",
|
|
733
|
+
spacing: SOLID
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
left: ["cl-14"],
|
|
737
|
+
right: ["cl-05"],
|
|
738
|
+
spacing: QUARTER
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
left: "any",
|
|
742
|
+
right: ["cl-26"],
|
|
743
|
+
spacing: SOLID
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
left: PUNCTUATION_TAKING_SPACE_AFTER,
|
|
747
|
+
right: ["cl-26"],
|
|
748
|
+
spacing: FIXED_HALF
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
left: ["cl-05"],
|
|
752
|
+
right: ["cl-26"],
|
|
753
|
+
spacing: FIXED_QUARTER
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
left: ["cl-26"],
|
|
757
|
+
right: "any",
|
|
758
|
+
spacing: SOLID
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
left: ["cl-26"],
|
|
762
|
+
right: ["cl-01"],
|
|
763
|
+
spacing: FIXED_HALF
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
left: ["cl-26"],
|
|
767
|
+
right: ["cl-05"],
|
|
768
|
+
spacing: FIXED_QUARTER
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
left: ["cl-08"],
|
|
772
|
+
right: ["cl-08"],
|
|
773
|
+
spacing: DASH_JOINT
|
|
774
|
+
}
|
|
775
|
+
].flatMap(({ left, right, spacing }) => (left === "any" ? japaneseCharacterClasses : left).flatMap((leftClass) => (right === "any" ? japaneseCharacterClasses : right).map((rightClass) => [`${leftClass}/${rightClass}`, spacing]))));
|
|
776
|
+
function pairSpacing(left, right) {
|
|
777
|
+
return PAIR_SPACING_TABLE.get(`${left}/${right}`) ?? SOLID;
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* `null` removes the break opportunity outright rather than pricing it, because the three rules
|
|
781
|
+
* below are prohibitions and not preferences.
|
|
782
|
+
*
|
|
783
|
+
* Line-end prohibition (行末禁則, JLReq 3.1.8): an opening bracket or a prefixed abbreviation may not
|
|
784
|
+
* be the last character on a line, since it belongs to what follows it.
|
|
785
|
+
*
|
|
786
|
+
* Line-start prohibition (行頭禁則, JLReq 3.1.7): closing brackets, hyphens, dividing punctuation,
|
|
787
|
+
* middle dots, full stops, commas, iteration marks, the prolonged sound mark, small kana and
|
|
788
|
+
* postfixed abbreviations may not open a line.
|
|
789
|
+
*
|
|
790
|
+
* Unbreakable sequences (分割禁止, JLReq 3.1.10): a 分離禁止文字 followed by another one is a single mark set
|
|
791
|
+
* over two ems and must not be split.
|
|
792
|
+
*/
|
|
793
|
+
function breakPenalty(left, right) {
|
|
794
|
+
if (left === "cl-01" || left === "cl-12") return null;
|
|
795
|
+
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;
|
|
796
|
+
if (left === "cl-08" && right === "cl-08") return null;
|
|
797
|
+
return 0;
|
|
798
|
+
}
|
|
799
|
+
const defaultJapaneseTypesettingProfile = {
|
|
800
|
+
classify,
|
|
801
|
+
/**
|
|
802
|
+
* Opening and closing brackets, full stops, commas and middle dots are set on a half em (JLReq
|
|
803
|
+
* 3.1.2); the space the pair table puts around them is what brings each back to a full em.
|
|
804
|
+
*
|
|
805
|
+
* A fullwidth glyph keeps drawing its ink where the font put it, so halving the advance alone
|
|
806
|
+
* would leave the ink outside the box. The offset pulls it back: an opening bracket draws in the
|
|
807
|
+
* trailing half of its em, a middle dot in the centre, and the rest in the leading half.
|
|
808
|
+
*/
|
|
809
|
+
boxMetrics: (characterClass, measuredAdvanceEm) => {
|
|
810
|
+
const advanceEm = HALF_EM_CLASSES.has(characterClass) ? Math.min(.5, measuredAdvanceEm) : measuredAdvanceEm;
|
|
811
|
+
const inkOverhangEm = measuredAdvanceEm - advanceEm;
|
|
812
|
+
return {
|
|
813
|
+
advanceEm,
|
|
814
|
+
renderOffsetEm: characterClass === "cl-01" ? -inkOverhangEm : characterClass === "cl-05" ? -inkOverhangEm / 2 : 0
|
|
815
|
+
};
|
|
816
|
+
},
|
|
817
|
+
pairSpacing,
|
|
818
|
+
/**
|
|
819
|
+
* An opening bracket at the line head keeps a half em of white, so a line of dialogue starts one
|
|
820
|
+
* half em in rather than tight against the edge. JLReq 3.1.5 lists three schemes for that white
|
|
821
|
+
* and 表1 注17 sets solid as the principle; the half em is the variant the publishers of Japanese
|
|
822
|
+
* novels use for a line that begins a paragraph.
|
|
823
|
+
*
|
|
824
|
+
* The white is not shrinkable. 表3 excludes the line head from line adjustment altogether, and
|
|
825
|
+
* shrinking it is exactly what pushed a line-head bracket flush against the edge in #94.
|
|
826
|
+
*/
|
|
827
|
+
lineStartSpacing: (first) => first === "cl-01" ? {
|
|
828
|
+
kind: "glue",
|
|
829
|
+
naturalWidthEm: .5
|
|
830
|
+
} : null,
|
|
831
|
+
/**
|
|
832
|
+
* The half em trailing a closing bracket, full stop or comma at the line end, and the quarter em
|
|
833
|
+
* trailing a middle dot there (JLReq 3.1.9). Priority 0: spending it changes nothing the reader
|
|
834
|
+
* can see.
|
|
835
|
+
*/
|
|
836
|
+
lineEndSpacing: (last) => last === "cl-02" || last === "cl-06" || last === "cl-07" ? {
|
|
837
|
+
kind: "glue",
|
|
838
|
+
naturalWidthEm: .5,
|
|
839
|
+
shrink: {
|
|
840
|
+
priority: REDUCTION_STAGE.lineEnd,
|
|
841
|
+
amountEm: .5
|
|
842
|
+
}
|
|
843
|
+
} : last === "cl-05" ? {
|
|
844
|
+
kind: "glue",
|
|
845
|
+
naturalWidthEm: .25,
|
|
846
|
+
shrink: {
|
|
847
|
+
priority: REDUCTION_STAGE.lineEnd,
|
|
848
|
+
amountEm: .25
|
|
849
|
+
}
|
|
850
|
+
} : null,
|
|
851
|
+
breakPenalty,
|
|
852
|
+
/**
|
|
853
|
+
* Hanging punctuation (ぶら下げ組, JLReq 3.8.2) lets a full stop or comma sit outside the text area
|
|
854
|
+
* instead of forcing the line to be respaced. JLReq admits no other class for it.
|
|
855
|
+
*/
|
|
856
|
+
canHang: (characterClass) => characterClass === "cl-07" || characterClass === "cl-06"
|
|
857
|
+
};
|
|
858
|
+
//#endregion
|
|
859
|
+
//#region src/composer/internal/paragraph-layout.ts
|
|
860
|
+
const EPSILON = 1e-9;
|
|
861
|
+
/**
|
|
862
|
+
* Spending capacity at this priority changes nothing the reader can see.
|
|
863
|
+
*/
|
|
864
|
+
const FREE_SHRINK_PRIORITY = 0;
|
|
865
|
+
function skipSourceGaps(atoms, start) {
|
|
866
|
+
let cursor = start;
|
|
867
|
+
while (atoms[cursor]?.sourceGap === true) cursor += 1;
|
|
868
|
+
return cursor;
|
|
869
|
+
}
|
|
870
|
+
function previousVisible(atoms, end) {
|
|
871
|
+
for (let index = end - 1; index >= 0; index -= 1) if (atoms[index]?.sourceGap === false) return index;
|
|
872
|
+
}
|
|
873
|
+
function opportunities(atoms, classes, start, end, profile) {
|
|
874
|
+
const result = [];
|
|
875
|
+
const firstClass = classes[start];
|
|
876
|
+
const startSpacing = firstClass === void 0 ? null : profile.lineStartSpacing(firstClass);
|
|
877
|
+
if (startSpacing !== null) result.push({
|
|
878
|
+
boundary: start,
|
|
879
|
+
spacing: startSpacing
|
|
880
|
+
});
|
|
881
|
+
for (let right = start + 1; right < end; right += 1) {
|
|
882
|
+
const leftAtom = atoms[right - 1];
|
|
883
|
+
const rightAtom = atoms[right];
|
|
884
|
+
const leftClass = classes[right - 1];
|
|
885
|
+
const rightClass = classes[right];
|
|
886
|
+
if (leftAtom === void 0 || rightAtom === void 0 || leftClass === void 0 || rightClass === void 0 || leftAtom.sourceGap || rightAtom.sourceGap || !leftAtom.pairSpacingAfter) continue;
|
|
887
|
+
result.push({
|
|
888
|
+
boundary: right,
|
|
889
|
+
spacing: profile.pairSpacing(leftClass, rightClass)
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
const lastClass = classes[end - 1];
|
|
893
|
+
const endSpacing = lastClass === void 0 ? null : profile.lineEndSpacing(lastClass);
|
|
894
|
+
if (endSpacing !== null) result.push({
|
|
895
|
+
boundary: end,
|
|
896
|
+
spacing: endSpacing
|
|
897
|
+
});
|
|
898
|
+
return result;
|
|
899
|
+
}
|
|
900
|
+
function capacity(values, select) {
|
|
901
|
+
return values.reduce((total, opportunity) => total + (select(opportunity.spacing)?.amountEm ?? 0), 0);
|
|
902
|
+
}
|
|
903
|
+
function resolveSpacings(values, adjustment, amountEm) {
|
|
904
|
+
const remainingByBoundary = /* @__PURE__ */ new Map();
|
|
905
|
+
let remaining = amountEm;
|
|
906
|
+
let priorityCost = 0;
|
|
907
|
+
const capacities = values.flatMap((opportunity) => {
|
|
908
|
+
const selected = adjustment === "shrink" ? opportunity.spacing.shrink : opportunity.spacing.stretch;
|
|
909
|
+
return selected === void 0 ? [] : [{
|
|
910
|
+
...opportunity,
|
|
911
|
+
capacity: selected
|
|
912
|
+
}];
|
|
913
|
+
}).sort((left, right) => left.capacity.priority - right.capacity.priority || left.boundary - right.boundary);
|
|
914
|
+
for (const opportunity of capacities) {
|
|
915
|
+
if (remaining <= EPSILON) break;
|
|
916
|
+
const used = Math.min(remaining, opportunity.capacity.amountEm);
|
|
917
|
+
remainingByBoundary.set(opportunity.boundary, used);
|
|
918
|
+
priorityCost += used * opportunity.capacity.priority;
|
|
919
|
+
remaining -= used;
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
spacings: values.map(({ boundary, spacing }) => {
|
|
923
|
+
const used = remainingByBoundary.get(boundary) ?? 0;
|
|
924
|
+
return {
|
|
925
|
+
boundary,
|
|
926
|
+
kind: spacing.kind,
|
|
927
|
+
naturalWidthEm: spacing.naturalWidthEm,
|
|
928
|
+
widthEm: adjustment === "shrink" ? spacing.naturalWidthEm - used : spacing.naturalWidthEm + used
|
|
929
|
+
};
|
|
930
|
+
}),
|
|
931
|
+
priorityCost
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
function candidate(atoms, classes, start, end, lineLengthEm, profile) {
|
|
935
|
+
const contentStart = skipSourceGaps(atoms, start);
|
|
936
|
+
const suppressedIndexes = Array.from({ length: contentStart - start }, (_, index) => start + index);
|
|
937
|
+
const pairValues = opportunities(atoms, classes, contentStart, end, profile);
|
|
938
|
+
const naturalSizeEm = atoms.slice(contentStart, end).reduce((total, atom) => total + atom.boxAdvanceEm, 0) + pairValues.reduce((total, value) => total + value.spacing.naturalWidthEm, 0);
|
|
939
|
+
const terminal = skipSourceGaps(atoms, end) === atoms.length;
|
|
940
|
+
const shrinkCapacity = capacity(pairValues, ({ shrink }) => shrink);
|
|
941
|
+
const freeShrinkCapacity = capacity(pairValues, ({ shrink }) => shrink?.priority === FREE_SHRINK_PRIORITY ? shrink : void 0);
|
|
942
|
+
const stretchCapacity = capacity(pairValues, ({ stretch }) => stretch);
|
|
943
|
+
const overflow = naturalSizeEm - lineLengthEm;
|
|
944
|
+
const underflow = lineLengthEm - naturalSizeEm;
|
|
945
|
+
const lastVisible = previousVisible(atoms, end);
|
|
946
|
+
const lastClass = lastVisible === void 0 ? void 0 : classes[lastVisible];
|
|
947
|
+
const lastAdvance = lastVisible === void 0 ? 0 : atoms[lastVisible]?.boxAdvanceEm ?? 0;
|
|
948
|
+
const trailingSpacing = pairValues.find(({ boundary }) => boundary === end)?.spacing.naturalWidthEm ?? 0;
|
|
949
|
+
if (terminal && overflow <= EPSILON) return {
|
|
950
|
+
start,
|
|
951
|
+
contentStart,
|
|
952
|
+
end,
|
|
953
|
+
suppressedIndexes,
|
|
954
|
+
pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings,
|
|
955
|
+
inlineSizeEm: naturalSizeEm,
|
|
956
|
+
break: { kind: "paragraph-end" },
|
|
957
|
+
hangingIndex: null,
|
|
958
|
+
deformationRatio: 0,
|
|
959
|
+
priorityCost: 0
|
|
960
|
+
};
|
|
961
|
+
if (Math.abs(overflow) <= EPSILON) return {
|
|
962
|
+
start,
|
|
963
|
+
contentStart,
|
|
964
|
+
end,
|
|
965
|
+
suppressedIndexes,
|
|
966
|
+
pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings,
|
|
967
|
+
inlineSizeEm: naturalSizeEm,
|
|
968
|
+
break: { kind: "natural" },
|
|
969
|
+
hangingIndex: null,
|
|
970
|
+
deformationRatio: 0,
|
|
971
|
+
priorityCost: 0
|
|
972
|
+
};
|
|
973
|
+
if (overflow > 0 && overflow <= shrinkCapacity + EPSILON) {
|
|
974
|
+
const resolved = resolveSpacings(pairValues, "shrink", overflow);
|
|
975
|
+
const chargeableCapacity = shrinkCapacity - freeShrinkCapacity;
|
|
976
|
+
const chargeableShrink = Math.max(0, overflow - freeShrinkCapacity);
|
|
977
|
+
return {
|
|
978
|
+
start,
|
|
979
|
+
contentStart,
|
|
980
|
+
end,
|
|
981
|
+
suppressedIndexes,
|
|
982
|
+
pairSpacings: resolved.spacings,
|
|
983
|
+
inlineSizeEm: lineLengthEm,
|
|
984
|
+
break: { kind: "shrunk" },
|
|
985
|
+
hangingIndex: null,
|
|
986
|
+
deformationRatio: chargeableCapacity <= EPSILON ? 0 : chargeableShrink / chargeableCapacity,
|
|
987
|
+
priorityCost: resolved.priorityCost
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
if (!terminal && overflow > 0 && lastVisible !== void 0 && lastClass !== void 0 && profile.canHang(lastClass) && naturalSizeEm - lastAdvance - trailingSpacing <= lineLengthEm + EPSILON) return {
|
|
991
|
+
start,
|
|
992
|
+
contentStart,
|
|
993
|
+
end,
|
|
994
|
+
suppressedIndexes,
|
|
995
|
+
pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings.filter(({ boundary }) => boundary !== end),
|
|
996
|
+
inlineSizeEm: naturalSizeEm - lastAdvance - trailingSpacing,
|
|
997
|
+
break: { kind: "hanging" },
|
|
998
|
+
hangingIndex: lastVisible,
|
|
999
|
+
deformationRatio: overflow / Math.max(lastAdvance, EPSILON),
|
|
1000
|
+
priorityCost: 0
|
|
1001
|
+
};
|
|
1002
|
+
if (!terminal && underflow > EPSILON && underflow <= stretchCapacity + EPSILON) {
|
|
1003
|
+
const resolved = resolveSpacings(pairValues, "stretch", underflow);
|
|
1004
|
+
return {
|
|
1005
|
+
start,
|
|
1006
|
+
contentStart,
|
|
1007
|
+
end,
|
|
1008
|
+
suppressedIndexes,
|
|
1009
|
+
pairSpacings: resolved.spacings,
|
|
1010
|
+
inlineSizeEm: lineLengthEm,
|
|
1011
|
+
break: { kind: "stretched" },
|
|
1012
|
+
hangingIndex: null,
|
|
1013
|
+
deformationRatio: stretchCapacity === 0 ? 0 : underflow / stretchCapacity,
|
|
1014
|
+
priorityCost: resolved.priorityCost
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
return {
|
|
1018
|
+
start,
|
|
1019
|
+
contentStart,
|
|
1020
|
+
end,
|
|
1021
|
+
suppressedIndexes,
|
|
1022
|
+
pairSpacings: resolveSpacings(pairValues, "natural", 0).spacings,
|
|
1023
|
+
inlineSizeEm: naturalSizeEm,
|
|
1024
|
+
break: { kind: "forced" },
|
|
1025
|
+
hangingIndex: null,
|
|
1026
|
+
deformationRatio: Math.abs(naturalSizeEm - lineLengthEm) / Math.max(lineLengthEm, EPSILON),
|
|
1027
|
+
priorityCost: 0
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
function scoreFor(line, previousFitness) {
|
|
1031
|
+
const mode = line.break.kind;
|
|
1032
|
+
const fitness = Math.min(3, Math.floor(line.deformationRatio * 4));
|
|
1033
|
+
const transition = Math.abs(previousFitness - fitness) > 1 ? 1 : 0;
|
|
1034
|
+
const modeCounts = {
|
|
1035
|
+
forced: mode === "forced" ? 1 : 0,
|
|
1036
|
+
stretched: mode === "stretched" ? 1 : 0,
|
|
1037
|
+
hanging: mode === "hanging" ? 1 : 0,
|
|
1038
|
+
shrunk: mode === "shrunk" && line.priorityCost > EPSILON ? 1 : 0
|
|
1039
|
+
};
|
|
1040
|
+
return {
|
|
1041
|
+
score: [
|
|
1042
|
+
modeCounts.forced,
|
|
1043
|
+
modeCounts.stretched,
|
|
1044
|
+
modeCounts.hanging,
|
|
1045
|
+
modeCounts.shrunk,
|
|
1046
|
+
line.priorityCost,
|
|
1047
|
+
line.deformationRatio ** 3,
|
|
1048
|
+
transition
|
|
1049
|
+
],
|
|
1050
|
+
fitness
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
function addScore(left, right) {
|
|
1054
|
+
return [
|
|
1055
|
+
left[0] + right[0],
|
|
1056
|
+
left[1] + right[1],
|
|
1057
|
+
left[2] + right[2],
|
|
1058
|
+
left[3] + right[3],
|
|
1059
|
+
left[4] + right[4],
|
|
1060
|
+
left[5] + right[5],
|
|
1061
|
+
left[6] + right[6]
|
|
1062
|
+
];
|
|
1063
|
+
}
|
|
1064
|
+
function compareBreaks(left, right) {
|
|
1065
|
+
const length = Math.min(left.length, right.length);
|
|
1066
|
+
for (let index = 0; index < length; index += 1) {
|
|
1067
|
+
const difference = (right[index] ?? 0) - (left[index] ?? 0);
|
|
1068
|
+
if (difference !== 0) return difference;
|
|
1069
|
+
}
|
|
1070
|
+
return left.length - right.length;
|
|
1071
|
+
}
|
|
1072
|
+
function isBetter(candidateState, current) {
|
|
1073
|
+
if (current === void 0) return true;
|
|
1074
|
+
for (let index = 0; index < candidateState.score.length; index += 1) {
|
|
1075
|
+
const difference = (candidateState.score[index] ?? 0) - (current.score[index] ?? 0);
|
|
1076
|
+
if (Math.abs(difference) > EPSILON) return difference < 0;
|
|
1077
|
+
}
|
|
1078
|
+
return compareBreaks(candidateState.breaks, current.breaks) < 0;
|
|
1079
|
+
}
|
|
1080
|
+
function layoutParagraph(atoms, lineLengthEm, profile, boundaryAllowed) {
|
|
1081
|
+
if (atoms.length === 0) return [];
|
|
1082
|
+
const classes = atoms.map(({ characterClass }) => characterClass);
|
|
1083
|
+
const states = /* @__PURE__ */ new Map([[0, /* @__PURE__ */ new Map([[0, {
|
|
1084
|
+
score: [
|
|
1085
|
+
0,
|
|
1086
|
+
0,
|
|
1087
|
+
0,
|
|
1088
|
+
0,
|
|
1089
|
+
0,
|
|
1090
|
+
0,
|
|
1091
|
+
0
|
|
1092
|
+
],
|
|
1093
|
+
fitness: 0,
|
|
1094
|
+
previous: null,
|
|
1095
|
+
line: null,
|
|
1096
|
+
breaks: []
|
|
1097
|
+
}]])]]);
|
|
1098
|
+
for (let start = 0; start < atoms.length; start += 1) {
|
|
1099
|
+
const activeStates = states.get(start);
|
|
1100
|
+
if (activeStates === void 0) continue;
|
|
1101
|
+
const contentStart = skipSourceGaps(atoms, start);
|
|
1102
|
+
if (contentStart === atoms.length) {
|
|
1103
|
+
const terminalStates = states.get(atoms.length) ?? /* @__PURE__ */ new Map();
|
|
1104
|
+
for (const state of activeStates.values()) if (isBetter(state, terminalStates.get(state.fitness))) terminalStates.set(state.fitness, state);
|
|
1105
|
+
states.set(atoms.length, terminalStates);
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
for (const state of activeStates.values()) for (let end = contentStart + 1; end <= atoms.length; end += 1) {
|
|
1109
|
+
const right = skipSourceGaps(atoms, end);
|
|
1110
|
+
const left = previousVisible(atoms, end);
|
|
1111
|
+
if (atoms[end - 1]?.sourceGap === true) continue;
|
|
1112
|
+
if (end < atoms.length && right < atoms.length && (left === void 0 || !boundaryAllowed(left, right))) continue;
|
|
1113
|
+
const line = candidate(atoms, classes, start, end, lineLengthEm, profile);
|
|
1114
|
+
if (line.break.kind === "forced" && line.inlineSizeEm > lineLengthEm + EPSILON && Array.from({ length: end - contentStart - 1 }, (_, index) => contentStart + index + 1).some((boundary) => boundaryAllowed(boundary - 1, boundary))) {
|
|
1115
|
+
if (line.inlineSizeEm > lineLengthEm * 2 && end > contentStart + 1) break;
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
const lineScore = scoreFor(line, state.fitness);
|
|
1119
|
+
const nextState = {
|
|
1120
|
+
score: addScore(state.score, lineScore.score),
|
|
1121
|
+
fitness: lineScore.fitness,
|
|
1122
|
+
previous: {
|
|
1123
|
+
index: start,
|
|
1124
|
+
fitness: state.fitness
|
|
1125
|
+
},
|
|
1126
|
+
line,
|
|
1127
|
+
breaks: [...state.breaks, end]
|
|
1128
|
+
};
|
|
1129
|
+
const statesAtEnd = states.get(end) ?? /* @__PURE__ */ new Map();
|
|
1130
|
+
if (isBetter(nextState, statesAtEnd.get(nextState.fitness))) {
|
|
1131
|
+
statesAtEnd.set(nextState.fitness, nextState);
|
|
1132
|
+
states.set(end, statesAtEnd);
|
|
1133
|
+
}
|
|
1134
|
+
if (line.inlineSizeEm > lineLengthEm * 2 && line.break.kind === "forced" && end > contentStart + 1) break;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
const terminal = [...states.get(atoms.length)?.values() ?? []].reduce((best, state) => isBetter(state, best) ? state : best, void 0);
|
|
1138
|
+
if (terminal === void 0) return [];
|
|
1139
|
+
const lines = [];
|
|
1140
|
+
let cursor = terminal;
|
|
1141
|
+
while (cursor?.line !== null && cursor?.line !== void 0) {
|
|
1142
|
+
lines.push(cursor.line);
|
|
1143
|
+
cursor = cursor.previous === null ? void 0 : states.get(cursor.previous.index)?.get(cursor.previous.fitness);
|
|
1144
|
+
}
|
|
1145
|
+
lines.reverse();
|
|
1146
|
+
const last = lines.at(-1);
|
|
1147
|
+
if (last !== void 0 && last.end < atoms.length) lines[lines.length - 1] = {
|
|
1148
|
+
...last,
|
|
1149
|
+
suppressedIndexes: [...last.suppressedIndexes, ...Array.from({ length: atoms.length - last.end }, (_, index) => last.end + index)]
|
|
1150
|
+
};
|
|
1151
|
+
return lines;
|
|
1152
|
+
}
|
|
1153
|
+
//#endregion
|
|
1154
|
+
//#region src/appearance/paper-size.ts
|
|
1155
|
+
const PAPER_SIZES = {
|
|
1156
|
+
a4: {
|
|
1157
|
+
id: "a4",
|
|
1158
|
+
label: "A4",
|
|
1159
|
+
widthMm: 210,
|
|
1160
|
+
heightMm: 297
|
|
1161
|
+
},
|
|
1162
|
+
a5: {
|
|
1163
|
+
id: "a5",
|
|
1164
|
+
label: "A5",
|
|
1165
|
+
widthMm: 148,
|
|
1166
|
+
heightMm: 210
|
|
1167
|
+
},
|
|
1168
|
+
a6: {
|
|
1169
|
+
id: "a6",
|
|
1170
|
+
label: "A6(文庫)",
|
|
1171
|
+
widthMm: 105,
|
|
1172
|
+
heightMm: 148
|
|
1173
|
+
},
|
|
1174
|
+
"jis-b5": {
|
|
1175
|
+
id: "jis-b5",
|
|
1176
|
+
label: "B5(JIS)",
|
|
1177
|
+
widthMm: 182,
|
|
1178
|
+
heightMm: 257
|
|
1179
|
+
},
|
|
1180
|
+
"jis-b6": {
|
|
1181
|
+
id: "jis-b6",
|
|
1182
|
+
label: "B6(JIS)",
|
|
1183
|
+
widthMm: 128,
|
|
1184
|
+
heightMm: 182
|
|
1185
|
+
},
|
|
1186
|
+
shinsho: {
|
|
1187
|
+
id: "shinsho",
|
|
1188
|
+
label: "新書",
|
|
1189
|
+
widthMm: 106,
|
|
1190
|
+
heightMm: 173
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
const PaperSize = {
|
|
1194
|
+
of: (id) => PAPER_SIZES[id],
|
|
1195
|
+
all: Object.values(PAPER_SIZES)
|
|
1196
|
+
};
|
|
1197
|
+
//#endregion
|
|
1198
|
+
//#region src/internal/length.ts
|
|
1199
|
+
const PT_PER_MM = 72 / 25.4;
|
|
1200
|
+
function ptToMm(pt) {
|
|
1201
|
+
return pt / PT_PER_MM;
|
|
1202
|
+
}
|
|
1203
|
+
function mmToPt(mm) {
|
|
1204
|
+
return mm * PT_PER_MM;
|
|
1205
|
+
}
|
|
1206
|
+
//#endregion
|
|
1207
|
+
//#region src/composer/manuscript-geometry.ts
|
|
1208
|
+
const STAGE_GAP_CELLS = 2;
|
|
1209
|
+
const LINE_GAP_CELLS = .5;
|
|
1210
|
+
const nonNegativeLength = () => v.pipe(v.number(), v.finite(), v.minValue(0));
|
|
1211
|
+
const ManuscriptGeometrySchema = readonlyObject({
|
|
1212
|
+
paperWidthMm: nonNegativeLength(),
|
|
1213
|
+
paperHeightMm: nonNegativeLength(),
|
|
1214
|
+
cellSizeMm: nonNegativeLength(),
|
|
1215
|
+
lineGapMm: nonNegativeLength(),
|
|
1216
|
+
fontSizePt: FontSizePt.schema,
|
|
1217
|
+
contentWidthMm: nonNegativeLength(),
|
|
1218
|
+
contentHeightMm: nonNegativeLength(),
|
|
1219
|
+
marginInlineMm: v.pipe(v.number(), v.finite()),
|
|
1220
|
+
marginBlockMm: v.pipe(v.number(), v.finite()),
|
|
1221
|
+
fitsPaper: v.boolean()
|
|
1222
|
+
});
|
|
1223
|
+
/**
|
|
1224
|
+
* Every stage stacked vertically, plus the gap between adjacent stages.
|
|
1225
|
+
*/
|
|
1226
|
+
function contentHeightInEm(flow) {
|
|
1227
|
+
return flow.stagesPerPage * flow.lineLengthEm + STAGE_GAP_CELLS * (flow.stagesPerPage - 1);
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Every vertical line side by side, plus the gap between adjacent lines.
|
|
1231
|
+
*/
|
|
1232
|
+
function contentWidthInEm(flow) {
|
|
1233
|
+
return flow.linesPerStage + LINE_GAP_CELLS * (flow.linesPerStage - 1);
|
|
1234
|
+
}
|
|
1235
|
+
const ManuscriptGeometry = {
|
|
1236
|
+
schema: ManuscriptGeometrySchema,
|
|
1237
|
+
of: (flow, appearance) => {
|
|
1238
|
+
const paper = PaperSize.of(appearance.paperSize);
|
|
1239
|
+
const cellSizeMm = ptToMm(appearance.fontSizePt);
|
|
1240
|
+
const contentWidthMm = contentWidthInEm(flow) * cellSizeMm;
|
|
1241
|
+
const contentHeightMm = contentHeightInEm(flow) * cellSizeMm;
|
|
1242
|
+
const marginInlineMm = (paper.widthMm - contentWidthMm) / 2;
|
|
1243
|
+
const marginBlockMm = (paper.heightMm - contentHeightMm) / 2;
|
|
1244
|
+
return {
|
|
1245
|
+
paperWidthMm: paper.widthMm,
|
|
1246
|
+
paperHeightMm: paper.heightMm,
|
|
1247
|
+
cellSizeMm,
|
|
1248
|
+
lineGapMm: LINE_GAP_CELLS * cellSizeMm,
|
|
1249
|
+
fontSizePt: appearance.fontSizePt,
|
|
1250
|
+
contentWidthMm,
|
|
1251
|
+
contentHeightMm,
|
|
1252
|
+
marginInlineMm,
|
|
1253
|
+
marginBlockMm,
|
|
1254
|
+
fitsPaper: marginInlineMm >= 0 && marginBlockMm >= 0
|
|
1255
|
+
};
|
|
1256
|
+
},
|
|
1257
|
+
/**
|
|
1258
|
+
* The largest step-aligned point size whose text area still fits the paper.
|
|
1259
|
+
*/
|
|
1260
|
+
maxFontSizePt: (flow, paperSizeId) => {
|
|
1261
|
+
const paper = PaperSize.of(paperSizeId);
|
|
1262
|
+
const maxCellSizeMm = Math.min(paper.widthMm / contentWidthInEm(flow), paper.heightMm / contentHeightInEm(flow));
|
|
1263
|
+
const steps = Math.floor(mmToPt(maxCellSizeMm) / FontSizePt.range.step + 1e-9);
|
|
1264
|
+
const aligned = Math.round(steps * FontSizePt.range.step * 1e3) / 1e3;
|
|
1265
|
+
return Math.min(FontSizePt.range.max, Math.max(FontSizePt.range.min, aligned));
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
const CompositionStatistics = { schema: readonlyObject({
|
|
1269
|
+
chars: nonNegativeInteger(),
|
|
1270
|
+
sourceLines: nonNegativeInteger(),
|
|
1271
|
+
pages: nonNegativeInteger()
|
|
1272
|
+
}) };
|
|
1273
|
+
//#endregion
|
|
1274
|
+
//#region src/composer/composed-annotation-fragment.ts
|
|
1275
|
+
const emLength = () => v.pipe(v.number(), v.finite(), v.minValue(0));
|
|
1276
|
+
const continuation = v.union([
|
|
1277
|
+
v.literal("whole"),
|
|
1278
|
+
v.literal("start"),
|
|
1279
|
+
v.literal("middle"),
|
|
1280
|
+
v.literal("end")
|
|
1281
|
+
]);
|
|
1282
|
+
const PositionedReadingGraphemeSchema = readonlyObject({
|
|
1283
|
+
value: v.string(),
|
|
1284
|
+
offsetEm: v.pipe(v.number(), v.finite()),
|
|
1285
|
+
advanceEm: emLength()
|
|
1286
|
+
});
|
|
1287
|
+
const common$1 = {
|
|
1288
|
+
annotationRange: ManuscriptRange.schema,
|
|
1289
|
+
fragmentRange: ManuscriptRange.schema,
|
|
1290
|
+
continuation
|
|
1291
|
+
};
|
|
1292
|
+
const ComposedAnnotationFragment = { schema: v.variant("kind", [
|
|
1293
|
+
readonlyObject({
|
|
1294
|
+
kind: v.literal("bold"),
|
|
1295
|
+
...common$1
|
|
1296
|
+
}),
|
|
1297
|
+
readonlyObject({
|
|
1298
|
+
kind: v.literal("italic"),
|
|
1299
|
+
...common$1
|
|
1300
|
+
}),
|
|
1301
|
+
readonlyObject({
|
|
1302
|
+
kind: v.literal("emphasis"),
|
|
1303
|
+
mark: v.string(),
|
|
1304
|
+
...common$1
|
|
1305
|
+
}),
|
|
1306
|
+
readonlyObject({
|
|
1307
|
+
kind: v.literal("ruby"),
|
|
1308
|
+
rubyKind: RubyKind.schema,
|
|
1309
|
+
reading: v.string(),
|
|
1310
|
+
baseOffsetEm: emLength(),
|
|
1311
|
+
baseAdvanceEm: emLength(),
|
|
1312
|
+
readingGraphemes: readonlyArray(PositionedReadingGraphemeSchema),
|
|
1313
|
+
...common$1
|
|
1314
|
+
})
|
|
1315
|
+
]) };
|
|
1316
|
+
const InlineSpan = { schema: readonlyObject({
|
|
1317
|
+
offsetEm: v.pipe(v.number(), v.finite()),
|
|
1318
|
+
advanceEm: v.pipe(v.number(), v.finite(), v.minValue(0))
|
|
1319
|
+
}) };
|
|
1320
|
+
//#endregion
|
|
1321
|
+
//#region src/composer/vertical-text-presentation.ts
|
|
1322
|
+
const common = { groupRange: ManuscriptRange.schema };
|
|
1323
|
+
const VerticalTextPresentation = { schema: v.variant("kind", [
|
|
1324
|
+
readonlyObject({
|
|
1325
|
+
kind: v.literal("mixed"),
|
|
1326
|
+
...common
|
|
1327
|
+
}),
|
|
1328
|
+
readonlyObject({
|
|
1329
|
+
kind: v.literal("upright"),
|
|
1330
|
+
...common
|
|
1331
|
+
}),
|
|
1332
|
+
readonlyObject({
|
|
1333
|
+
kind: v.literal("sideways"),
|
|
1334
|
+
...common
|
|
1335
|
+
}),
|
|
1336
|
+
readonlyObject({
|
|
1337
|
+
kind: v.literal("tate-chu-yoko"),
|
|
1338
|
+
...common
|
|
1339
|
+
})
|
|
1340
|
+
]) };
|
|
1341
|
+
//#endregion
|
|
1342
|
+
//#region src/composer/composed-inline-item.ts
|
|
1343
|
+
const signedEm = () => v.pipe(v.number(), v.finite());
|
|
1344
|
+
const GlyphItemSchema = readonlyObject({
|
|
1345
|
+
kind: v.literal("glyph"),
|
|
1346
|
+
value: v.string(),
|
|
1347
|
+
range: ManuscriptRange.schema,
|
|
1348
|
+
layoutSpan: InlineSpan.schema,
|
|
1349
|
+
renderSpan: InlineSpan.schema,
|
|
1350
|
+
disposition: v.union([v.literal("placed"), v.literal("hanging")]),
|
|
1351
|
+
presentation: VerticalTextPresentation.schema
|
|
1352
|
+
});
|
|
1353
|
+
const GeneratedGlueItemSchema = readonlyObject({
|
|
1354
|
+
kind: v.literal("glue"),
|
|
1355
|
+
origin: v.literal("generated"),
|
|
1356
|
+
offsetEm: signedEm(),
|
|
1357
|
+
widthEm: signedEm(),
|
|
1358
|
+
naturalWidthEm: signedEm(),
|
|
1359
|
+
adjustment: v.union([
|
|
1360
|
+
v.literal("natural"),
|
|
1361
|
+
v.literal("shrunk"),
|
|
1362
|
+
v.literal("stretched")
|
|
1363
|
+
])
|
|
1364
|
+
});
|
|
1365
|
+
const SourceGlueItemSchema = readonlyObject({
|
|
1366
|
+
kind: v.literal("glue"),
|
|
1367
|
+
origin: v.literal("source"),
|
|
1368
|
+
value: v.string(),
|
|
1369
|
+
range: ManuscriptRange.schema,
|
|
1370
|
+
offsetEm: signedEm(),
|
|
1371
|
+
widthEm: signedEm(),
|
|
1372
|
+
naturalWidthEm: signedEm(),
|
|
1373
|
+
adjustment: v.union([
|
|
1374
|
+
v.literal("natural"),
|
|
1375
|
+
v.literal("shrunk"),
|
|
1376
|
+
v.literal("stretched")
|
|
1377
|
+
])
|
|
1378
|
+
});
|
|
1379
|
+
const KernItemSchema = readonlyObject({
|
|
1380
|
+
kind: v.literal("kern"),
|
|
1381
|
+
offsetEm: signedEm(),
|
|
1382
|
+
widthEm: signedEm()
|
|
1383
|
+
});
|
|
1384
|
+
const SuppressedItemSchema = readonlyObject({
|
|
1385
|
+
kind: v.literal("suppressed"),
|
|
1386
|
+
value: v.string(),
|
|
1387
|
+
range: ManuscriptRange.schema,
|
|
1388
|
+
reason: v.literal("question-or-exclamation-gap")
|
|
1389
|
+
});
|
|
1390
|
+
const ComposedInlineItem = { schema: v.union([
|
|
1391
|
+
GlyphItemSchema,
|
|
1392
|
+
GeneratedGlueItemSchema,
|
|
1393
|
+
SourceGlueItemSchema,
|
|
1394
|
+
KernItemSchema,
|
|
1395
|
+
SuppressedItemSchema
|
|
1396
|
+
]) };
|
|
1397
|
+
const LineBreakResult = { schema: v.variant("kind", [
|
|
1398
|
+
readonlyObject({ kind: v.literal("natural") }),
|
|
1399
|
+
readonlyObject({ kind: v.literal("shrunk") }),
|
|
1400
|
+
readonlyObject({ kind: v.literal("stretched") }),
|
|
1401
|
+
readonlyObject({ kind: v.literal("hanging") }),
|
|
1402
|
+
readonlyObject({ kind: v.literal("forced") }),
|
|
1403
|
+
readonlyObject({ kind: v.literal("paragraph-end") })
|
|
1404
|
+
]) };
|
|
1405
|
+
const NovelLine = {
|
|
1406
|
+
schema: readonlyObject({
|
|
1407
|
+
range: v.nullable(ManuscriptRange.schema),
|
|
1408
|
+
inlineSizeEm: v.pipe(v.number(), v.finite(), v.minValue(0)),
|
|
1409
|
+
items: readonlyArray(ComposedInlineItem.schema),
|
|
1410
|
+
break: LineBreakResult.schema,
|
|
1411
|
+
annotations: readonlyArray(ComposedAnnotationFragment.schema)
|
|
1412
|
+
}),
|
|
1413
|
+
empty: () => ({
|
|
1414
|
+
range: null,
|
|
1415
|
+
inlineSizeEm: 0,
|
|
1416
|
+
items: [],
|
|
1417
|
+
break: { kind: "paragraph-end" },
|
|
1418
|
+
annotations: []
|
|
1419
|
+
})
|
|
1420
|
+
};
|
|
1421
|
+
const NovelStage = {
|
|
1422
|
+
schema: readonlyObject({
|
|
1423
|
+
range: v.nullable(ManuscriptRange.schema),
|
|
1424
|
+
lines: readonlyArray(NovelLine.schema)
|
|
1425
|
+
}),
|
|
1426
|
+
of: (lines) => ({
|
|
1427
|
+
lines,
|
|
1428
|
+
range: ManuscriptRange.merge(lines.map(({ range }) => range))
|
|
1429
|
+
})
|
|
1430
|
+
};
|
|
1431
|
+
const NovelPage = {
|
|
1432
|
+
schema: readonlyObject({
|
|
1433
|
+
range: v.nullable(ManuscriptRange.schema),
|
|
1434
|
+
stages: readonlyArray(NovelStage.schema)
|
|
1435
|
+
}),
|
|
1436
|
+
of: (stages) => ({
|
|
1437
|
+
stages,
|
|
1438
|
+
range: ManuscriptRange.merge(stages.map(({ range }) => range))
|
|
1439
|
+
})
|
|
1440
|
+
};
|
|
1441
|
+
const NovelLayout = { schema: readonlyObject({
|
|
1442
|
+
pages: readonlyArray(NovelPage.schema),
|
|
1443
|
+
geometry: ManuscriptGeometry.schema,
|
|
1444
|
+
stats: CompositionStatistics.schema
|
|
1445
|
+
}) };
|
|
1446
|
+
//#endregion
|
|
1447
|
+
//#region src/composer/novel-composer.ts
|
|
1448
|
+
const COMPOSER_ID = NamespacedId.of("kg/novel");
|
|
1449
|
+
const TYPESETTING_PROFILE = defaultJapaneseTypesettingProfile;
|
|
1450
|
+
const ASCII_ALPHANUMERIC = /^[A-Za-z0-9]$/u;
|
|
1451
|
+
const ASCII_TWO_DIGITS = /^[0-9]{2}$/u;
|
|
1452
|
+
const UPRIGHT_LATIN_ABBREVIATION = /^(?:[A-Z]+|[A-Z][a-z]{1,2})$/u;
|
|
1453
|
+
const FULLWIDTH_ALPHANUMERIC = /^[A-Za-z0-9]$/u;
|
|
1454
|
+
function presentation(kind, graphemes) {
|
|
1455
|
+
const groupRange = ManuscriptRange.merge(graphemes.map(({ range }) => range));
|
|
1456
|
+
return groupRange === null ? void 0 : {
|
|
1457
|
+
kind,
|
|
1458
|
+
groupRange
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
function presentedSourceLine(sourceLine) {
|
|
1462
|
+
const presented = [];
|
|
1463
|
+
let cursor = 0;
|
|
1464
|
+
while (cursor < sourceLine.length) {
|
|
1465
|
+
const first = sourceLine[cursor];
|
|
1466
|
+
if (first === void 0) break;
|
|
1467
|
+
if (!ASCII_ALPHANUMERIC.test(first.value)) {
|
|
1468
|
+
const itemPresentation = presentation(FULLWIDTH_ALPHANUMERIC.test(first.value) ? "upright" : "mixed", [first]);
|
|
1469
|
+
if (itemPresentation !== void 0) presented.push({
|
|
1470
|
+
grapheme: first,
|
|
1471
|
+
presentation: itemPresentation
|
|
1472
|
+
});
|
|
1473
|
+
cursor += 1;
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
const start = cursor;
|
|
1477
|
+
while (ASCII_ALPHANUMERIC.test(sourceLine[cursor]?.value ?? "")) cursor += 1;
|
|
1478
|
+
const run = sourceLine.slice(start, cursor);
|
|
1479
|
+
const text = run.map(({ value }) => value).join("");
|
|
1480
|
+
const runPresentation = presentation(ASCII_TWO_DIGITS.test(text) ? "tate-chu-yoko" : text.length === 1 || UPRIGHT_LATIN_ABBREVIATION.test(text) ? "upright" : "sideways", run);
|
|
1481
|
+
if (runPresentation !== void 0) presented.push(...run.map((grapheme) => ({
|
|
1482
|
+
grapheme,
|
|
1483
|
+
presentation: runPresentation
|
|
1484
|
+
})));
|
|
1485
|
+
}
|
|
1486
|
+
return presented;
|
|
1487
|
+
}
|
|
1488
|
+
function displayedLines(graphemes) {
|
|
1489
|
+
const lines = [[]];
|
|
1490
|
+
let endedWithLineBreak = false;
|
|
1491
|
+
for (const grapheme of graphemes) {
|
|
1492
|
+
if (grapheme.value === "\n" || grapheme.value === "\r" || grapheme.value === "\r\n") {
|
|
1493
|
+
lines.push([]);
|
|
1494
|
+
endedWithLineBreak = true;
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
lines.at(-1)?.push(grapheme);
|
|
1498
|
+
endedWithLineBreak = false;
|
|
1499
|
+
}
|
|
1500
|
+
if (lines.length > 1 && endedWithLineBreak) lines.pop();
|
|
1501
|
+
return lines;
|
|
1502
|
+
}
|
|
1503
|
+
function validGapIndexes(sourceLine) {
|
|
1504
|
+
const line = sourceLine.map(({ value }) => value).join("");
|
|
1505
|
+
const gapStarts = new Set(questionOrExclamationSpacings(line).filter((spacing) => spacing.kind === "valid").map(({ gap }) => gap.start));
|
|
1506
|
+
const indexes = /* @__PURE__ */ new Set();
|
|
1507
|
+
let offset = 0;
|
|
1508
|
+
for (const [index, grapheme] of sourceLine.entries()) {
|
|
1509
|
+
if (gapStarts.has(offset)) indexes.add(index);
|
|
1510
|
+
offset += grapheme.value.length;
|
|
1511
|
+
}
|
|
1512
|
+
return indexes;
|
|
1513
|
+
}
|
|
1514
|
+
function indicesInside(atoms, annotation) {
|
|
1515
|
+
return atoms.flatMap(({ grapheme }, index) => ManuscriptRange.overlaps(grapheme.range, annotation.range) ? [index] : []);
|
|
1516
|
+
}
|
|
1517
|
+
function measure(measurer, text, role, settings, presentationKind) {
|
|
1518
|
+
const context = {
|
|
1519
|
+
text,
|
|
1520
|
+
fontPreset: settings.appearance.fontPreset,
|
|
1521
|
+
writingMode: "vertical-rl"
|
|
1522
|
+
};
|
|
1523
|
+
const measurement = role === "base" ? measurer({
|
|
1524
|
+
...context,
|
|
1525
|
+
role,
|
|
1526
|
+
presentation: presentationKind ?? "mixed"
|
|
1527
|
+
}) : measurer({
|
|
1528
|
+
...context,
|
|
1529
|
+
role
|
|
1530
|
+
});
|
|
1531
|
+
const parsed = v.safeParse(InlineMeasurement.schema, measurement);
|
|
1532
|
+
if (!parsed.success) return void 0;
|
|
1533
|
+
const { advanceEm } = parsed.output;
|
|
1534
|
+
return advanceEm;
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Widen a base character by `extraEm` so a reading longer than it has room, keeping the character's
|
|
1538
|
+
* ink in the middle of the widened box.
|
|
1539
|
+
*
|
|
1540
|
+
* JLReq 3.3.6 sets such a reading solid and spends the surplus on the base instead: two units
|
|
1541
|
+
* between the base characters for one unit before the first and after the last. Handing every base
|
|
1542
|
+
* character an equal share of the surplus and centring it inside that share produces exactly those
|
|
1543
|
+
* proportions, and it is also the 中付き position JLReq 3.3.5 asks of a mono ruby whose reading
|
|
1544
|
+
* outruns its single base character.
|
|
1545
|
+
*/
|
|
1546
|
+
function widenForReading(atom, extraEm) {
|
|
1547
|
+
if (extraEm <= 0) return;
|
|
1548
|
+
atom.boxAdvanceEm += extraEm;
|
|
1549
|
+
atom.renderOffsetEm += extraEm / 2;
|
|
1550
|
+
}
|
|
1551
|
+
function measureSourceLine(sourceLine, annotations, settings, measurer) {
|
|
1552
|
+
const mutable = presentedSourceLine(sourceLine).map(({ grapheme, presentation }) => {
|
|
1553
|
+
const renderAdvanceEm = measure(measurer, grapheme.value, "base", settings, presentation.kind);
|
|
1554
|
+
if (renderAdvanceEm === void 0) return void 0;
|
|
1555
|
+
const characterClass = TYPESETTING_PROFILE.classify({
|
|
1556
|
+
value: grapheme.value,
|
|
1557
|
+
presentation: presentation.kind
|
|
1558
|
+
});
|
|
1559
|
+
const metrics = TYPESETTING_PROFILE.boxMetrics(characterClass, renderAdvanceEm);
|
|
1560
|
+
return {
|
|
1561
|
+
grapheme,
|
|
1562
|
+
boxAdvanceEm: metrics.advanceEm,
|
|
1563
|
+
renderAdvanceEm,
|
|
1564
|
+
renderOffsetEm: metrics.renderOffsetEm,
|
|
1565
|
+
presentation
|
|
1566
|
+
};
|
|
1567
|
+
});
|
|
1568
|
+
if (mutable.some((atom) => atom === void 0)) return void 0;
|
|
1569
|
+
const atoms = mutable.flatMap((atom) => atom === void 0 ? [] : [atom]);
|
|
1570
|
+
for (const annotation of annotations) {
|
|
1571
|
+
if (annotation.kind !== "ruby") continue;
|
|
1572
|
+
const indexes = indicesInside(atoms, annotation);
|
|
1573
|
+
if (indexes.length === 0) continue;
|
|
1574
|
+
if ((annotation.reading.kind === "group" ? [annotation.reading.text] : annotation.reading.segments).some((text) => [...graphemeSegmenter.segment(text)].some(({ segment }) => measure(measurer, segment, "ruby", settings) === void 0))) return void 0;
|
|
1575
|
+
if (annotation.reading.kind === "group") {
|
|
1576
|
+
const readingAdvance = measure(measurer, annotation.reading.text, "ruby", settings);
|
|
1577
|
+
if (readingAdvance === void 0) return void 0;
|
|
1578
|
+
const baseAdvance = indexes.reduce((total, index) => total + (atoms[index]?.boxAdvanceEm ?? 0), 0);
|
|
1579
|
+
const extra = Math.max(0, readingAdvance - baseAdvance) / indexes.length;
|
|
1580
|
+
for (const index of indexes) {
|
|
1581
|
+
const atom = atoms[index];
|
|
1582
|
+
if (atom !== void 0) widenForReading(atom, extra);
|
|
1583
|
+
}
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
for (const [segmentIndex, index] of indexes.entries()) {
|
|
1587
|
+
const segment = annotation.reading.segments[segmentIndex];
|
|
1588
|
+
const atom = atoms[index];
|
|
1589
|
+
if (segment === void 0 || atom === void 0) continue;
|
|
1590
|
+
const readingAdvance = measure(measurer, segment, "ruby", settings);
|
|
1591
|
+
if (readingAdvance === void 0) return void 0;
|
|
1592
|
+
widenForReading(atom, readingAdvance - atom.boxAdvanceEm);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
return {
|
|
1596
|
+
atoms,
|
|
1597
|
+
suppressedIndexes: validGapIndexes(sourceLine)
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
function breakInsideFittableGroupRuby(atoms, annotations, boundary, lineLengthEm) {
|
|
1601
|
+
const right = atoms[boundary];
|
|
1602
|
+
if (right === void 0) return false;
|
|
1603
|
+
const graphemeIndex = right.grapheme.range.graphemes.start;
|
|
1604
|
+
return annotations.some((annotation) => {
|
|
1605
|
+
if (annotation.kind !== "ruby" || annotation.reading.kind !== "group") return false;
|
|
1606
|
+
if (graphemeIndex <= annotation.range.graphemes.start || graphemeIndex >= annotation.range.graphemes.end) return false;
|
|
1607
|
+
return indicesInside(atoms, annotation).reduce((total, index) => total + (atoms[index]?.boxAdvanceEm ?? 0), 0) <= lineLengthEm;
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
function samePresentationGroup(left, right) {
|
|
1611
|
+
return left.presentation.groupRange.graphemes.start === right.presentation.groupRange.graphemes.start && left.presentation.groupRange.graphemes.end === right.presentation.groupRange.graphemes.end;
|
|
1612
|
+
}
|
|
1613
|
+
function legalBoundary(atoms, annotations, leftIndex, rightIndex, lineLengthEm, profile) {
|
|
1614
|
+
const left = atoms[leftIndex];
|
|
1615
|
+
const right = atoms[rightIndex];
|
|
1616
|
+
if (left === void 0 || right === void 0) return true;
|
|
1617
|
+
return !samePresentationGroup(left, right) && profile.breakPenalty(profile.classify({
|
|
1618
|
+
value: left.grapheme.value,
|
|
1619
|
+
presentation: left.presentation.kind
|
|
1620
|
+
}), profile.classify({
|
|
1621
|
+
value: right.grapheme.value,
|
|
1622
|
+
presentation: right.presentation.kind
|
|
1623
|
+
})) !== null && !breakInsideFittableGroupRuby(atoms, annotations, rightIndex, lineLengthEm);
|
|
1624
|
+
}
|
|
1625
|
+
function continuationFor(annotation, range) {
|
|
1626
|
+
const starts = range.graphemes.start === annotation.range.graphemes.start;
|
|
1627
|
+
const ends = range.graphemes.end === annotation.range.graphemes.end;
|
|
1628
|
+
if (starts && ends) return "whole";
|
|
1629
|
+
if (starts) return "start";
|
|
1630
|
+
if (ends) return "end";
|
|
1631
|
+
return "middle";
|
|
1632
|
+
}
|
|
1633
|
+
function groupReadingsForFragments(annotation, fragmentRanges, baseAdvances) {
|
|
1634
|
+
const baseLength = annotation.range.graphemes.end - annotation.range.graphemes.start;
|
|
1635
|
+
if (annotation.reading.kind !== "group") return [];
|
|
1636
|
+
const reading = [...graphemeSegmenter.segment(annotation.reading.text)].map(({ segment }) => segment);
|
|
1637
|
+
const advances = Array.from({ length: baseLength }, (_, index) => baseAdvances.get(annotation.range.graphemes.start + index));
|
|
1638
|
+
const measured = advances.every((advance) => advance !== void 0);
|
|
1639
|
+
const advanceFor = (fragment) => {
|
|
1640
|
+
const fragmentStart = fragment.graphemes.start - annotation.range.graphemes.start;
|
|
1641
|
+
const fragmentLength = fragment.graphemes.end - fragment.graphemes.start;
|
|
1642
|
+
return measured ? advances.slice(fragmentStart, fragmentStart + fragmentLength).reduce((total, advance) => total + advance, 0) : fragmentLength;
|
|
1643
|
+
};
|
|
1644
|
+
const weights = fragmentRanges.map(advanceFor);
|
|
1645
|
+
const effectiveWeights = weights.reduce((total, weight) => total + weight, 0) > 0 ? weights : fragmentRanges.map(({ graphemes }) => graphemes.end - graphemes.start);
|
|
1646
|
+
const totalWeight = effectiveWeights.reduce((total, weight) => total + weight, 0);
|
|
1647
|
+
const reserveEveryFragment = reading.length >= fragmentRanges.length;
|
|
1648
|
+
const fragments = [];
|
|
1649
|
+
let readingStart = 0;
|
|
1650
|
+
let accumulatedWeight = 0;
|
|
1651
|
+
for (const [index, weight] of effectiveWeights.entries()) {
|
|
1652
|
+
accumulatedWeight += weight;
|
|
1653
|
+
const remainingFragments = effectiveWeights.length - index - 1;
|
|
1654
|
+
const proportionalEnd = index === effectiveWeights.length - 1 ? reading.length : Math.round(accumulatedWeight * reading.length / totalWeight);
|
|
1655
|
+
const minimumEnd = reserveEveryFragment ? readingStart + 1 : readingStart;
|
|
1656
|
+
const maximumEnd = reserveEveryFragment ? reading.length - remainingFragments : reading.length;
|
|
1657
|
+
const readingEnd = Math.min(maximumEnd, Math.max(minimumEnd, proportionalEnd));
|
|
1658
|
+
fragments.push(reading.slice(readingStart, readingEnd).join(""));
|
|
1659
|
+
readingStart = readingEnd;
|
|
1660
|
+
}
|
|
1661
|
+
return fragments;
|
|
1662
|
+
}
|
|
1663
|
+
function readingForFragment(annotation, range, fragmentPlan) {
|
|
1664
|
+
const start = range.graphemes.start - annotation.range.graphemes.start;
|
|
1665
|
+
const length = range.graphemes.end - range.graphemes.start;
|
|
1666
|
+
if (annotation.reading.kind !== "group") return {
|
|
1667
|
+
kind: annotation.reading.kind,
|
|
1668
|
+
text: annotation.reading.segments.slice(start, start + length).join("")
|
|
1669
|
+
};
|
|
1670
|
+
const fragmentIndex = fragmentPlan.ranges.findIndex(({ graphemes }) => graphemes.start === range.graphemes.start && graphemes.end === range.graphemes.end);
|
|
1671
|
+
return {
|
|
1672
|
+
kind: "group",
|
|
1673
|
+
text: fragmentPlan.groupReadings[fragmentIndex] ?? ""
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
function positionReading(text, baseOffsetEm, baseAdvanceEm, settings, measurer) {
|
|
1677
|
+
const measured = [...graphemeSegmenter.segment(text)].map(({ segment }) => ({
|
|
1678
|
+
value: segment,
|
|
1679
|
+
advanceEm: measure(measurer, segment, "ruby", settings) ?? 0
|
|
1680
|
+
}));
|
|
1681
|
+
const total = measured.reduce((sum, grapheme) => sum + grapheme.advanceEm, 0);
|
|
1682
|
+
const edgeGap = measured.length > 0 && total < baseAdvanceEm ? (baseAdvanceEm - total) / (2 * measured.length) : 0;
|
|
1683
|
+
let offsetEm = baseOffsetEm + (total > baseAdvanceEm ? (baseAdvanceEm - total) / 2 : edgeGap);
|
|
1684
|
+
return measured.map((grapheme, index) => {
|
|
1685
|
+
const positioned = {
|
|
1686
|
+
value: grapheme.value,
|
|
1687
|
+
advanceEm: grapheme.advanceEm,
|
|
1688
|
+
offsetEm
|
|
1689
|
+
};
|
|
1690
|
+
offsetEm += grapheme.advanceEm;
|
|
1691
|
+
if (index < measured.length - 1) offsetEm += edgeGap * 2;
|
|
1692
|
+
return positioned;
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
function annotationFragmentRange(graphemes, suppressed, sourceGlues, annotation) {
|
|
1696
|
+
return ManuscriptRange.merge([
|
|
1697
|
+
...graphemes.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range),
|
|
1698
|
+
...suppressed.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range),
|
|
1699
|
+
...sourceGlues.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range)).map(({ range }) => range)
|
|
1700
|
+
]);
|
|
1701
|
+
}
|
|
1702
|
+
function annotationFragments(graphemes, suppressed, sourceGlues, annotations, settings, measurer, fragmentPlansByAnnotation) {
|
|
1703
|
+
const fragments = [];
|
|
1704
|
+
for (const annotation of annotations) {
|
|
1705
|
+
const covered = graphemes.filter(({ range }) => ManuscriptRange.overlaps(range, annotation.range));
|
|
1706
|
+
const fragmentRange = annotationFragmentRange(graphemes, suppressed, sourceGlues, annotation);
|
|
1707
|
+
const first = covered[0];
|
|
1708
|
+
const last = covered.at(-1);
|
|
1709
|
+
if (fragmentRange === null || first === void 0 || last === void 0) continue;
|
|
1710
|
+
const common = {
|
|
1711
|
+
annotationRange: annotation.range,
|
|
1712
|
+
fragmentRange,
|
|
1713
|
+
continuation: continuationFor(annotation, fragmentRange)
|
|
1714
|
+
};
|
|
1715
|
+
switch (annotation.kind) {
|
|
1716
|
+
case "bold":
|
|
1717
|
+
case "italic":
|
|
1718
|
+
fragments.push({
|
|
1719
|
+
kind: annotation.kind,
|
|
1720
|
+
...common
|
|
1721
|
+
});
|
|
1722
|
+
break;
|
|
1723
|
+
case "emphasis":
|
|
1724
|
+
fragments.push({
|
|
1725
|
+
kind: "emphasis",
|
|
1726
|
+
mark: annotation.mark,
|
|
1727
|
+
...common
|
|
1728
|
+
});
|
|
1729
|
+
break;
|
|
1730
|
+
case "ruby": {
|
|
1731
|
+
const fragmentPlan = fragmentPlansByAnnotation.get(annotation);
|
|
1732
|
+
if (fragmentPlan === void 0) break;
|
|
1733
|
+
const baseOffsetEm = first.layoutSpan.offsetEm;
|
|
1734
|
+
const baseAdvanceEm = last.layoutSpan.offsetEm + last.layoutSpan.advanceEm - baseOffsetEm;
|
|
1735
|
+
const reading = readingForFragment(annotation, fragmentRange, fragmentPlan);
|
|
1736
|
+
fragments.push({
|
|
1737
|
+
kind: "ruby",
|
|
1738
|
+
rubyKind: reading.kind,
|
|
1739
|
+
reading: reading.text,
|
|
1740
|
+
baseOffsetEm,
|
|
1741
|
+
baseAdvanceEm,
|
|
1742
|
+
readingGraphemes: positionReading(reading.text, baseOffsetEm, baseAdvanceEm, settings, measurer),
|
|
1743
|
+
...common
|
|
1744
|
+
});
|
|
1745
|
+
break;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
return fragments;
|
|
1750
|
+
}
|
|
1751
|
+
function positionedLine(atoms, sourceGapIndexes, plan) {
|
|
1752
|
+
const items = plan.suppressedIndexes.flatMap((index) => {
|
|
1753
|
+
const atom = atoms[index];
|
|
1754
|
+
return atom === void 0 ? [] : [{
|
|
1755
|
+
kind: "suppressed",
|
|
1756
|
+
value: atom.grapheme.value,
|
|
1757
|
+
range: atom.grapheme.range,
|
|
1758
|
+
reason: "question-or-exclamation-gap"
|
|
1759
|
+
}];
|
|
1760
|
+
});
|
|
1761
|
+
const spacings = new Map(plan.pairSpacings.map((spacing) => [spacing.boundary, spacing]));
|
|
1762
|
+
let offsetEm = 0;
|
|
1763
|
+
const positionSpacing = (spacing) => {
|
|
1764
|
+
if (spacing.kind === "kern") items.push({
|
|
1765
|
+
kind: "kern",
|
|
1766
|
+
offsetEm,
|
|
1767
|
+
widthEm: spacing.widthEm
|
|
1768
|
+
});
|
|
1769
|
+
else items.push({
|
|
1770
|
+
kind: "glue",
|
|
1771
|
+
origin: "generated",
|
|
1772
|
+
offsetEm,
|
|
1773
|
+
widthEm: spacing.widthEm,
|
|
1774
|
+
naturalWidthEm: spacing.naturalWidthEm,
|
|
1775
|
+
adjustment: spacing.widthEm < spacing.naturalWidthEm ? "shrunk" : spacing.widthEm > spacing.naturalWidthEm ? "stretched" : "natural"
|
|
1776
|
+
});
|
|
1777
|
+
offsetEm += spacing.widthEm;
|
|
1778
|
+
};
|
|
1779
|
+
for (let index = plan.contentStart; index < plan.end; index += 1) {
|
|
1780
|
+
const spacing = spacings.get(index);
|
|
1781
|
+
if (spacing !== void 0) positionSpacing(spacing);
|
|
1782
|
+
const atom = atoms[index];
|
|
1783
|
+
if (atom === void 0) continue;
|
|
1784
|
+
if (sourceGapIndexes.has(index)) {
|
|
1785
|
+
items.push({
|
|
1786
|
+
kind: "glue",
|
|
1787
|
+
origin: "source",
|
|
1788
|
+
value: atom.grapheme.value,
|
|
1789
|
+
range: atom.grapheme.range,
|
|
1790
|
+
offsetEm,
|
|
1791
|
+
widthEm: atom.boxAdvanceEm,
|
|
1792
|
+
naturalWidthEm: atom.boxAdvanceEm,
|
|
1793
|
+
adjustment: "natural"
|
|
1794
|
+
});
|
|
1795
|
+
offsetEm += atom.boxAdvanceEm;
|
|
1796
|
+
continue;
|
|
1797
|
+
}
|
|
1798
|
+
const hanging = plan.hangingIndex === index;
|
|
1799
|
+
const layoutAdvanceEm = hanging ? 0 : atom.boxAdvanceEm;
|
|
1800
|
+
items.push({
|
|
1801
|
+
kind: "glyph",
|
|
1802
|
+
value: atom.grapheme.value,
|
|
1803
|
+
range: atom.grapheme.range,
|
|
1804
|
+
layoutSpan: {
|
|
1805
|
+
offsetEm,
|
|
1806
|
+
advanceEm: layoutAdvanceEm
|
|
1807
|
+
},
|
|
1808
|
+
renderSpan: {
|
|
1809
|
+
offsetEm: offsetEm + atom.renderOffsetEm,
|
|
1810
|
+
advanceEm: atom.renderAdvanceEm
|
|
1811
|
+
},
|
|
1812
|
+
disposition: hanging ? "hanging" : "placed",
|
|
1813
|
+
presentation: atom.presentation
|
|
1814
|
+
});
|
|
1815
|
+
offsetEm += layoutAdvanceEm;
|
|
1816
|
+
}
|
|
1817
|
+
const trailingSpacing = spacings.get(plan.end);
|
|
1818
|
+
if (trailingSpacing !== void 0) positionSpacing(trailingSpacing);
|
|
1819
|
+
const ranges = items.flatMap((item) => item.kind === "glyph" || item.kind === "suppressed" || item.kind === "glue" && item.origin === "source" ? [item.range] : []);
|
|
1820
|
+
return {
|
|
1821
|
+
range: ManuscriptRange.merge(ranges),
|
|
1822
|
+
inlineSizeEm: Math.max(0, plan.inlineSizeEm),
|
|
1823
|
+
items,
|
|
1824
|
+
break: plan.break,
|
|
1825
|
+
annotations: []
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
function wrapSourceLine(sourceLine, annotations, settings, measurer, baseAdvances) {
|
|
1829
|
+
if (sourceLine.atoms.length === 0) return [NovelLine.empty()];
|
|
1830
|
+
const profile = TYPESETTING_PROFILE;
|
|
1831
|
+
const lines = layoutParagraph(sourceLine.atoms.map(({ grapheme, boxAdvanceEm, presentation }, index) => {
|
|
1832
|
+
const current = sourceLine.atoms[index];
|
|
1833
|
+
const next = sourceLine.atoms[index + 1];
|
|
1834
|
+
return {
|
|
1835
|
+
value: grapheme.value,
|
|
1836
|
+
boxAdvanceEm,
|
|
1837
|
+
sourceGap: sourceLine.suppressedIndexes.has(index),
|
|
1838
|
+
characterClass: profile.classify({
|
|
1839
|
+
value: grapheme.value,
|
|
1840
|
+
presentation: presentation.kind
|
|
1841
|
+
}),
|
|
1842
|
+
pairSpacingAfter: current === void 0 || next === void 0 || !samePresentationGroup(current, next) && !breakInsideFittableGroupRuby(sourceLine.atoms, annotations, index + 1, settings.flow.lineLengthEm)
|
|
1843
|
+
};
|
|
1844
|
+
}), settings.flow.lineLengthEm, profile, (leftIndex, rightIndex) => legalBoundary(sourceLine.atoms, annotations, leftIndex, rightIndex, settings.flow.lineLengthEm, profile)).map((plan) => positionedLine(sourceLine.atoms, sourceLine.suppressedIndexes, plan));
|
|
1845
|
+
const fragmentPlansByAnnotation = new Map(annotations.map((annotation) => {
|
|
1846
|
+
const ranges = lines.flatMap((line) => {
|
|
1847
|
+
const range = annotationFragmentRange(line.items.filter((item) => item.kind === "glyph"), line.items.filter((item) => item.kind === "suppressed"), line.items.flatMap((item) => item.kind === "glue" && item.origin === "source" ? [item] : []), annotation);
|
|
1848
|
+
return range === null ? [] : [range];
|
|
1849
|
+
});
|
|
1850
|
+
return [annotation, {
|
|
1851
|
+
ranges,
|
|
1852
|
+
groupReadings: annotation.kind === "ruby" ? groupReadingsForFragments(annotation, ranges, baseAdvances) : []
|
|
1853
|
+
}];
|
|
1854
|
+
}));
|
|
1855
|
+
return lines.map((line) => ({
|
|
1856
|
+
range: line.range,
|
|
1857
|
+
inlineSizeEm: line.inlineSizeEm,
|
|
1858
|
+
items: line.items,
|
|
1859
|
+
break: line.break,
|
|
1860
|
+
annotations: annotationFragments(line.items.filter((item) => item.kind === "glyph"), line.items.filter((item) => item.kind === "suppressed"), line.items.flatMap((item) => item.kind === "glue" && item.origin === "source" ? [item] : []), annotations, settings, measurer, fragmentPlansByAnnotation)
|
|
1861
|
+
}));
|
|
1862
|
+
}
|
|
1863
|
+
function blankLines(count) {
|
|
1864
|
+
return Array.from({ length: count }, NovelLine.empty);
|
|
1865
|
+
}
|
|
1866
|
+
function buildPages(contentLines, settings) {
|
|
1867
|
+
const { linesPerStage, stagesPerPage } = settings.flow;
|
|
1868
|
+
const { stage: stageOffset, page: pageOffset } = settings.offsets;
|
|
1869
|
+
const usablePerStage = linesPerStage - LineOffset.total(stageOffset);
|
|
1870
|
+
const usablePerPage = usablePerStage * stagesPerPage;
|
|
1871
|
+
const pages = [];
|
|
1872
|
+
let contentIndex = 0;
|
|
1873
|
+
while (contentIndex < contentLines.length || pages.length === 0) {
|
|
1874
|
+
const stages = [];
|
|
1875
|
+
for (let stageIndex = 0; stageIndex < stagesPerPage; stageIndex += 1) {
|
|
1876
|
+
const lines = [];
|
|
1877
|
+
for (let position = 0; position < linesPerStage; position += 1) {
|
|
1878
|
+
const usableIndex = stageIndex * usablePerStage + position - stageOffset.leading;
|
|
1879
|
+
const withinStage = position >= stageOffset.leading && position < linesPerStage - stageOffset.trailing;
|
|
1880
|
+
const withinPage = usableIndex >= pageOffset.leading && usableIndex < usablePerPage - pageOffset.trailing;
|
|
1881
|
+
const next = contentLines[contentIndex];
|
|
1882
|
+
if (!withinStage || !withinPage || next === void 0) {
|
|
1883
|
+
lines.push(NovelLine.empty());
|
|
1884
|
+
continue;
|
|
1885
|
+
}
|
|
1886
|
+
lines.push(next);
|
|
1887
|
+
contentIndex += 1;
|
|
1888
|
+
}
|
|
1889
|
+
stages.push(NovelStage.of(lines));
|
|
1890
|
+
}
|
|
1891
|
+
pages.push(NovelPage.of(stages));
|
|
1892
|
+
}
|
|
1893
|
+
return pages;
|
|
1894
|
+
}
|
|
1895
|
+
function createCompose(measurer) {
|
|
1896
|
+
return (manuscript, settings) => {
|
|
1897
|
+
const sourceLines = displayedLines(manuscript.graphemes);
|
|
1898
|
+
const measured = sourceLines.map((line) => measureSourceLine(line, manuscript.annotations, settings, measurer));
|
|
1899
|
+
if (measured.some((line) => line === void 0)) return ManuscriptResult.fail({ reason: "inline measurer returned a negative or non-finite value" });
|
|
1900
|
+
const baseAdvances = /* @__PURE__ */ new Map();
|
|
1901
|
+
for (const line of measured) for (const atom of line?.atoms ?? []) baseAdvances.set(atom.grapheme.range.graphemes.start, atom.boxAdvanceEm);
|
|
1902
|
+
const manuscriptLines = measured.flatMap((line) => line === void 0 ? [] : wrapSourceLine(line, manuscript.annotations, settings, measurer, baseAdvances));
|
|
1903
|
+
const pages = buildPages([
|
|
1904
|
+
...blankLines(settings.offsets.document.leading),
|
|
1905
|
+
...manuscriptLines,
|
|
1906
|
+
...blankLines(settings.offsets.document.trailing)
|
|
1907
|
+
], settings);
|
|
1908
|
+
return ManuscriptResult.succeed({
|
|
1909
|
+
pages,
|
|
1910
|
+
geometry: ManuscriptGeometry.of(settings.flow, settings.appearance),
|
|
1911
|
+
stats: {
|
|
1912
|
+
chars: sourceLines.reduce((total, line) => total + line.length, 0),
|
|
1913
|
+
sourceLines: sourceLines.length,
|
|
1914
|
+
pages: pages.length
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
function createNovelComposer(options) {
|
|
1920
|
+
return {
|
|
1921
|
+
id: COMPOSER_ID,
|
|
1922
|
+
settingsSchema: NovelCompositionSettings.schema,
|
|
1923
|
+
layoutSchema: NovelLayout.schema,
|
|
1924
|
+
compose: createCompose(options.measurer)
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
const novelComposer = createNovelComposer({ measurer: logicalInlineMeasurer });
|
|
1928
|
+
//#endregion
|
|
1929
|
+
export { logicalInlineMeasurer as a, ManuscriptOffsets as c, FontSizePt as d, FontPresetId as f, PaperSize as i, ManuscriptAppearanceSettings as l, novelComposer as n, NovelCompositionSettings as o, ParsedManuscript as p, ManuscriptGeometry as r, NovelFlowSettings as s, createNovelComposer as t, PaperSizeId as u };
|