authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-03-27 01:18:43-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-04-02 22:48:31-07:00
log907e9083b681531d3be2f8ba0cafc63dede6e61d
treef06c4b574f11f33e55ec6bd15afc672165a82d17
parente5053a06d43a46f83b18f1cc2acc436024a8bcb6
signature Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs

feat: embedded opentype fonts


11 files changed, 593 insertions(+), 260 deletions(-)

framework/css.ts+1-1
......@@ -81,7 +81,7 @@ export async function bundleCssFiles(
8181 const build = await esbuild.build({
8282 bundle: true,
8383 entryPoints: ["$input$"],
84 external: ["*.woff2", "*.ttf", "*.png", "*.jpeg"],
84 external: ["*.woff2", "*.eot", "*.eot?#iefix", "*.ttf", "*.png", "*.jpeg"],
8585 metafile: true,
8686 minify: !dev,
8787 plugins: [
framework/font.ts+131-172
......@@ -10,15 +10,6 @@ export async function buildFonts(p: progress.Ref, fonts: sg.Font[]) {
1010 });
1111 }
1212
13 function getHash(input: string, args: string[]) {
14 return crypto
15 .createHash("sha256")
16 .update(`${input}${args.join(" ")}`)
17 .digest("hex")
18 .slice(0, 16)
19 .toLowerCase();
20 }
21
2213 function getVariationAxes(vars: sg.FontVars) {
2314 return Object.fromEntries(
2415 Object.entries(vars).map(([key, value]) => [
......@@ -31,36 +22,56 @@ export async function buildFonts(p: progress.Ref, fonts: sg.Font[]) {
3122 await Promise.all(fonts.map((font) =>
3223 incr.work(node, async (io, font) => {
3324 using node = io.start(font.name);
34 const baseFile = await fetchFont(font.name, font.sources);
35 const baseBuffer = await fs.readFile(baseFile);
25 const baseFile = await fetchFont(node, font.name, font.sources);
26 await using baseBuffer = async.deferred(fs.readFile(baseFile));
3627 await Promise.all(font.subsets.map(async (subset) => {
3728 const args = getArgs(subset.vars ?? {});
38 const subsetText = fontRangesToText(
39 Array.isArray(subset.unicodes) ? subset.unicodes : [subset.unicodes],
40 );
41 using _ = node.start(args.join(" "));
42 const woff = await subsetFontBuffer(baseBuffer, subsetText, {
43 targetFormat: "woff2",
44 variationAxes: subset.vars
45 ? getVariationAxes(subset.vars)
46 : undefined,
47 sourceName: baseFile,
48 assetName: subset.asset.replace("[ext]", "woff2"),
49 });
29 const unicodes = Array.isArray(subset.unicodes) ? subset.unicodes : [subset.unicodes];
30 const charCount = unicodes.reduce<number>((count, range) => {
31 if (typeof range === "string") return count + [...range].length;
32 if (typeof range === "number") return count + 1;
33 return count + (range.end - range.start + 1);
34 }, 0);
35 const variationAxes = subset.vars ? getVariationAxes(subset.vars) : undefined;
36 using _ = node.start("subset: " + [...args, `chars=${charCount}`].join(" "));
37 const [woff, ttf] = await Promise.all([
38 subsetFontBuffer(await baseBuffer, unicodes, {
39 targetFormat: "woff2",
40 variationAxes,
41 sourceName: baseFile,
42 assetName: subset.asset.replace("[ext]", "woff2"),
43 }),
44 subsetFontBuffer(await baseBuffer, unicodes, {
45 targetFormat: "sfnt",
46 variationAxes,
47 sourceName: baseFile,
48 assetName: subset.asset.replace("[ext]", "ttf"),
49 }),
50 ]);
51 const eot = ttf2eot(ttf);
5052 await io.writeAsset({
5153 pathname: subset.asset.replace("[ext]", "woff2"),
5254 buffer: woff,
5355 });
56 await io.writeAsset({
57 pathname: subset.asset.replace("[ext]", "ttf"),
58 buffer: ttf,
59 });
60 await io.writeAsset({
61 pathname: subset.asset.replace("[ext]", "eot"),
62 buffer: eot,
63 });
5464 }));
5565 }, font)
5666 ));
5767}
5868
59export async function fetchFont(name: string, sources: string[]) {
69export async function fetchFont(ref: progress.Ref, name: string, sources: string[]) {
70 using _ = ref.start("fetch");
6071 const errs = [];
6172
6273 for (const source of sources) {
63 const cacheName = path.join("", name + path.extname(source));
74 const cacheName = name + path.extname(source);
6475 if (fs.existsSync(cacheName)) return cacheName;
6576
6677 if (source.startsWith("https://")) {
......@@ -106,11 +117,9 @@ export async function fetchFont(name: string, sources: string[]) {
106117 path.posix.join(home, ".local/share/fonts"),
107118 ];
108119 for (const base of bases) {
109 const found = fs.readDirRecOptionalSync(base)
110 .find((file) => path.basename(file) === source);
111 if (found) {
112 return path.join(base, found);
113 }
120 const files = fs.readDirRecOptionalSync(base);
121 const found = files.find((file) => path.basename(file) === source);
122 if (found) return path.join(base, found);
114123 }
115124 errs.push(
116125 new Error(
......@@ -123,46 +132,20 @@ export async function fetchFont(name: string, sources: string[]) {
123132 throw new AggregateError(errs, `Failed to fetch the ${name} font`);
124133}
125134
126function fontRangesToText(ranges: sg.FontRange[]): string {
127 const chars = new Set<number>();
128 ranges.forEach((range) => {
129 if (typeof range === "string") {
130 for (const char of range) {
131 chars.add(char.codePointAt(0) || 0);
132 }
133 } else if (typeof range === "number") {
134 chars.add(range);
135 } else {
136 for (let cp = range.start; cp <= range.end; cp++) chars.add(cp);
137 }
138 });
139 return Array.from(chars)
140 .sort((a, b) => a - b)
141 .map((cp) => String.fromCodePoint(cp))
142 .join("");
143}
144
145135function normalizeAxisTag(tag: string) {
146136 const lower = tag.toLowerCase();
147137 return lower === "wght" || lower === "slnt" ? lower : tag;
148138}
149139
150function detectFontFormat(buffer: Uint8Array): "sfnt" | "woff2" {
140async function convertToSfnt(buffer: Uint8Array) {
151141 const signature = String.fromCharCode(...buffer.subarray(0, 4));
152 if (signature === "wOF2") return "woff2";
153142 if (
154143 signature === "true"
155144 || signature === "OTTO"
156145 || signature === "\x00\x01\x00\x00"
157146 ) {
158 return "sfnt";
147 return buffer;
159148 }
160 throw new Error(`Unsupported font format signature ${JSON.stringify(signature)}`);
161}
162
163async function convertToSfnt(buffer: Uint8Array) {
164 const format = detectFontFormat(buffer);
165 if (format === "sfnt") return buffer;
166149 return new Uint8Array(await wawoff2.decompress(buffer));
167150}
168151
......@@ -174,18 +157,29 @@ async function convertFromSfnt(
174157 return new Uint8Array(await wawoff2.compress(buffer));
175158}
176159
177function getSubsetContext(opt: {
178 sourceName?: string;
179 assetName?: string;
180 variationAxes?: VariationAxes;
181 targetFormat: "sfnt" | "woff2";
182 text: string;
183}) {
184 const fields = [
160const subsetQueue = new queue.PriorityQueue(1);
161const subsetFontBuffer = subsetQueue.wrap(subsetFontBufferUnqueued);
162
163async function subsetFontBufferUnqueued(
164 originalFont: Uint8Array,
165 unicodes: sg.FontRange[],
166 opt: {
167 targetFormat: "sfnt" | "woff2";
168 variationAxes?: VariationAxes;
169 sourceName?: string;
170 assetName?: string;
171 },
172) {
173 const charCount = unicodes.reduce<number>((count, range) => {
174 if (typeof range === "string") return count + [...range].length;
175 if (typeof range === "number") return count + 1;
176 return count + (range.end - range.start + 1);
177 }, 0);
178 const ctx = [
179 `${charCount} chars`,
185180 opt.sourceName ? `source=${opt.sourceName}` : "",
186181 opt.assetName ? `asset=${opt.assetName}` : "",
187182 `target=${opt.targetFormat}`,
188 `textLength=${opt.text.length}`,
189183 opt.variationAxes
190184 ? `axes=${
191185 Object.entries(opt.variationAxes).map(([key, value]) =>
......@@ -195,120 +189,90 @@ function getSubsetContext(opt: {
195189 ).join(",")
196190 }`
197191 : "",
198 ].filter(Boolean);
199 return fields.join(" ");
200}
192 ].filter(Boolean).join(" ");
201193
202async function subsetFontBuffer(
203 originalFont: Uint8Array,
204 text: string,
205 opt: {
206 targetFormat: "sfnt" | "woff2";
207 variationAxes?: VariationAxes;
208 sourceName?: string;
209 assetName?: string;
210 },
211) {
212 return await subsetFontQueue(async () => {
213 const ctx = getSubsetContext({ ...opt, text });
214 if (typeof text !== "string") {
215 throw new Error(`Subset text must be a string. ${ctx}`);
216 }
194 const { harfbuzzJsWasm, getHeapU8 } = await loadAndInitializeHarfbuzz();
195 const sfntFont = await convertToSfnt(originalFont);
217196
218 const { harfbuzzJsWasm, getHeapU8 } = await loadAndInitializeHarfbuzz();
219 const sfntFont = await convertToSfnt(originalFont);
197 const input = harfbuzzJsWasm.hb_subset_input_create_or_fail();
198 if (input === 0) throw new Error(`Failed to create HarfBuzz subset input. ${ctx}`);
220199
221 const input = harfbuzzJsWasm.hb_subset_input_create_or_fail();
222 if (input === 0) {
223 throw new Error(`Failed to create HarfBuzz subset input. ${ctx}`);
224 }
200 const fontBuffer = harfbuzzJsWasm.malloc(sfntFont.byteLength);
201 getHeapU8().set(new Uint8Array(sfntFont), fontBuffer);
225202
226 const fontBuffer = harfbuzzJsWasm.malloc(sfntFont.byteLength);
227 getHeapU8().set(new Uint8Array(sfntFont), fontBuffer);
203 const blob = harfbuzzJsWasm.hb_blob_create(
204 fontBuffer,
205 sfntFont.byteLength,
206 2,
207 0,
208 0,
209 );
210 const face = harfbuzzJsWasm.hb_face_create(blob, 0);
211 harfbuzzJsWasm.hb_blob_destroy(blob);
228212
229 const blob = harfbuzzJsWasm.hb_blob_create(
230 fontBuffer,
231 sfntFont.byteLength,
232 2,
233 0,
234 0,
235 );
236 const face = harfbuzzJsWasm.hb_face_create(blob, 0);
237 harfbuzzJsWasm.hb_blob_destroy(blob);
213 try {
214 const layoutFeatures = harfbuzzJsWasm.hb_subset_input_set(input, 6);
215 ASSERT(layoutFeatures, `Failed to set layout features. ${ctx}`);
216 harfbuzzJsWasm.hb_set_clear(layoutFeatures);
217 harfbuzzJsWasm.hb_set_invert(layoutFeatures);
238218
239 try {
240 const layoutFeatures = harfbuzzJsWasm.hb_subset_input_set(input, 6);
241 harfbuzzJsWasm.hb_set_clear(layoutFeatures);
242 harfbuzzJsWasm.hb_set_invert(layoutFeatures);
243
244 const inputUnicodes = harfbuzzJsWasm.hb_subset_input_unicode_set(input);
245 for (const char of text) {
246 harfbuzzJsWasm.hb_set_add(inputUnicodes, char.codePointAt(0) || 0);
219 const inputUnicodes = harfbuzzJsWasm.hb_subset_input_unicode_set(input);
220 for (const range of unicodes) {
221 if (typeof range === "string") {
222 for (const char of range) harfbuzzJsWasm.hb_set_add(inputUnicodes, char.codePointAt(0) || 0);
223 } else if (typeof range === "number") {
224 harfbuzzJsWasm.hb_set_add(inputUnicodes, range);
225 } else {
226 for (let cp = range.start; cp <= range.end; cp++) harfbuzzJsWasm.hb_set_add(inputUnicodes, cp);
247227 }
228 }
248229
249 if (opt.variationAxes) {
250 for (const [axisName, value] of Object.entries(opt.variationAxes)) {
251 if (typeof value === "number") {
252 if (
253 !harfbuzzJsWasm.hb_subset_input_pin_axis_location(
254 input,
255 face,
256 hbTag(axisName),
257 value,
258 )
259 ) {
260 throw new Error(
261 `Failed to pin variation axis ${axisName}=${value}. ${ctx}`,
262 );
263 }
264 } else if (
265 !harfbuzzJsWasm.hb_subset_input_set_axis_range(
266 input,
267 face,
268 hbTag(axisName),
269 value.min,
270 value.max,
271 value.default ?? Number.NaN,
272 )
273 ) {
274 throw new Error(
275 `Failed to set variation axis range ${axisName}=[${value.min}, ${value.max}]${
276 value.default == null ? "" : ` default=${value.default}`
277 }. ${ctx}`,
278 );
279 }
230 if (opt.variationAxes) {
231 for (const [axisName, value] of Object.entries(opt.variationAxes)) {
232 if (typeof value === "number") {
233 ASSERT(harfbuzzJsWasm.hb_subset_input_pin_axis_location(
234 input,
235 face,
236 hbTag(axisName),
237 value,
238 ));
239 } else {
240 ASSERT(harfbuzzJsWasm.hb_subset_input_set_axis_range(
241 input,
242 face,
243 hbTag(axisName),
244 value.min,
245 value.max,
246 value.default ?? Number.NaN,
247 ));
280248 }
281249 }
250 }
282251
283 const subset = harfbuzzJsWasm.hb_subset_or_fail(face, input);
284 if (subset === 0) {
285 throw new Error(`HarfBuzz failed to subset font. ${ctx}`);
286 }
252 const subset = harfbuzzJsWasm.hb_subset_or_fail(face, input);
253 ASSERT(subset, `HarfBuzz failed to subset font. ${ctx}`);
287254
255 try {
256 const result = harfbuzzJsWasm.hb_face_reference_blob(subset);
288257 try {
289 const result = harfbuzzJsWasm.hb_face_reference_blob(subset);
290 try {
291 const offset = harfbuzzJsWasm.hb_blob_get_data(result, 0);
292 const subsetByteLength = harfbuzzJsWasm.hb_blob_get_length(result);
293 if (subsetByteLength === 0) {
294 throw new Error(`Subset font was empty. ${ctx}`);
295 }
296 const subsetFont = Uint8Array.from(
297 getHeapU8().subarray(offset, offset + subsetByteLength),
298 );
299 return await convertFromSfnt(subsetFont, opt.targetFormat);
300 } finally {
301 harfbuzzJsWasm.hb_blob_destroy(result);
302 }
258 const offset = harfbuzzJsWasm.hb_blob_get_data(result, 0);
259 const subsetByteLength = harfbuzzJsWasm.hb_blob_get_length(result);
260 ASSERT(subsetByteLength > 0, `Subset font was empty. ${ctx}`);
261 const subsetFont = Uint8Array.from(
262 getHeapU8().subarray(offset, offset + subsetByteLength),
263 );
264 return await convertFromSfnt(subsetFont, opt.targetFormat);
303265 } finally {
304 harfbuzzJsWasm.hb_face_destroy(subset);
266 harfbuzzJsWasm.hb_blob_destroy(result);
305267 }
306268 } finally {
307 harfbuzzJsWasm.hb_subset_input_destroy(input);
308 harfbuzzJsWasm.hb_face_destroy(face);
309 harfbuzzJsWasm.free(fontBuffer);
269 harfbuzzJsWasm.hb_face_destroy(subset);
310270 }
311 });
271 } finally {
272 harfbuzzJsWasm.hb_subset_input_destroy(input);
273 harfbuzzJsWasm.hb_face_destroy(face);
274 harfbuzzJsWasm.free(fontBuffer);
275 }
312276}
313277
314278type VariationAxisRange = { min: number; max: number; default?: number };
......@@ -367,25 +331,20 @@ const loadAndInitializeHarfbuzz = async.once(async () => {
367331 };
368332});
369333
370let subsetFontLock = Promise.resolve();
371function subsetFontQueue<T>(fn: () => Promise<T>) {
372 const run = subsetFontLock.then(fn, fn);
373 subsetFontLock = run.then(() => undefined, () => undefined);
374 return run;
375}
376
377334function hbTag(str: string) {
378335 return str.split("").reduce((a, ch) => (a << 8) + ch.charCodeAt(0), 0);
379336}
380337
381338import * as sg from "#sitegen";
382339import * as fs from "#sitegen/fs";
340import { ASSERT } from "@clo/lib/assert";
383341import * as async from "@clo/lib/async";
384342import * as error from "@clo/lib/error";
385343import * as progress from "@clo/lib/progress";
386import * as crypto from "node:crypto";
344import * as queue from "@clo/lib/queue";
387345import { readFile } from "node:fs/promises";
388346import * as os from "node:os";
389347import * as path from "node:path";
390348import * as wawoff2 from "wawoff2";
349import { ttf2eot } from "./font/ttf2eot.ts";
391350import * as incr from "./incremental.ts";
framework/font/ttf2eot.ts created+301
......@@ -0,0 +1,301 @@
1/**
2 * ESM port of https://github.com/fontello/ttf2eot
3 * @module
4 */
5
6/**
7 * Offsets in EOT file structure. Refer to EOTPrefix in OpenTypeUtilities.cpp
8 */
9const offsets = {
10 length: 0,
11 fontLength: 4,
12 version: 8,
13 charset: 26,
14 magic: 34,
15 fontPanose: 16,
16 italic: 27,
17 weight: 28,
18 unicodeRange: 36,
19 codepageRange: 52,
20 checksumAdjustment: 60,
21};
22
23/**
24 * Offsets in different SFNT (TTF) structures. See OpenTypeUtilities.cpp
25 */
26const sfntOffsets = {
27 // sfntHeader:
28 numTables: 4,
29
30 // TableDirectoryEntry
31 tableTag: 0,
32 tableOffset: 8,
33 tableLength: 12,
34
35 // OS2Table
36 os2Weight: 4,
37 os2FontPanose: 32,
38 os2UnicodeRange: 42,
39 os2FsSelection: 62,
40 os2CodepageRange: 78,
41
42 // headTable
43 headChecksumAdjustment: 8,
44
45 // nameTable
46 nametableFormat: 0,
47 nametableCount: 2,
48 nametableStringOffset: 4,
49
50 // nameRecord
51 namePlatformId: 0,
52 nameEncodingId: 2,
53 nameLanguageId: 4,
54 nameNameId: 6,
55 nameLength: 8,
56 nameOffset: 10,
57};
58
59/**
60 * Sizes of structures
61 */
62const sizeof = {
63 sfntTableEntry: 16,
64 sfntHeader: 12,
65 sfntNametable: 6,
66 sfntNametableEntry: 12,
67 eotPrefix: 82,
68};
69
70/**
71 * Magic numbers
72 */
73const magic = {
74 version: 0x00020001,
75 magic: 0x504c,
76 charset: 1,
77 english: 0x0409,
78};
79
80interface SfntTableEntry {
81 tag: string;
82 offset: number;
83 length: number;
84}
85
86interface NameTable {
87 format: number;
88 count: number;
89 stringOffset: number;
90}
91
92interface NameRecord {
93 platformID: number;
94 encodingID: number;
95 languageID: number;
96 nameID: number;
97 length: number;
98 offset: number;
99}
100
101function dataView(bytes: Uint8Array): DataView {
102 return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
103}
104
105function readUint8(bytes: Uint8Array, offset = 0): number {
106 return dataView(bytes).getUint8(offset);
107}
108
109function readUint16BE(bytes: Uint8Array, offset = 0): number {
110 return dataView(bytes).getUint16(offset, false);
111}
112
113function readUint32BE(bytes: Uint8Array, offset = 0): number {
114 return dataView(bytes).getUint32(offset, false);
115}
116
117function writeUint8(bytes: Uint8Array, value: number, offset: number): void {
118 dataView(bytes).setUint8(offset, value);
119}
120
121function writeUint16LE(bytes: Uint8Array, value: number, offset: number): void {
122 dataView(bytes).setUint16(offset, value, true);
123}
124
125function writeUint32LE(bytes: Uint8Array, value: number, offset: number): void {
126 dataView(bytes).setUint32(offset, value, true);
127}
128
129function concat(parts: Uint8Array[]): Uint8Array {
130 let length = 0;
131 for (const part of parts) length += part.length;
132
133 const out = new Uint8Array(length);
134 let offset = 0;
135 for (const part of parts) {
136 out.set(part, offset);
137 offset += part.length;
138 }
139 return out;
140}
141
142function readTag(bytes: Uint8Array, offset: number): string {
143 return String.fromCharCode(
144 bytes[offset]!,
145 bytes[offset + 1]!,
146 bytes[offset + 2]!,
147 bytes[offset + 3]!,
148 );
149}
150
151/**
152 * Utility function to convert buffer of utf16be chars to buffer of utf16le
153 * chars prefixed with length and suffixed with zero
154 */
155function strbuf(str: Uint8Array): Uint8Array {
156 const arr = new Uint8Array(str.length + 4);
157
158 writeUint16LE(arr, str.length, 0);
159
160 for (let i = 0; i < str.length; i += 2) {
161 writeUint16LE(arr, readUint16BE(str, i), i + 2);
162 }
163
164 writeUint16LE(arr, 0, arr.length - 2);
165
166 return arr;
167}
168
169/**
170 * Convert a TTF font -> EOT font for use in older Internet Explorer versions.
171 *
172 * NOTE: The font's FullName MUST begin with FamilyName. For example, if
173 * FamilyName is `fontello`, then FullName should be `fontello regular` and so on.
174 */
175export function ttf2eot(arr: Uint8Array): Uint8Array {
176 arr = arr.slice();
177
178 const out = new Uint8Array(sizeof.eotPrefix);
179
180 writeUint32LE(out, arr.length, offsets.fontLength);
181 writeUint32LE(out, magic.version, offsets.version);
182 writeUint8(out, magic.charset, offsets.charset);
183 writeUint16LE(out, magic.magic, offsets.magic);
184
185 let familyName: Uint8Array = new Uint8Array([0]);
186 let subfamilyName: Uint8Array = new Uint8Array([0]);
187 let fullName: Uint8Array = new Uint8Array([0]);
188 let versionString: Uint8Array = new Uint8Array([0]);
189
190 let haveOs2 = false;
191 let haveName = false;
192 let haveHead = false;
193
194 const numTables = readUint16BE(arr, sfntOffsets.numTables);
195
196 for (let i = 0; i < numTables; ++i) {
197 const data = arr.subarray(sizeof.sfntHeader + i * sizeof.sfntTableEntry);
198 const tableEntry: SfntTableEntry = {
199 tag: readTag(data, sfntOffsets.tableTag),
200 offset: readUint32BE(data, sfntOffsets.tableOffset),
201 length: readUint32BE(data, sfntOffsets.tableLength),
202 };
203
204 const table = arr.subarray(tableEntry.offset, tableEntry.offset + tableEntry.length);
205
206 if (tableEntry.tag === "OS/2") {
207 haveOs2 = true;
208 const os2Version = readUint16BE(table);
209
210 for (let j = 0; j < 10; ++j) {
211 writeUint8(out, readUint8(table, sfntOffsets.os2FontPanose + j), offsets.fontPanose + j);
212 }
213
214 writeUint8(out, readUint16BE(table, sfntOffsets.os2FsSelection) & 0x01, offsets.italic);
215 writeUint32LE(out, readUint16BE(table, sfntOffsets.os2Weight), offsets.weight);
216
217 for (let j = 0; j < 4; ++j) {
218 writeUint32LE(
219 out,
220 readUint32BE(table, sfntOffsets.os2UnicodeRange + j * 4),
221 offsets.unicodeRange + j * 4,
222 );
223 }
224
225 if (os2Version >= 1) {
226 for (let j = 0; j < 2; ++j) {
227 writeUint32LE(
228 out,
229 readUint32BE(table, sfntOffsets.os2CodepageRange + j * 4),
230 offsets.codepageRange + j * 4,
231 );
232 }
233 }
234 } else if (tableEntry.tag === "head") {
235 haveHead = true;
236 writeUint32LE(out, readUint32BE(table, sfntOffsets.headChecksumAdjustment), offsets.checksumAdjustment);
237 } else if (tableEntry.tag === "name") {
238 haveName = true;
239
240 const nameTable: NameTable = {
241 format: readUint16BE(table, sfntOffsets.nametableFormat),
242 count: readUint16BE(table, sfntOffsets.nametableCount),
243 stringOffset: readUint16BE(table, sfntOffsets.nametableStringOffset),
244 };
245
246 for (let j = 0; j < nameTable.count; ++j) {
247 const nameRecordBytes = table.subarray(sizeof.sfntNametable + j * sizeof.sfntNametableEntry);
248 const name: NameRecord = {
249 platformID: readUint16BE(nameRecordBytes, sfntOffsets.namePlatformId),
250 encodingID: readUint16BE(nameRecordBytes, sfntOffsets.nameEncodingId),
251 languageID: readUint16BE(nameRecordBytes, sfntOffsets.nameLanguageId),
252 nameID: readUint16BE(nameRecordBytes, sfntOffsets.nameNameId),
253 length: readUint16BE(nameRecordBytes, sfntOffsets.nameLength),
254 offset: readUint16BE(nameRecordBytes, sfntOffsets.nameOffset),
255 };
256
257 if (name.platformID === 3 && name.encodingID === 1 && name.languageID === magic.english) {
258 const s = strbuf(table.subarray(
259 nameTable.stringOffset + name.offset,
260 nameTable.stringOffset + name.offset + name.length,
261 ));
262
263 switch (name.nameID) {
264 case 1:
265 familyName = s;
266 break;
267 case 2:
268 subfamilyName = s;
269 break;
270 case 4:
271 fullName = s;
272 break;
273 case 5:
274 versionString = s;
275 break;
276 }
277 }
278 }
279 }
280 if (haveOs2 && haveName && haveHead) break;
281 }
282
283 if (!(haveOs2 && haveName && haveHead)) {
284 throw new Error("Required section not found");
285 }
286
287 // Create final buffer with the the same array type as input one.
288 const eot = concat([
289 out,
290 familyName,
291 subfamilyName,
292 versionString,
293 fullName,
294 new Uint8Array([0, 0]),
295 arr,
296 ]);
297
298 writeUint32LE(eot, eot.length, offsets.length); // Calculate overall length
299
300 return eot;
301}
framework/lib/sitegen.ts+1-1
......@@ -44,7 +44,7 @@ export interface Font {
4444 vars?: Record<string, FontVariableAxis>;
4545 layoutFeatures?: string[];
4646 unicodes: FontRange | FontRange[];
47 /** Include [ext] to autofill 'woff2' */
47 /** example: `/myfont.[ext]` */
4848 asset: string;
4949 }>;
5050}
lib/async.ts+31
......@@ -261,5 +261,36 @@ export function delay(ms: number): Cancelable<void> {
261261 );
262262}
263263
264export interface Deferred<T> extends AsyncDisposable, PromiseLike<T> {
265}
266
267/**
268 * Return something that can be awaited later without worry of an unhandled
269 * rejection. By disposing it, the promise exception will always be reported.
270 *
271 * ```ts
272 * // incorrect, possible unhandled rejection between a and b.
273 * const a = promise();
274 * const b = await promise();
275 * const c = await promise(b, await a);
276 * // correct
277 * await using a = async.deferred(promise());
278 * const b = await promise();
279 * const c = await promise(b, await a);
280 * ```
281 */
282export function deferred<T>(promise: Promise<T>): Deferred<T> {
283 let handled = false;
284 promise.catch(() => {}); // potential error is re-thrown on asyncDispose
285 return {
286 async [Symbol.asyncDispose]() {
287 if (!handled) await promise;
288 },
289 then(onFufilled, onRejected) {
290 return promise.finally(() => (handled = true)).then(onFufilled, onRejected);
291 },
292 };
293}
294
264295import { ASSERT } from "./assert.ts";
265296import * as ts from "./ts.ts";
lib/mime.ts+2-2
......@@ -1,6 +1,5 @@
11/**
2 * a small mime type lookup table library.
3 *
2 * a small mime type library.
43 * @module
54 */
65
......@@ -56,6 +55,7 @@ const extensions = new Map(
5655 + ".dmg" + "#" + "application/octet-stream" + "\n"
5756 + ".doc" + "#" + "application/msword" + "\n"
5857 + ".docx" + "#" + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + "\n"
58 + ".eot" + "#" + "application/vnd.ms-fontobject" + "\n"
5959 + ".epub" + "#" + "application/epub+zip" + "\n"
6060 + ".exe" + "#" + "application/x-msdownload" + "\n"
6161 + ".flv" + "#" + "video/x-flv" + "\n"
lib/readme.changes.md+7
......@@ -1,5 +1,12 @@
11# notable changes in clover's typescript library
22
3## v5
4
5### features
6
7- `mime`'s database contains `.eot` for embedded opentype fonts.
8- `async.deferred`
9
310## v4
411
512### breaking
package-lock.json+88-78
......@@ -9,6 +9,7 @@
99 "@clo/markodown": "npm:@paperclover/markodown@1.0.0",
1010 "@marko/compiler": "5.39.62",
1111 "@types/node": "24.x",
12 "@types/wawoff2": "^1.0.2",
1213 "dashjs": "5.0.3",
1314 "dprint": "0.53.1",
1415 "esbuild": "0.25.5",
......@@ -571,9 +572,9 @@
571572 }
572573 },
573574 "node_modules/@esbuild/openharmony-arm64": {
574 "version": "0.27.4",
575 "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
576 "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
575 "version": "0.27.7",
576 "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
577 "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
577578 "cpu": [
578579 "arm64"
579580 ],
......@@ -1116,6 +1117,15 @@
11161117 "undici-types": "~7.16.0"
11171118 }
11181119 },
1120 "node_modules/@types/wawoff2": {
1121 "version": "1.0.2",
1122 "resolved": "https://registry.npmjs.org/@types/wawoff2/-/wawoff2-1.0.2.tgz",
1123 "integrity": "sha512-UVq4NxMuBywz5gklafg1HcENrK02trh/q+ifyvO6xktflGXRtf11GYsia3/l5nxg2edFQRlnUT6ivnk898WDiw==",
1124 "license": "MIT",
1125 "dependencies": {
1126 "@types/node": "*"
1127 }
1128 },
11191129 "node_modules/@vitest/expect": {
11201130 "version": "4.1.2",
11211131 "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz",
......@@ -2493,9 +2503,9 @@
24932503 }
24942504 },
24952505 "node_modules/vitest/node_modules/@esbuild/aix-ppc64": {
2496 "version": "0.27.4",
2497 "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
2498 "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
2506 "version": "0.27.7",
2507 "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
2508 "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
24992509 "cpu": [
25002510 "ppc64"
25012511 ],
......@@ -2509,9 +2519,9 @@
25092519 }
25102520 },
25112521 "node_modules/vitest/node_modules/@esbuild/android-arm": {
2512 "version": "0.27.4",
2513 "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
2514 "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
2522 "version": "0.27.7",
2523 "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
2524 "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
25152525 "cpu": [
25162526 "arm"
25172527 ],
......@@ -2525,9 +2535,9 @@
25252535 }
25262536 },
25272537 "node_modules/vitest/node_modules/@esbuild/android-arm64": {
2528 "version": "0.27.4",
2529 "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
2530 "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
2538 "version": "0.27.7",
2539 "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
2540 "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
25312541 "cpu": [
25322542 "arm64"
25332543 ],
......@@ -2541,9 +2551,9 @@
25412551 }
25422552 },
25432553 "node_modules/vitest/node_modules/@esbuild/android-x64": {
2544 "version": "0.27.4",
2545 "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
2546 "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
2554 "version": "0.27.7",
2555 "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
2556 "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
25472557 "cpu": [
25482558 "x64"
25492559 ],
......@@ -2557,9 +2567,9 @@
25572567 }
25582568 },
25592569 "node_modules/vitest/node_modules/@esbuild/darwin-arm64": {
2560 "version": "0.27.4",
2561 "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
2562 "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
2570 "version": "0.27.7",
2571 "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
2572 "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
25632573 "cpu": [
25642574 "arm64"
25652575 ],
......@@ -2573,9 +2583,9 @@
25732583 }
25742584 },
25752585 "node_modules/vitest/node_modules/@esbuild/darwin-x64": {
2576 "version": "0.27.4",
2577 "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
2578 "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
2586 "version": "0.27.7",
2587 "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
2588 "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
25792589 "cpu": [
25802590 "x64"
25812591 ],
......@@ -2589,9 +2599,9 @@
25892599 }
25902600 },
25912601 "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": {
2592 "version": "0.27.4",
2593 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
2594 "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
2602 "version": "0.27.7",
2603 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
2604 "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
25952605 "cpu": [
25962606 "arm64"
25972607 ],
......@@ -2605,9 +2615,9 @@
26052615 }
26062616 },
26072617 "node_modules/vitest/node_modules/@esbuild/freebsd-x64": {
2608 "version": "0.27.4",
2609 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
2610 "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
2618 "version": "0.27.7",
2619 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
2620 "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
26112621 "cpu": [
26122622 "x64"
26132623 ],
......@@ -2621,9 +2631,9 @@
26212631 }
26222632 },
26232633 "node_modules/vitest/node_modules/@esbuild/linux-arm": {
2624 "version": "0.27.4",
2625 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
2626 "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
2634 "version": "0.27.7",
2635 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
2636 "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
26272637 "cpu": [
26282638 "arm"
26292639 ],
......@@ -2637,9 +2647,9 @@
26372647 }
26382648 },
26392649 "node_modules/vitest/node_modules/@esbuild/linux-arm64": {
2640 "version": "0.27.4",
2641 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
2642 "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
2650 "version": "0.27.7",
2651 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
2652 "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
26432653 "cpu": [
26442654 "arm64"
26452655 ],
......@@ -2653,9 +2663,9 @@
26532663 }
26542664 },
26552665 "node_modules/vitest/node_modules/@esbuild/linux-ia32": {
2656 "version": "0.27.4",
2657 "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
2658 "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
2666 "version": "0.27.7",
2667 "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
2668 "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
26592669 "cpu": [
26602670 "ia32"
26612671 ],
......@@ -2669,9 +2679,9 @@
26692679 }
26702680 },
26712681 "node_modules/vitest/node_modules/@esbuild/linux-loong64": {
2672 "version": "0.27.4",
2673 "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
2674 "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
2682 "version": "0.27.7",
2683 "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
2684 "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
26752685 "cpu": [
26762686 "loong64"
26772687 ],
......@@ -2685,9 +2695,9 @@
26852695 }
26862696 },
26872697 "node_modules/vitest/node_modules/@esbuild/linux-mips64el": {
2688 "version": "0.27.4",
2689 "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
2690 "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
2698 "version": "0.27.7",
2699 "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
2700 "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
26912701 "cpu": [
26922702 "mips64el"
26932703 ],
......@@ -2701,9 +2711,9 @@
27012711 }
27022712 },
27032713 "node_modules/vitest/node_modules/@esbuild/linux-ppc64": {
2704 "version": "0.27.4",
2705 "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
2706 "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
2714 "version": "0.27.7",
2715 "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
2716 "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
27072717 "cpu": [
27082718 "ppc64"
27092719 ],
......@@ -2717,9 +2727,9 @@
27172727 }
27182728 },
27192729 "node_modules/vitest/node_modules/@esbuild/linux-riscv64": {
2720 "version": "0.27.4",
2721 "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
2722 "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
2730 "version": "0.27.7",
2731 "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
2732 "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
27232733 "cpu": [
27242734 "riscv64"
27252735 ],
......@@ -2733,9 +2743,9 @@
27332743 }
27342744 },
27352745 "node_modules/vitest/node_modules/@esbuild/linux-s390x": {
2736 "version": "0.27.4",
2737 "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
2738 "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
2746 "version": "0.27.7",
2747 "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
2748 "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
27392749 "cpu": [
27402750 "s390x"
27412751 ],
......@@ -2749,9 +2759,9 @@
27492759 }
27502760 },
27512761 "node_modules/vitest/node_modules/@esbuild/linux-x64": {
2752 "version": "0.27.4",
2753 "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
2754 "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
2762 "version": "0.27.7",
2763 "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
2764 "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
27552765 "cpu": [
27562766 "x64"
27572767 ],
......@@ -2765,9 +2775,9 @@
27652775 }
27662776 },
27672777 "node_modules/vitest/node_modules/@esbuild/netbsd-arm64": {
2768 "version": "0.27.4",
2769 "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
2770 "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
2778 "version": "0.27.7",
2779 "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
2780 "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
27712781 "cpu": [
27722782 "arm64"
27732783 ],
......@@ -2781,9 +2791,9 @@
27812791 }
27822792 },
27832793 "node_modules/vitest/node_modules/@esbuild/netbsd-x64": {
2784 "version": "0.27.4",
2785 "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
2786 "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
2794 "version": "0.27.7",
2795 "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
2796 "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
27872797 "cpu": [
27882798 "x64"
27892799 ],
......@@ -2797,9 +2807,9 @@
27972807 }
27982808 },
27992809 "node_modules/vitest/node_modules/@esbuild/openbsd-arm64": {
2800 "version": "0.27.4",
2801 "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
2802 "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
2810 "version": "0.27.7",
2811 "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
2812 "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
28032813 "cpu": [
28042814 "arm64"
28052815 ],
......@@ -2813,9 +2823,9 @@
28132823 }
28142824 },
28152825 "node_modules/vitest/node_modules/@esbuild/openbsd-x64": {
2816 "version": "0.27.4",
2817 "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
2818 "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
2826 "version": "0.27.7",
2827 "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
2828 "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
28192829 "cpu": [
28202830 "x64"
28212831 ],
......@@ -2829,9 +2839,9 @@
28292839 }
28302840 },
28312841 "node_modules/vitest/node_modules/@esbuild/sunos-x64": {
2832 "version": "0.27.4",
2833 "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
2834 "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
2842 "version": "0.27.7",
2843 "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
2844 "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
28352845 "cpu": [
28362846 "x64"
28372847 ],
......@@ -2845,9 +2855,9 @@
28452855 }
28462856 },
28472857 "node_modules/vitest/node_modules/@esbuild/win32-arm64": {
2848 "version": "0.27.4",
2849 "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
2850 "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
2858 "version": "0.27.7",
2859 "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
2860 "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
28512861 "cpu": [
28522862 "arm64"
28532863 ],
......@@ -2861,9 +2871,9 @@
28612871 }
28622872 },
28632873 "node_modules/vitest/node_modules/@esbuild/win32-ia32": {
2864 "version": "0.27.4",
2865 "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
2866 "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
2874 "version": "0.27.7",
2875 "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
2876 "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
28672877 "cpu": [
28682878 "ia32"
28692879 ],
......@@ -2877,9 +2887,9 @@
28772887 }
28782888 },
28792889 "node_modules/vitest/node_modules/@esbuild/win32-x64": {
2880 "version": "0.27.4",
2881 "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
2882 "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
2890 "version": "0.27.7",
2891 "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
2892 "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
28832893 "cpu": [
28842894 "x64"
28852895 ],
package.json+1
......@@ -5,6 +5,7 @@
55 "@clo/markodown": "npm:@paperclover/markodown@1.0.0",
66 "@marko/compiler": "5.39.62",
77 "@types/node": "24.x",
8 "@types/wawoff2": "^1.0.2",
89 "dashjs": "5.0.3",
910 "dprint": "0.53.1",
1011 "esbuild": "0.25.5",
src/global.css+20-4
......@@ -1,6 +1,10 @@
11@font-face {
22 font-family: "recu";
3 src: url("/recultramin.woff2") format("woff2");
3 src: url("/recultramin.eot");
4 src:
5 url("/recultramin.eot?#iefix") format("embedded-opentype"),
6 url("/recultramin.woff2") format("woff2"),
7 url("/recultramin.ttf") format("truetype");
48 font-weight: 400 750;
59 font-style: normal;
610 font-display: swap;
......@@ -10,7 +14,11 @@
1014}
1115@font-face {
1216 font-family: "rmo";
13 src: url("/recmono.woff2") format("woff2");
17 src: url("/recmono.eot");
18 src:
19 url("/recmono.eot?#iefix") format("embedded-opentype"),
20 url("/recmono.woff2") format("woff2"),
21 url("/recmono.ttf") format("truetype");
1422 font-weight: 400 800;
1523 font-style: normal;
1624 font-display: swap;
......@@ -20,7 +28,11 @@
2028}
2129@font-face {
2230 font-family: "rmo";
23 src: url("/recmonorest.woff2") format("woff2");
31 src: url("/recmonorest.eot");
32 src:
33 url("/recmonorest.eot?#iefix") format("embedded-opentype"),
34 url("/recmonorest.woff2") format("woff2"),
35 url("/recmonorest.ttf") format("truetype");
2436 font-weight: 400 800;
2537 font-style: normal;
2638 font-display: swap;
......@@ -42,7 +54,11 @@
4254}
4355@font-face {
4456 font-family: "recu";
45 src: url("/recthin.woff2") format("woff2");
57 src: url("/recthin.eot");
58 src:
59 url("/recthin.eot?#iefix") format("embedded-opentype"),
60 url("/recthin.woff2") format("woff2"),
61 url("/recthin.ttf") format("truetype");
4662 font-weight: 350;
4763 font-style: normal;
4864 font-display: swap;
src/q+a/questions.css+10-2
......@@ -1,6 +1,10 @@
11@font-face {
22 font-family: "recu";
3 src: url("/recqa.woff2") format("woff2");
3 src: url("/recqa.eot");
4 src:
5 url("/recqa.eot?#iefix") format("embedded-opentype"),
6 url("/recqa.woff2") format("woff2"),
7 url("/recqa.ttf") format("truetype");
48 font-weight: 400 800;
59 font-style: normal;
610 font-display: swap;
......@@ -10,7 +14,11 @@
1014}
1115@font-face {
1216 font-family: recu;
13 src: url("/recexotic.woff2") format("woff2");
17 src: url("/recexotic.eot");
18 src:
19 url("/recexotic.eot?#iefix") format("embedded-opentype"),
20 url("/recexotic.woff2") format("woff2"),
21 url("/recexotic.ttf") format("truetype");
1422 font-weight: 400 800;
1523 font-style: normal;
1624 font-display: swap;