@heroiclands/package-build 24.0.1 → 24.0.3
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/CHANGELOG.md +32 -0
- package/assets/preview/gitea.ini +1 -1
- package/assets/styles/content-base.css +33 -0
- package/bin/content-commands.mjs +17 -17
- package/content-config.mjs +1 -52
- package/docs/api.md +20 -13
- package/docs/authoring/links-and-markup.md +165 -17
- package/docs/commands.md +20 -20
- package/docs/configuration.md +29 -69
- package/docs/content-format.md +4 -2
- package/docs/guides/site.md +3 -1
- package/docs/guides/writing-in-invented-scripts.md +68 -6
- package/docs/reference/format-details.md +160 -118
- package/docs/reference/note-types.md +49 -29
- package/docs/source-preview.md +2 -2
- package/engine/content-blocks.mjs +6 -2
- package/engine/content-charset.mjs +1 -0
- package/engine/content-format.yaml +2 -0
- package/engine/content-index.mjs +5 -1
- package/engine/content-render.mjs +2 -1
- package/engine/content-scripts.mjs +268 -21
- package/engine/content-scroll.mjs +389 -0
- package/engine/dictionary-layout.mjs +16 -3
- package/engine/font-outlines.mjs +394 -0
- package/engine/foreign-catalog.mjs +11 -122
- package/engine/language-data.mjs +154 -63
- package/engine/language-rows.mjs +35 -9
- package/engine/language-terms.mjs +39 -9
- package/engine/map-lexicon.mjs +31 -6
- package/engine/note-vocabulary.mjs +58 -6
- package/engine/pdf-build.mjs +9 -178
- package/engine/pdf-fonts.mjs +1 -1
- package/engine/pdf-render.mjs +52 -11
- package/engine/run-rows.mjs +45 -3
- package/engine/script-direction.mjs +67 -0
- package/engine/script-emphasis-terms.mjs +11 -3
- package/engine/script-font-delivery.mjs +42 -9
- package/engine/script-fonts.mjs +7 -0
- package/engine/site-reach.mjs +23 -0
- package/engine/site-render.mjs +18 -3
- package/engine/verse-metre.mjs +1 -0
- package/engine/written-in.mjs +208 -0
- package/package.json +2 -1
- package/site-theme/static/css/style.css +7 -0
- package/types/content-config.d.mts +1 -19
- package/types/engine/content-scripts.d.mts +31 -6
- package/types/engine/content-scroll.d.mts +179 -0
- package/types/engine/dictionary-layout.d.mts +5 -0
- package/types/engine/font-outlines.d.mts +67 -0
- package/types/engine/foreign-catalog.d.mts +0 -38
- package/types/engine/language-data.d.mts +13 -0
- package/types/engine/language-rows.d.mts +5 -4
- package/types/engine/language-terms.d.mts +7 -0
- package/types/engine/map-lexicon.d.mts +4 -3
- package/types/engine/pdf-build.d.mts +5 -27
- package/types/engine/pdf-fonts.d.mts +10 -0
- package/types/engine/run-rows.d.mts +28 -6
- package/types/engine/script-direction.d.mts +10 -0
- package/types/engine/script-emphasis-terms.d.mts +10 -3
- package/types/engine/script-font-delivery.d.mts +5 -3
- package/types/engine/script-fonts.d.mts +9 -0
- package/types/engine/site-reach.d.mts +11 -0
- package/types/engine/written-in.d.mts +76 -0
- package/engine/asset-replacement.mjs +0 -93
- package/types/engine/asset-replacement.d.mts +0 -22
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* **A face's outlines, drawn as SVG.** The book sets a `scroll` fence as panel
|
|
16
|
+
* images because Typst has no vertical writing, and draws each character from
|
|
17
|
+
* the face's own TrueType outlines, so a panel needs no renderer and no font
|
|
18
|
+
* on the machine that compiles the book.
|
|
19
|
+
*
|
|
20
|
+
* - **`cmap`** (formats 4 and 12) gives each code point its glyph.
|
|
21
|
+
* - **`loca` and `glyf`** give each glyph's quadratic contours, a composite
|
|
22
|
+
* glyph's components moved and scaled into place.
|
|
23
|
+
* - **`hmtx`** gives a glyph's width, which centres it in its column.
|
|
24
|
+
* - **`vmtx`**, where the face has it, gives the space above a glyph in a
|
|
25
|
+
* column one em tall; a face without it is centred on its ascender and
|
|
26
|
+
* descender.
|
|
27
|
+
*
|
|
28
|
+
* A panel is one SVG: each glyph drawn once as a path and placed by `<use>`,
|
|
29
|
+
* the columns read from the right, `rows` ems tall and
|
|
30
|
+
* {@link module:engine/content-scroll.SCROLL_PITCH} ems from column to column.
|
|
31
|
+
* A character the face does not draw, and whitespace, leave their cell empty.
|
|
32
|
+
*
|
|
33
|
+
* @module
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import fs from "node:fs";
|
|
37
|
+
|
|
38
|
+
import { SCROLL_PITCH } from "./content-scroll.mjs";
|
|
39
|
+
import { tableDirectory } from "./pdf-fonts.mjs";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* One glyph, in font units, its y axis pointing down from the baseline.
|
|
43
|
+
*
|
|
44
|
+
* @typedef {object} Glyph
|
|
45
|
+
* @property {number} index - Its glyph index.
|
|
46
|
+
* @property {string} d - Its outline as SVG path data; `""` for none.
|
|
47
|
+
* @property {number} advance - Its horizontal advance.
|
|
48
|
+
* @property {number} top - From the top of its cell to its baseline.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A face's outlines.
|
|
53
|
+
*
|
|
54
|
+
* @typedef {object} Outlines
|
|
55
|
+
* @property {number} unitsPerEm - Its units per em.
|
|
56
|
+
* @property {(point: number) => Glyph|undefined} glyph - The glyph a code
|
|
57
|
+
* point is drawn with, or `undefined` when the face does not draw it.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/** @type {Map<string, Outlines>} */
|
|
61
|
+
const cache = new Map();
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Code point → glyph index, from a `cmap` of format 4 or 12.
|
|
65
|
+
*
|
|
66
|
+
* @param {Buffer} buf - The font file.
|
|
67
|
+
* @param {{offset: number}} table - The `cmap` record.
|
|
68
|
+
* @returns {Map<number, number>} Glyph indices by code point.
|
|
69
|
+
*/
|
|
70
|
+
function codepointGlyphs(buf, table) {
|
|
71
|
+
const out = new Map();
|
|
72
|
+
const at = table.offset;
|
|
73
|
+
const numTables = buf.readUInt16BE(at + 2);
|
|
74
|
+
for (let i = 0; i < numTables; i += 1) {
|
|
75
|
+
const sub = at + buf.readUInt32BE(at + 4 + i * 8 + 4);
|
|
76
|
+
const format = buf.readUInt16BE(sub);
|
|
77
|
+
if (format === 4) {
|
|
78
|
+
const segX2 = buf.readUInt16BE(sub + 6);
|
|
79
|
+
const endAt = sub + 14;
|
|
80
|
+
const startAt = endAt + segX2 + 2;
|
|
81
|
+
const deltaAt = startAt + segX2;
|
|
82
|
+
const rangeAt = deltaAt + segX2;
|
|
83
|
+
for (let s = 0; s < segX2 / 2; s += 1) {
|
|
84
|
+
const end = buf.readUInt16BE(endAt + s * 2);
|
|
85
|
+
const start = buf.readUInt16BE(startAt + s * 2);
|
|
86
|
+
const delta = buf.readInt16BE(deltaAt + s * 2);
|
|
87
|
+
const rangeOffset = buf.readUInt16BE(rangeAt + s * 2);
|
|
88
|
+
if (start === 0xffff) continue;
|
|
89
|
+
for (let cp = start; cp <= end; cp += 1) {
|
|
90
|
+
let glyph;
|
|
91
|
+
if (rangeOffset === 0) glyph = (cp + delta) & 0xffff;
|
|
92
|
+
else {
|
|
93
|
+
glyph = buf.readUInt16BE(rangeAt + s * 2 + rangeOffset + (cp - start) * 2);
|
|
94
|
+
if (glyph) glyph = (glyph + delta) & 0xffff;
|
|
95
|
+
}
|
|
96
|
+
if (glyph && !out.has(cp)) out.set(cp, glyph);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} else if (format === 12) {
|
|
100
|
+
const groups = buf.readUInt32BE(sub + 12);
|
|
101
|
+
for (let g = 0; g < groups; g += 1) {
|
|
102
|
+
const row = sub + 16 + g * 12;
|
|
103
|
+
const start = buf.readUInt32BE(row);
|
|
104
|
+
const end = Math.min(buf.readUInt32BE(row + 4), start + 0x1_0000);
|
|
105
|
+
const first = buf.readUInt32BE(row + 8);
|
|
106
|
+
for (let cp = start; cp <= end; cp += 1)
|
|
107
|
+
if (!out.has(cp)) out.set(cp, first + (cp - start));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A point of a contour.
|
|
116
|
+
*
|
|
117
|
+
* @typedef {{x: number, y: number, on: boolean}} Point
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A glyph's contours, its components resolved.
|
|
122
|
+
*
|
|
123
|
+
* @param {Buffer} buf - The font file.
|
|
124
|
+
* @param {number} glyf - The `glyf` table's offset.
|
|
125
|
+
* @param {(index: number) => [number, number]} span - A glyph's offset in
|
|
126
|
+
* `glyf` and the offset past it.
|
|
127
|
+
* @param {number} index - The glyph.
|
|
128
|
+
* @param {number} depth - How deep in a composite this glyph sits.
|
|
129
|
+
* @returns {Point[][]} Its contours.
|
|
130
|
+
*/
|
|
131
|
+
function contours(buf, glyf, span, index, depth = 0) {
|
|
132
|
+
const [from, to] = span(index);
|
|
133
|
+
if (to <= from || depth > 8) return [];
|
|
134
|
+
const at = glyf + from;
|
|
135
|
+
const count = buf.readInt16BE(at);
|
|
136
|
+
if (count >= 0) {
|
|
137
|
+
const ends = [];
|
|
138
|
+
for (let c = 0; c < count; c += 1) ends.push(buf.readUInt16BE(at + 10 + c * 2));
|
|
139
|
+
const points = count ? ends[count - 1] + 1 : 0;
|
|
140
|
+
let p = at + 10 + count * 2;
|
|
141
|
+
p += 2 + buf.readUInt16BE(p);
|
|
142
|
+
const flags = [];
|
|
143
|
+
while (flags.length < points) {
|
|
144
|
+
const flag = buf.readUInt8(p++);
|
|
145
|
+
flags.push(flag);
|
|
146
|
+
if (flag & 8) {
|
|
147
|
+
const repeat = buf.readUInt8(p++);
|
|
148
|
+
for (let r = 0; r < repeat; r += 1) flags.push(flag);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const read = (short, same) => {
|
|
152
|
+
const values = [];
|
|
153
|
+
let value = 0;
|
|
154
|
+
for (const flag of flags) {
|
|
155
|
+
if (flag & short) {
|
|
156
|
+
const delta = buf.readUInt8(p++);
|
|
157
|
+
value += flag & same ? delta : -delta;
|
|
158
|
+
} else if (!(flag & same)) {
|
|
159
|
+
value += buf.readInt16BE(p);
|
|
160
|
+
p += 2;
|
|
161
|
+
}
|
|
162
|
+
values.push(value);
|
|
163
|
+
}
|
|
164
|
+
return values;
|
|
165
|
+
};
|
|
166
|
+
const xs = read(2, 16);
|
|
167
|
+
const ys = read(4, 32);
|
|
168
|
+
const out = [];
|
|
169
|
+
let start = 0;
|
|
170
|
+
for (const end of ends) {
|
|
171
|
+
const contour = [];
|
|
172
|
+
for (let k = start; k <= end; k += 1)
|
|
173
|
+
contour.push({ x: xs[k], y: ys[k], on: Boolean(flags[k] & 1) });
|
|
174
|
+
out.push(contour);
|
|
175
|
+
start = end + 1;
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
const out = [];
|
|
180
|
+
let p = at + 10;
|
|
181
|
+
for (;;) {
|
|
182
|
+
const flags = buf.readUInt16BE(p);
|
|
183
|
+
const component = buf.readUInt16BE(p + 2);
|
|
184
|
+
p += 4;
|
|
185
|
+
let dx;
|
|
186
|
+
let dy;
|
|
187
|
+
if (flags & 1) {
|
|
188
|
+
dx = buf.readInt16BE(p);
|
|
189
|
+
dy = buf.readInt16BE(p + 2);
|
|
190
|
+
p += 4;
|
|
191
|
+
} else {
|
|
192
|
+
dx = buf.readInt8(p);
|
|
193
|
+
dy = buf.readInt8(p + 1);
|
|
194
|
+
p += 2;
|
|
195
|
+
}
|
|
196
|
+
if (!(flags & 2)) {
|
|
197
|
+
dx = 0;
|
|
198
|
+
dy = 0;
|
|
199
|
+
}
|
|
200
|
+
let [a, b, c, d] = [1, 0, 0, 1];
|
|
201
|
+
const f2dot14 = (offset) => buf.readInt16BE(offset) / 16384;
|
|
202
|
+
if (flags & 8) {
|
|
203
|
+
a = d = f2dot14(p);
|
|
204
|
+
p += 2;
|
|
205
|
+
} else if (flags & 0x40) {
|
|
206
|
+
a = f2dot14(p);
|
|
207
|
+
d = f2dot14(p + 2);
|
|
208
|
+
p += 4;
|
|
209
|
+
} else if (flags & 0x80) {
|
|
210
|
+
a = f2dot14(p);
|
|
211
|
+
b = f2dot14(p + 2);
|
|
212
|
+
c = f2dot14(p + 4);
|
|
213
|
+
d = f2dot14(p + 6);
|
|
214
|
+
p += 8;
|
|
215
|
+
}
|
|
216
|
+
for (const contour of contours(buf, glyf, span, component, depth + 1))
|
|
217
|
+
out.push(
|
|
218
|
+
contour.map(({ x, y, on }) => ({
|
|
219
|
+
x: a * x + c * y + dx,
|
|
220
|
+
y: b * x + d * y + dy,
|
|
221
|
+
on,
|
|
222
|
+
})),
|
|
223
|
+
);
|
|
224
|
+
if (!(flags & 0x20)) break;
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** A number as path data writes it. */
|
|
230
|
+
const num = (value) => String(Math.round(value * 100) / 100);
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* A contour of quadratic segments as SVG path data, its y axis flipped so it
|
|
234
|
+
* points down from the baseline.
|
|
235
|
+
*
|
|
236
|
+
* @param {Point[]} contour - The contour.
|
|
237
|
+
* @returns {string} The path data.
|
|
238
|
+
*/
|
|
239
|
+
function contourPath(contour) {
|
|
240
|
+
if (!contour.length) return "";
|
|
241
|
+
const points = contour.map(({ x, y, on }) => ({ x, y: -y, on }));
|
|
242
|
+
const mid = (p, q) => ({ x: (p.x + q.x) / 2, y: (p.y + q.y) / 2, on: true });
|
|
243
|
+
let first = points.findIndex((point) => point.on);
|
|
244
|
+
let start;
|
|
245
|
+
let ordered;
|
|
246
|
+
if (first < 0) {
|
|
247
|
+
start = mid(points[points.length - 1], points[0]);
|
|
248
|
+
ordered = points;
|
|
249
|
+
} else {
|
|
250
|
+
start = points[first];
|
|
251
|
+
ordered = [...points.slice(first + 1), ...points.slice(0, first)];
|
|
252
|
+
}
|
|
253
|
+
let d = `M${num(start.x)} ${num(start.y)}`;
|
|
254
|
+
let control;
|
|
255
|
+
for (const point of [...ordered, start]) {
|
|
256
|
+
if (point.on) {
|
|
257
|
+
d +=
|
|
258
|
+
control ?
|
|
259
|
+
`Q${num(control.x)} ${num(control.y)} ${num(point.x)} ${num(point.y)}`
|
|
260
|
+
: `L${num(point.x)} ${num(point.y)}`;
|
|
261
|
+
control = undefined;
|
|
262
|
+
} else if (control) {
|
|
263
|
+
const between = mid(control, point);
|
|
264
|
+
d += `Q${num(control.x)} ${num(control.y)} ${num(between.x)} ${num(between.y)}`;
|
|
265
|
+
control = point;
|
|
266
|
+
} else control = point;
|
|
267
|
+
}
|
|
268
|
+
return `${d}Z`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The outlines of a TrueType face, read once per file.
|
|
273
|
+
*
|
|
274
|
+
* @param {string} file - The `.ttf`.
|
|
275
|
+
* @returns {Outlines} Its outlines.
|
|
276
|
+
* @throws {Error} When the file is not a TrueType face with outlines.
|
|
277
|
+
*/
|
|
278
|
+
export function fontOutlines(file) {
|
|
279
|
+
const known = cache.get(file);
|
|
280
|
+
if (known) return known;
|
|
281
|
+
const buf = fs.readFileSync(file);
|
|
282
|
+
const tables = tableDirectory(buf);
|
|
283
|
+
for (const tag of ["head", "maxp", "hhea", "hmtx", "loca", "glyf", "cmap"])
|
|
284
|
+
if (!tables.has(tag))
|
|
285
|
+
throw new Error(
|
|
286
|
+
`${file}: error: the face has no \`${tag}\` table to draw a scroll from`,
|
|
287
|
+
);
|
|
288
|
+
const head = /** @type {{offset: number}} */ (tables.get("head")).offset;
|
|
289
|
+
const unitsPerEm = buf.readUInt16BE(head + 18);
|
|
290
|
+
const longOffsets = buf.readInt16BE(head + 50) === 1;
|
|
291
|
+
const numGlyphs = buf.readUInt16BE(
|
|
292
|
+
/** @type {{offset: number}} */ (tables.get("maxp")).offset + 4,
|
|
293
|
+
);
|
|
294
|
+
const hhea = /** @type {{offset: number}} */ (tables.get("hhea")).offset;
|
|
295
|
+
const ascender = buf.readInt16BE(hhea + 4);
|
|
296
|
+
const descender = buf.readInt16BE(hhea + 6);
|
|
297
|
+
const hMetrics = buf.readUInt16BE(hhea + 34);
|
|
298
|
+
const hmtx = /** @type {{offset: number}} */ (tables.get("hmtx")).offset;
|
|
299
|
+
const loca = /** @type {{offset: number}} */ (tables.get("loca")).offset;
|
|
300
|
+
const glyf = /** @type {{offset: number}} */ (tables.get("glyf")).offset;
|
|
301
|
+
const vhea = tables.get("vhea");
|
|
302
|
+
const vmtx = tables.get("vmtx");
|
|
303
|
+
const vMetrics = vhea && vmtx ? buf.readUInt16BE(vhea.offset + 34) : 0;
|
|
304
|
+
const span = (index) =>
|
|
305
|
+
index >= numGlyphs ? [0, 0]
|
|
306
|
+
: longOffsets ? [buf.readUInt32BE(loca + index * 4), buf.readUInt32BE(loca + index * 4 + 4)]
|
|
307
|
+
: [buf.readUInt16BE(loca + index * 2) * 2, buf.readUInt16BE(loca + index * 2 + 2) * 2];
|
|
308
|
+
const glyphs = codepointGlyphs(buf, /** @type {{offset: number}} */ (tables.get("cmap")));
|
|
309
|
+
/** @type {Map<number, Glyph>} */
|
|
310
|
+
const drawn = new Map();
|
|
311
|
+
const glyphOf = (index) => {
|
|
312
|
+
const had = drawn.get(index);
|
|
313
|
+
if (had) return had;
|
|
314
|
+
const advance = buf.readUInt16BE(hmtx + Math.min(index, hMetrics - 1) * 4);
|
|
315
|
+
const [from, to] = span(index);
|
|
316
|
+
const yMax = to > from ? buf.readInt16BE(glyf + from + 8) : 0;
|
|
317
|
+
let top;
|
|
318
|
+
if (vmtx && vMetrics) {
|
|
319
|
+
const tsb =
|
|
320
|
+
index < vMetrics ?
|
|
321
|
+
buf.readInt16BE(vmtx.offset + index * 4 + 2)
|
|
322
|
+
: buf.readInt16BE(vmtx.offset + vMetrics * 4 + (index - vMetrics) * 2);
|
|
323
|
+
top = yMax + tsb;
|
|
324
|
+
} else top = ascender + (unitsPerEm - (ascender - descender)) / 2;
|
|
325
|
+
const glyph = {
|
|
326
|
+
index,
|
|
327
|
+
d: contours(buf, glyf, span, index).map(contourPath).join(""),
|
|
328
|
+
advance,
|
|
329
|
+
top,
|
|
330
|
+
};
|
|
331
|
+
drawn.set(index, glyph);
|
|
332
|
+
return glyph;
|
|
333
|
+
};
|
|
334
|
+
const outlines = {
|
|
335
|
+
unitsPerEm,
|
|
336
|
+
glyph: (point) => {
|
|
337
|
+
const index = glyphs.get(point);
|
|
338
|
+
return index === undefined ? undefined : glyphOf(index);
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
cache.set(file, outlines);
|
|
342
|
+
return outlines;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** The book's rubric red, which its preamble names `book-rubric`. */
|
|
346
|
+
export const BOOK_RUBRIC = "#9c281b";
|
|
347
|
+
|
|
348
|
+
/** The book's ink, which its preamble names `book-ink`. */
|
|
349
|
+
export const BOOK_INK = "#241f1a";
|
|
350
|
+
|
|
351
|
+
/** The colour of plain text and of text set in the rubric. */
|
|
352
|
+
const INK = Object.freeze({ plain: BOOK_INK, rubric: BOOK_RUBRIC });
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* One panel of a scroll as SVG: its columns read from the right, `rows` ems
|
|
356
|
+
* tall, each character centred in its column, in red where it is set in the
|
|
357
|
+
* rubric and with a line beside it, on the right, where it is overlined.
|
|
358
|
+
*
|
|
359
|
+
* @param {import("./content-scroll.mjs").ScrollCell[][]} panel - Its columns,
|
|
360
|
+
* the first the rightmost.
|
|
361
|
+
* @param {number} rows - The characters in a column.
|
|
362
|
+
* @param {Outlines} outlines - The face.
|
|
363
|
+
* @returns {string} The SVG.
|
|
364
|
+
*/
|
|
365
|
+
export function scrollPanelSvg(panel, rows, outlines) {
|
|
366
|
+
const em = outlines.unitsPerEm;
|
|
367
|
+
const pitch = SCROLL_PITCH * em;
|
|
368
|
+
const width = panel.length * pitch;
|
|
369
|
+
const height = rows * em;
|
|
370
|
+
const defs = new Map();
|
|
371
|
+
const marks = [];
|
|
372
|
+
panel.forEach((column, at) => {
|
|
373
|
+
const centre = width - (at + 0.5) * pitch;
|
|
374
|
+
column.forEach((cell, row) => {
|
|
375
|
+
const cellTop = row * em;
|
|
376
|
+
if (cell.emphasis === "overline")
|
|
377
|
+
marks.push(
|
|
378
|
+
`<rect x="${num(centre + 0.55 * em)}" y="${num(cellTop)}" width="${num(0.05 * em)}" height="${num(em)}" fill="${INK.plain}"/>`,
|
|
379
|
+
);
|
|
380
|
+
const point = cell.text.codePointAt(0);
|
|
381
|
+
if (point === undefined || /\s/u.test(cell.text)) return;
|
|
382
|
+
const glyph = outlines.glyph(point);
|
|
383
|
+
if (!glyph?.d) return;
|
|
384
|
+
defs.set(glyph.index, `<path id="g${glyph.index}" d="${glyph.d}"/>`);
|
|
385
|
+
marks.push(
|
|
386
|
+
`<use href="#g${glyph.index}" x="${num(centre - glyph.advance / 2)}" y="${num(cellTop + glyph.top)}" fill="${cell.emphasis === "rubric" ? INK.rubric : INK.plain}"/>`,
|
|
387
|
+
);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
return (
|
|
391
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${num(width)} ${num(height)}" width="${num(width)}" height="${num(height)}">` +
|
|
392
|
+
`<defs>${[...defs.values()].join("")}</defs>${marks.join("")}</svg>`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
@@ -38,12 +38,6 @@
|
|
|
38
38
|
* local pack's output — the resolution logic needs no knowledge of where an
|
|
39
39
|
* item came from.
|
|
40
40
|
*
|
|
41
|
-
* `assetArchive: true` is the sibling flag, for a dependency whose bytes are
|
|
42
|
-
* wanted and whose items are not: an asset-replacement source such as
|
|
43
|
-
* `thalornaaltart` ships no Item packs at all. It unpacks the same release
|
|
44
|
-
* archive without extracting anything from it, so a relationship declaring
|
|
45
|
-
* both flags on one entry is fetched once rather than twice.
|
|
46
|
-
*
|
|
47
41
|
* **The network is never touched by a compile.** Fetching is its own command
|
|
48
42
|
* (`package-build deps fetch`), and a compile whose cache is cold fails saying
|
|
49
43
|
* so. A build that silently downloads is not reproducible, fails strangely
|
|
@@ -78,22 +72,17 @@ import {
|
|
|
78
72
|
const STAMP = ".complete";
|
|
79
73
|
|
|
80
74
|
/**
|
|
81
|
-
* Every declared relationship
|
|
82
|
-
*
|
|
83
|
-
* Shared by {@link itemCatalogRelationships} and
|
|
84
|
-
* {@link assetArchiveRelationships}: both read the same shape out of the same
|
|
85
|
-
* walk, filtered on a different key.
|
|
75
|
+
* Every declared relationship that opted into supplying an item catalogue.
|
|
86
76
|
*
|
|
87
77
|
* @param {object} config - The resolved build configuration.
|
|
88
|
-
* @param {"itemCatalog"|"assetArchive"} flag - Which boolean to filter on.
|
|
89
78
|
* @returns {Array<{id: string, manifest: string, kind: string, verified: string|undefined}>}
|
|
90
|
-
* The opted-in relationships.
|
|
79
|
+
* The opted-in relationships, in declaration order.
|
|
91
80
|
*/
|
|
92
|
-
function
|
|
81
|
+
export function itemCatalogRelationships(config) {
|
|
93
82
|
const out = [];
|
|
94
83
|
for (const [kind, entries] of Object.entries(config.relationships ?? {})) {
|
|
95
84
|
for (const rel of entries ?? []) {
|
|
96
|
-
if (rel
|
|
85
|
+
if (rel.itemCatalog) {
|
|
97
86
|
out.push({
|
|
98
87
|
id: rel.id,
|
|
99
88
|
manifest: rel.manifest,
|
|
@@ -106,29 +95,6 @@ function flaggedRelationships(config, flag) {
|
|
|
106
95
|
return out;
|
|
107
96
|
}
|
|
108
97
|
|
|
109
|
-
/**
|
|
110
|
-
* Every declared relationship that opted into supplying an item catalogue.
|
|
111
|
-
*
|
|
112
|
-
* @param {object} config - The resolved build configuration.
|
|
113
|
-
* @returns {Array<{id: string, manifest: string, kind: string, verified: string|undefined}>}
|
|
114
|
-
* The opted-in relationships, in declaration order.
|
|
115
|
-
*/
|
|
116
|
-
export function itemCatalogRelationships(config) {
|
|
117
|
-
return flaggedRelationships(config, "itemCatalog");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Every declared relationship that opted into supplying its release archive
|
|
122
|
-
* for asset bytes alone, with no item catalogue built from it.
|
|
123
|
-
*
|
|
124
|
-
* @param {object} config - The resolved build configuration.
|
|
125
|
-
* @returns {Array<{id: string, manifest: string, kind: string, verified: string|undefined}>}
|
|
126
|
-
* The opted-in relationships, in declaration order.
|
|
127
|
-
*/
|
|
128
|
-
export function assetArchiveRelationships(config) {
|
|
129
|
-
return flaggedRelationships(config, "assetArchive");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
98
|
/**
|
|
133
99
|
* The cache directory for one dependency at one version.
|
|
134
100
|
*
|
|
@@ -401,21 +367,14 @@ export function pinnedManifestUrl(url, verified) {
|
|
|
401
367
|
}
|
|
402
368
|
|
|
403
369
|
/**
|
|
404
|
-
* Resolve a dependency's manifest, pinned to `compatibility.verified
|
|
405
|
-
* both fetch paths require.
|
|
406
|
-
*
|
|
407
|
-
* Shared by {@link fetchCatalog} and {@link fetchAssetArchive}: both read the
|
|
408
|
-
* same manifest before deciding what to unpack from it.
|
|
370
|
+
* Resolve a dependency's manifest, pinned to `compatibility.verified`.
|
|
409
371
|
*
|
|
410
372
|
* @param {{id: string, manifest: string, verified?: string}} rel - The
|
|
411
373
|
* declared relationship.
|
|
412
|
-
* @param {string} subject - What floats when no version is verified —
|
|
413
|
-
* "catalogue" or "archive" — named in the warning a reader would otherwise
|
|
414
|
-
* get no word for.
|
|
415
374
|
* @returns {Promise<{manifest: object, version: string}>} The manifest and
|
|
416
375
|
* its resolved version.
|
|
417
376
|
*/
|
|
418
|
-
async function resolveManifest(rel
|
|
377
|
+
async function resolveManifest(rel) {
|
|
419
378
|
const { url, pinned } = pinnedManifestUrl(rel.manifest, rel.verified);
|
|
420
379
|
const manifest = await fetchManifest(url);
|
|
421
380
|
const version = manifest.version;
|
|
@@ -432,7 +391,7 @@ async function resolveManifest(rel, subject) {
|
|
|
432
391
|
}
|
|
433
392
|
if (!rel.verified) {
|
|
434
393
|
log.warn(
|
|
435
|
-
`${rel.id}: no \`compatibility.verified\`, so its
|
|
394
|
+
`${rel.id}: no \`compatibility.verified\`, so its catalogue floats ` +
|
|
436
395
|
`with whatever ${url} currently serves`,
|
|
437
396
|
);
|
|
438
397
|
}
|
|
@@ -449,7 +408,7 @@ async function resolveManifest(rel, subject) {
|
|
|
449
408
|
* @returns {Promise<string>} The dependency's cache directory.
|
|
450
409
|
*/
|
|
451
410
|
export async function fetchCatalog(config, rel) {
|
|
452
|
-
const { manifest, version } = await resolveManifest(rel
|
|
411
|
+
const { manifest, version } = await resolveManifest(rel);
|
|
453
412
|
const dir = catalogDir(config, rel.id, version);
|
|
454
413
|
if (isComplete(dir)) {
|
|
455
414
|
log.info(`${rel.id}@${version}: already cached`);
|
|
@@ -475,61 +434,6 @@ export async function fetchCatalog(config, rel) {
|
|
|
475
434
|
return dir;
|
|
476
435
|
}
|
|
477
436
|
|
|
478
|
-
/**
|
|
479
|
-
* Whether an asset-archive-only cache is present and complete.
|
|
480
|
-
*
|
|
481
|
-
* No item catalogue is built on this path, so completeness never asks for
|
|
482
|
-
* `item-packs.json` — an asset-only dependency, such as `thalornaaltart`,
|
|
483
|
-
* ships none.
|
|
484
|
-
*
|
|
485
|
-
* @param {string} dir - The dependency's cache directory.
|
|
486
|
-
* @returns {boolean} True when the archive was fetched to completion.
|
|
487
|
-
*/
|
|
488
|
-
const isArchiveComplete = (dir) => fs.existsSync(path.join(dir, STAMP));
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Fetch one dependency's release archive for its asset bytes alone, building
|
|
492
|
-
* no item catalogue from it.
|
|
493
|
-
*
|
|
494
|
-
* Idempotent: a complete cache for the resolved version is left alone. Where a
|
|
495
|
-
* relationship also declares `itemCatalog: true`, {@link fetchCatalog} already
|
|
496
|
-
* unpacked this same archive into this same cache directory, and
|
|
497
|
-
* {@link fetchAllCatalogs} does not call this function for it — the two never
|
|
498
|
-
* download the same bytes twice.
|
|
499
|
-
*
|
|
500
|
-
* @param {object} config - The resolved build configuration.
|
|
501
|
-
* @param {{id: string, manifest: string, verified?: string}} rel - The
|
|
502
|
-
* declared relationship.
|
|
503
|
-
* @returns {Promise<string>} The dependency's cache directory.
|
|
504
|
-
*/
|
|
505
|
-
export async function fetchAssetArchive(config, rel) {
|
|
506
|
-
const { manifest, version } = await resolveManifest(rel, "archive");
|
|
507
|
-
const dir = catalogDir(config, rel.id, version);
|
|
508
|
-
if (isArchiveComplete(dir)) {
|
|
509
|
-
log.info(`${rel.id}@${version}: already cached`);
|
|
510
|
-
return dir;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
const download = manifest.download;
|
|
514
|
-
if (!download) {
|
|
515
|
-
throw new Error(`${rel.id}@${version}: its manifest declares no \`download\``);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Rebuild from empty: a previous run may have died partway, and a stale
|
|
519
|
-
// half-tree is worse than no tree.
|
|
520
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
521
|
-
const raw = path.join(dir, "package");
|
|
522
|
-
fs.mkdirSync(raw, { recursive: true });
|
|
523
|
-
|
|
524
|
-
log.info(`${rel.id}@${version}: downloading ${download}`);
|
|
525
|
-
await downloadAndUnzip(download, raw);
|
|
526
|
-
|
|
527
|
-
cacheSchemaArtifact(raw, dir);
|
|
528
|
-
// Last, so a fetch that died partway is never mistaken for a complete one.
|
|
529
|
-
fs.writeFileSync(path.join(dir, STAMP), `${version}\n`);
|
|
530
|
-
return dir;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
437
|
/**
|
|
534
438
|
* Locate a declared pack inside an unpacked archive.
|
|
535
439
|
*
|
|
@@ -811,33 +715,18 @@ export async function fetchAllMetadata(config) {
|
|
|
811
715
|
/**
|
|
812
716
|
* Fetch every opted-in dependency. The `deps fetch` command.
|
|
813
717
|
*
|
|
814
|
-
* A relationship declaring both `itemCatalog: true` and `assetArchive: true`
|
|
815
|
-
* is fetched once: {@link fetchCatalog} already unpacks the archive on its
|
|
816
|
-
* way to extracting the item catalogue, so this skips the archive-only fetch
|
|
817
|
-
* for any id {@link fetchCatalog} already handled.
|
|
818
718
|
*
|
|
819
719
|
* @param {object} config - The resolved build configuration.
|
|
820
720
|
* @returns {Promise<number>} How many dependencies were fetched.
|
|
821
721
|
*/
|
|
822
722
|
export async function fetchAllCatalogs(config) {
|
|
823
723
|
const itemRels = itemCatalogRelationships(config);
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
log.info(
|
|
827
|
-
"No relationship declares `itemCatalog: true` or `assetArchive: true`; " +
|
|
828
|
-
"nothing to fetch.",
|
|
829
|
-
);
|
|
724
|
+
if (!itemRels.length) {
|
|
725
|
+
log.info("No relationship declares `itemCatalog: true`; nothing to fetch.");
|
|
830
726
|
return 0;
|
|
831
727
|
}
|
|
832
728
|
for (const rel of itemRels) await fetchCatalog(config, rel);
|
|
833
|
-
|
|
834
|
-
let archiveFetched = 0;
|
|
835
|
-
for (const rel of archiveRels) {
|
|
836
|
-
if (alreadyFetched.has(rel.id)) continue;
|
|
837
|
-
await fetchAssetArchive(config, rel);
|
|
838
|
-
archiveFetched++;
|
|
839
|
-
}
|
|
840
|
-
return itemRels.length + archiveFetched;
|
|
729
|
+
return itemRels.length;
|
|
841
730
|
}
|
|
842
731
|
|
|
843
732
|
/**
|