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