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(...@@ -81,7 +81,7 @@ export async function bundleCssFiles(
81 const build = await esbuild.build({81 const build = await esbuild.build({
82 bundle: true,82 bundle: true,
83 entryPoints: ["$input$"],83 entryPoints: ["$input$"],
84 external: ["*.woff2", "*.ttf", "*.png", "*.jpeg"],84 external: ["*.woff2", "*.eot", "*.eot?#iefix", "*.ttf", "*.png", "*.jpeg"],
85 metafile: true,85 metafile: true,
86 minify: !dev,86 minify: !dev,
87 plugins: [87 plugins: [
framework/font.ts+131-172
...@@ -10,15 +10,6 @@ export async function buildFonts(p: progress.Ref, fonts: sg.Font[]) {...@@ -10,15 +10,6 @@ export async function buildFonts(p: progress.Ref, fonts: sg.Font[]) {
10 });10 });
11 }11 }
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
22 function getVariationAxes(vars: sg.FontVars) {13 function getVariationAxes(vars: sg.FontVars) {
23 return Object.fromEntries(14 return Object.fromEntries(
24 Object.entries(vars).map(([key, value]) => [15 Object.entries(vars).map(([key, value]) => [
...@@ -31,36 +22,56 @@ export async function buildFonts(p: progress.Ref, fonts: sg.Font[]) {...@@ -31,36 +22,56 @@ export async function buildFonts(p: progress.Ref, fonts: sg.Font[]) {
31 await Promise.all(fonts.map((font) =>22 await Promise.all(fonts.map((font) =>
32 incr.work(node, async (io, font) => {23 incr.work(node, async (io, font) => {
33 using node = io.start(font.name);24 using node = io.start(font.name);
34 const baseFile = await fetchFont(font.name, font.sources);25 const baseFile = await fetchFont(node, font.name, font.sources);
35 const baseBuffer = await fs.readFile(baseFile);26 await using baseBuffer = async.deferred(fs.readFile(baseFile));
36 await Promise.all(font.subsets.map(async (subset) => {27 await Promise.all(font.subsets.map(async (subset) => {
37 const args = getArgs(subset.vars ?? {});28 const args = getArgs(subset.vars ?? {});
38 const subsetText = fontRangesToText(29 const unicodes = Array.isArray(subset.unicodes) ? subset.unicodes : [subset.unicodes];
39 Array.isArray(subset.unicodes) ? subset.unicodes : [subset.unicodes],30 const charCount = unicodes.reduce<number>((count, range) => {
40 );31 if (typeof range === "string") return count + [...range].length;
41 using _ = node.start(args.join(" "));32 if (typeof range === "number") return count + 1;
42 const woff = await subsetFontBuffer(baseBuffer, subsetText, {33 return count + (range.end - range.start + 1);
43 targetFormat: "woff2",34 }, 0);
44 variationAxes: subset.vars35 const variationAxes = subset.vars ? getVariationAxes(subset.vars) : undefined;
45 ? getVariationAxes(subset.vars)36 using _ = node.start("subset: " + [...args, `chars=${charCount}`].join(" "));
46 : undefined,37 const [woff, ttf] = await Promise.all([
47 sourceName: baseFile,38 subsetFontBuffer(await baseBuffer, unicodes, {
48 assetName: subset.asset.replace("[ext]", "woff2"),39 targetFormat: "woff2",
49 });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);
50 await io.writeAsset({52 await io.writeAsset({
51 pathname: subset.asset.replace("[ext]", "woff2"),53 pathname: subset.asset.replace("[ext]", "woff2"),
52 buffer: woff,54 buffer: woff,
53 });55 });
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 });
54 }));64 }));
55 }, font)65 }, font)
56 ));66 ));
57}67}
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");
60 const errs = [];71 const errs = [];
6172
62 for (const source of sources) {73 for (const source of sources) {
63 const cacheName = path.join("", name + path.extname(source));74 const cacheName = name + path.extname(source);
64 if (fs.existsSync(cacheName)) return cacheName;75 if (fs.existsSync(cacheName)) return cacheName;
6576
66 if (source.startsWith("https://")) {77 if (source.startsWith("https://")) {
...@@ -106,11 +117,9 @@ export async function fetchFont(name: string, sources: string[]) {...@@ -106,11 +117,9 @@ export async function fetchFont(name: string, sources: string[]) {
106 path.posix.join(home, ".local/share/fonts"),117 path.posix.join(home, ".local/share/fonts"),
107 ];118 ];
108 for (const base of bases) {119 for (const base of bases) {
109 const found = fs.readDirRecOptionalSync(base)120 const files = fs.readDirRecOptionalSync(base);
110 .find((file) => path.basename(file) === source);121 const found = files.find((file) => path.basename(file) === source);
111 if (found) {122 if (found) return path.join(base, found);
112 return path.join(base, found);
113 }
114 }123 }
115 errs.push(124 errs.push(
116 new Error(125 new Error(
...@@ -123,46 +132,20 @@ export async function fetchFont(name: string, sources: string[]) {...@@ -123,46 +132,20 @@ export async function fetchFont(name: string, sources: string[]) {
123 throw new AggregateError(errs, `Failed to fetch the ${name} font`);132 throw new AggregateError(errs, `Failed to fetch the ${name} font`);
124}133}
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
145function normalizeAxisTag(tag: string) {135function normalizeAxisTag(tag: string) {
146 const lower = tag.toLowerCase();136 const lower = tag.toLowerCase();
147 return lower === "wght" || lower === "slnt" ? lower : tag;137 return lower === "wght" || lower === "slnt" ? lower : tag;
148}138}
149139
150function detectFontFormat(buffer: Uint8Array): "sfnt" | "woff2" {140async function convertToSfnt(buffer: Uint8Array) {
151 const signature = String.fromCharCode(...buffer.subarray(0, 4));141 const signature = String.fromCharCode(...buffer.subarray(0, 4));
152 if (signature === "wOF2") return "woff2";
153 if (142 if (
154 signature === "true"143 signature === "true"
155 || signature === "OTTO"144 || signature === "OTTO"
156 || signature === "\x00\x01\x00\x00"145 || signature === "\x00\x01\x00\x00"
157 ) {146 ) {
158 return "sfnt";147 return buffer;
159 }148 }
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;
166 return new Uint8Array(await wawoff2.decompress(buffer));149 return new Uint8Array(await wawoff2.decompress(buffer));
167}150}
168151
...@@ -174,18 +157,29 @@ async function convertFromSfnt(...@@ -174,18 +157,29 @@ async function convertFromSfnt(
174 return new Uint8Array(await wawoff2.compress(buffer));157 return new Uint8Array(await wawoff2.compress(buffer));
175}158}
176159
177function getSubsetContext(opt: {160const subsetQueue = new queue.PriorityQueue(1);
178 sourceName?: string;161const subsetFontBuffer = subsetQueue.wrap(subsetFontBufferUnqueued);
179 assetName?: string;162
180 variationAxes?: VariationAxes;163async function subsetFontBufferUnqueued(
181 targetFormat: "sfnt" | "woff2";164 originalFont: Uint8Array,
182 text: string;165 unicodes: sg.FontRange[],
183}) {166 opt: {
184 const fields = [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`,
185 opt.sourceName ? `source=${opt.sourceName}` : "",180 opt.sourceName ? `source=${opt.sourceName}` : "",
186 opt.assetName ? `asset=${opt.assetName}` : "",181 opt.assetName ? `asset=${opt.assetName}` : "",
187 `target=${opt.targetFormat}`,182 `target=${opt.targetFormat}`,
188 `textLength=${opt.text.length}`,
189 opt.variationAxes183 opt.variationAxes
190 ? `axes=${184 ? `axes=${
191 Object.entries(opt.variationAxes).map(([key, value]) =>185 Object.entries(opt.variationAxes).map(([key, value]) =>
...@@ -195,120 +189,90 @@ function getSubsetContext(opt: {...@@ -195,120 +189,90 @@ function getSubsetContext(opt: {
195 ).join(",")189 ).join(",")
196 }`190 }`
197 : "",191 : "",
198 ].filter(Boolean);192 ].filter(Boolean).join(" ");
199 return fields.join(" ");
200}
201193
202async function subsetFontBuffer(194 const { harfbuzzJsWasm, getHeapU8 } = await loadAndInitializeHarfbuzz();
203 originalFont: Uint8Array,195 const sfntFont = await convertToSfnt(originalFont);
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 }
217196
218 const { harfbuzzJsWasm, getHeapU8 } = await loadAndInitializeHarfbuzz();197 const input = harfbuzzJsWasm.hb_subset_input_create_or_fail();
219 const sfntFont = await convertToSfnt(originalFont);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();200 const fontBuffer = harfbuzzJsWasm.malloc(sfntFont.byteLength);
222 if (input === 0) {201 getHeapU8().set(new Uint8Array(sfntFont), fontBuffer);
223 throw new Error(`Failed to create HarfBuzz subset input. ${ctx}`);
224 }
225202
226 const fontBuffer = harfbuzzJsWasm.malloc(sfntFont.byteLength);203 const blob = harfbuzzJsWasm.hb_blob_create(
227 getHeapU8().set(new Uint8Array(sfntFont), fontBuffer);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(213 try {
230 fontBuffer,214 const layoutFeatures = harfbuzzJsWasm.hb_subset_input_set(input, 6);
231 sfntFont.byteLength,215 ASSERT(layoutFeatures, `Failed to set layout features. ${ctx}`);
232 2,216 harfbuzzJsWasm.hb_set_clear(layoutFeatures);
233 0,217 harfbuzzJsWasm.hb_set_invert(layoutFeatures);
234 0,
235 );
236 const face = harfbuzzJsWasm.hb_face_create(blob, 0);
237 harfbuzzJsWasm.hb_blob_destroy(blob);
238218
239 try {219 const inputUnicodes = harfbuzzJsWasm.hb_subset_input_unicode_set(input);
240 const layoutFeatures = harfbuzzJsWasm.hb_subset_input_set(input, 6);220 for (const range of unicodes) {
241 harfbuzzJsWasm.hb_set_clear(layoutFeatures);221 if (typeof range === "string") {
242 harfbuzzJsWasm.hb_set_invert(layoutFeatures);222 for (const char of range) harfbuzzJsWasm.hb_set_add(inputUnicodes, char.codePointAt(0) || 0);
243223 } else if (typeof range === "number") {
244 const inputUnicodes = harfbuzzJsWasm.hb_subset_input_unicode_set(input);224 harfbuzzJsWasm.hb_set_add(inputUnicodes, range);
245 for (const char of text) {225 } else {
246 harfbuzzJsWasm.hb_set_add(inputUnicodes, char.codePointAt(0) || 0);226 for (let cp = range.start; cp <= range.end; cp++) harfbuzzJsWasm.hb_set_add(inputUnicodes, cp);
247 }227 }
228 }
248229
249 if (opt.variationAxes) {230 if (opt.variationAxes) {
250 for (const [axisName, value] of Object.entries(opt.variationAxes)) {231 for (const [axisName, value] of Object.entries(opt.variationAxes)) {
251 if (typeof value === "number") {232 if (typeof value === "number") {
252 if (233 ASSERT(harfbuzzJsWasm.hb_subset_input_pin_axis_location(
253 !harfbuzzJsWasm.hb_subset_input_pin_axis_location(234 input,
254 input,235 face,
255 face,236 hbTag(axisName),
256 hbTag(axisName),237 value,
257 value,238 ));
258 )239 } else {
259 ) {240 ASSERT(harfbuzzJsWasm.hb_subset_input_set_axis_range(
260 throw new Error(241 input,
261 `Failed to pin variation axis ${axisName}=${value}. ${ctx}`,242 face,
262 );243 hbTag(axisName),
263 }244 value.min,
264 } else if (245 value.max,
265 !harfbuzzJsWasm.hb_subset_input_set_axis_range(246 value.default ?? Number.NaN,
266 input,247 ));
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 }
280 }248 }
281 }249 }
250 }
282251
283 const subset = harfbuzzJsWasm.hb_subset_or_fail(face, input);252 const subset = harfbuzzJsWasm.hb_subset_or_fail(face, input);
284 if (subset === 0) {253 ASSERT(subset, `HarfBuzz failed to subset font. ${ctx}`);
285 throw new Error(`HarfBuzz failed to subset font. ${ctx}`);
286 }
287254
255 try {
256 const result = harfbuzzJsWasm.hb_face_reference_blob(subset);
288 try {257 try {
289 const result = harfbuzzJsWasm.hb_face_reference_blob(subset);258 const offset = harfbuzzJsWasm.hb_blob_get_data(result, 0);
290 try {259 const subsetByteLength = harfbuzzJsWasm.hb_blob_get_length(result);
291 const offset = harfbuzzJsWasm.hb_blob_get_data(result, 0);260 ASSERT(subsetByteLength > 0, `Subset font was empty. ${ctx}`);
292 const subsetByteLength = harfbuzzJsWasm.hb_blob_get_length(result);261 const subsetFont = Uint8Array.from(
293 if (subsetByteLength === 0) {262 getHeapU8().subarray(offset, offset + subsetByteLength),
294 throw new Error(`Subset font was empty. ${ctx}`);263 );
295 }264 return await convertFromSfnt(subsetFont, opt.targetFormat);
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 }
303 } finally {265 } finally {
304 harfbuzzJsWasm.hb_face_destroy(subset);266 harfbuzzJsWasm.hb_blob_destroy(result);
305 }267 }
306 } finally {268 } finally {
307 harfbuzzJsWasm.hb_subset_input_destroy(input);269 harfbuzzJsWasm.hb_face_destroy(subset);
308 harfbuzzJsWasm.hb_face_destroy(face);
309 harfbuzzJsWasm.free(fontBuffer);
310 }270 }
311 });271 } finally {
272 harfbuzzJsWasm.hb_subset_input_destroy(input);
273 harfbuzzJsWasm.hb_face_destroy(face);
274 harfbuzzJsWasm.free(fontBuffer);
275 }
312}276}
313277
314type VariationAxisRange = { min: number; max: number; default?: number };278type VariationAxisRange = { min: number; max: number; default?: number };
...@@ -367,25 +331,20 @@ const loadAndInitializeHarfbuzz = async.once(async () => {...@@ -367,25 +331,20 @@ const loadAndInitializeHarfbuzz = async.once(async () => {
367 };331 };
368});332});
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
377function hbTag(str: string) {334function hbTag(str: string) {
378 return str.split("").reduce((a, ch) => (a << 8) + ch.charCodeAt(0), 0);335 return str.split("").reduce((a, ch) => (a << 8) + ch.charCodeAt(0), 0);
379}336}
380337
381import * as sg from "#sitegen";338import * as sg from "#sitegen";
382import * as fs from "#sitegen/fs";339import * as fs from "#sitegen/fs";
340import { ASSERT } from "@clo/lib/assert";
383import * as async from "@clo/lib/async";341import * as async from "@clo/lib/async";
384import * as error from "@clo/lib/error";342import * as error from "@clo/lib/error";
385import * as progress from "@clo/lib/progress";343import * as progress from "@clo/lib/progress";
386import * as crypto from "node:crypto";344import * as queue from "@clo/lib/queue";
387import { readFile } from "node:fs/promises";345import { readFile } from "node:fs/promises";
388import * as os from "node:os";346import * as os from "node:os";
389import * as path from "node:path";347import * as path from "node:path";
390import * as wawoff2 from "wawoff2";348import * as wawoff2 from "wawoff2";
349import { ttf2eot } from "./font/ttf2eot.ts";
391import * as incr from "./incremental.ts";350import * 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 {...@@ -44,7 +44,7 @@ export interface Font {
44 vars?: Record<string, FontVariableAxis>;44 vars?: Record<string, FontVariableAxis>;
45 layoutFeatures?: string[];45 layoutFeatures?: string[];
46 unicodes: FontRange | FontRange[];46 unicodes: FontRange | FontRange[];
47 /** Include [ext] to autofill 'woff2' */47 /** example: `/myfont.[ext]` */
48 asset: string;48 asset: string;
49 }>;49 }>;
50}50}
lib/async.ts+31
...@@ -261,5 +261,36 @@ export function delay(ms: number): Cancelable<void> {...@@ -261,5 +261,36 @@ export function delay(ms: number): Cancelable<void> {
261 );261 );
262}262}
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
264import { ASSERT } from "./assert.ts";295import { ASSERT } from "./assert.ts";
265import * as ts from "./ts.ts";296import * as ts from "./ts.ts";
lib/mime.ts+2-2
...@@ -1,6 +1,5 @@...@@ -1,6 +1,5 @@
1/**1/**
2 * a small mime type lookup table library.2 * a small mime type library.
3 *
4 * @module3 * @module
5 */4 */
65
...@@ -56,6 +55,7 @@ const extensions = new Map(...@@ -56,6 +55,7 @@ const extensions = new Map(
56 + ".dmg" + "#" + "application/octet-stream" + "\n"55 + ".dmg" + "#" + "application/octet-stream" + "\n"
57 + ".doc" + "#" + "application/msword" + "\n"56 + ".doc" + "#" + "application/msword" + "\n"
58 + ".docx" + "#" + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + "\n"57 + ".docx" + "#" + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + "\n"
58 + ".eot" + "#" + "application/vnd.ms-fontobject" + "\n"
59 + ".epub" + "#" + "application/epub+zip" + "\n"59 + ".epub" + "#" + "application/epub+zip" + "\n"
60 + ".exe" + "#" + "application/x-msdownload" + "\n"60 + ".exe" + "#" + "application/x-msdownload" + "\n"
61 + ".flv" + "#" + "video/x-flv" + "\n"61 + ".flv" + "#" + "video/x-flv" + "\n"
lib/readme.changes.md+7
...@@ -1,5 +1,12 @@...@@ -1,5 +1,12 @@
1# notable changes in clover's typescript library1# 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
3## v410## v4
411
5### breaking12### breaking
package-lock.json+88-78
...@@ -9,6 +9,7 @@...@@ -9,6 +9,7 @@
9 "@clo/markodown": "npm:@paperclover/markodown@1.0.0",9 "@clo/markodown": "npm:@paperclover/markodown@1.0.0",
10 "@marko/compiler": "5.39.62",10 "@marko/compiler": "5.39.62",
11 "@types/node": "24.x",11 "@types/node": "24.x",
12 "@types/wawoff2": "^1.0.2",
12 "dashjs": "5.0.3",13 "dashjs": "5.0.3",
13 "dprint": "0.53.1",14 "dprint": "0.53.1",
14 "esbuild": "0.25.5",15 "esbuild": "0.25.5",
...@@ -571,9 +572,9 @@...@@ -571,9 +572,9 @@
571 }572 }
572 },573 },
573 "node_modules/@esbuild/openharmony-arm64": {574 "node_modules/@esbuild/openharmony-arm64": {
574 "version": "0.27.4",575 "version": "0.27.7",
575 "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",576 "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
576 "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",577 "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
577 "cpu": [578 "cpu": [
578 "arm64"579 "arm64"
579 ],580 ],
...@@ -1116,6 +1117,15 @@...@@ -1116,6 +1117,15 @@
1116 "undici-types": "~7.16.0"1117 "undici-types": "~7.16.0"
1117 }1118 }
1118 },1119 },
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 },
1119 "node_modules/@vitest/expect": {1129 "node_modules/@vitest/expect": {
1120 "version": "4.1.2",1130 "version": "4.1.2",
1121 "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz",1131 "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz",
...@@ -2493,9 +2503,9 @@...@@ -2493,9 +2503,9 @@
2493 }2503 }
2494 },2504 },
2495 "node_modules/vitest/node_modules/@esbuild/aix-ppc64": {2505 "node_modules/vitest/node_modules/@esbuild/aix-ppc64": {
2496 "version": "0.27.4",2506 "version": "0.27.7",
2497 "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",2507 "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
2498 "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",2508 "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
2499 "cpu": [2509 "cpu": [
2500 "ppc64"2510 "ppc64"
2501 ],2511 ],
...@@ -2509,9 +2519,9 @@...@@ -2509,9 +2519,9 @@
2509 }2519 }
2510 },2520 },
2511 "node_modules/vitest/node_modules/@esbuild/android-arm": {2521 "node_modules/vitest/node_modules/@esbuild/android-arm": {
2512 "version": "0.27.4",2522 "version": "0.27.7",
2513 "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",2523 "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
2514 "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",2524 "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
2515 "cpu": [2525 "cpu": [
2516 "arm"2526 "arm"
2517 ],2527 ],
...@@ -2525,9 +2535,9 @@...@@ -2525,9 +2535,9 @@
2525 }2535 }
2526 },2536 },
2527 "node_modules/vitest/node_modules/@esbuild/android-arm64": {2537 "node_modules/vitest/node_modules/@esbuild/android-arm64": {
2528 "version": "0.27.4",2538 "version": "0.27.7",
2529 "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",2539 "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
2530 "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",2540 "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
2531 "cpu": [2541 "cpu": [
2532 "arm64"2542 "arm64"
2533 ],2543 ],
...@@ -2541,9 +2551,9 @@...@@ -2541,9 +2551,9 @@
2541 }2551 }
2542 },2552 },
2543 "node_modules/vitest/node_modules/@esbuild/android-x64": {2553 "node_modules/vitest/node_modules/@esbuild/android-x64": {
2544 "version": "0.27.4",2554 "version": "0.27.7",
2545 "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",2555 "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
2546 "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",2556 "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
2547 "cpu": [2557 "cpu": [
2548 "x64"2558 "x64"
2549 ],2559 ],
...@@ -2557,9 +2567,9 @@...@@ -2557,9 +2567,9 @@
2557 }2567 }
2558 },2568 },
2559 "node_modules/vitest/node_modules/@esbuild/darwin-arm64": {2569 "node_modules/vitest/node_modules/@esbuild/darwin-arm64": {
2560 "version": "0.27.4",2570 "version": "0.27.7",
2561 "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",2571 "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
2562 "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",2572 "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
2563 "cpu": [2573 "cpu": [
2564 "arm64"2574 "arm64"
2565 ],2575 ],
...@@ -2573,9 +2583,9 @@...@@ -2573,9 +2583,9 @@
2573 }2583 }
2574 },2584 },
2575 "node_modules/vitest/node_modules/@esbuild/darwin-x64": {2585 "node_modules/vitest/node_modules/@esbuild/darwin-x64": {
2576 "version": "0.27.4",2586 "version": "0.27.7",
2577 "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",2587 "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
2578 "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",2588 "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
2579 "cpu": [2589 "cpu": [
2580 "x64"2590 "x64"
2581 ],2591 ],
...@@ -2589,9 +2599,9 @@...@@ -2589,9 +2599,9 @@
2589 }2599 }
2590 },2600 },
2591 "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": {2601 "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": {
2592 "version": "0.27.4",2602 "version": "0.27.7",
2593 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",2603 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
2594 "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",2604 "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
2595 "cpu": [2605 "cpu": [
2596 "arm64"2606 "arm64"
2597 ],2607 ],
...@@ -2605,9 +2615,9 @@...@@ -2605,9 +2615,9 @@
2605 }2615 }
2606 },2616 },
2607 "node_modules/vitest/node_modules/@esbuild/freebsd-x64": {2617 "node_modules/vitest/node_modules/@esbuild/freebsd-x64": {
2608 "version": "0.27.4",2618 "version": "0.27.7",
2609 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",2619 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
2610 "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",2620 "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
2611 "cpu": [2621 "cpu": [
2612 "x64"2622 "x64"
2613 ],2623 ],
...@@ -2621,9 +2631,9 @@...@@ -2621,9 +2631,9 @@
2621 }2631 }
2622 },2632 },
2623 "node_modules/vitest/node_modules/@esbuild/linux-arm": {2633 "node_modules/vitest/node_modules/@esbuild/linux-arm": {
2624 "version": "0.27.4",2634 "version": "0.27.7",
2625 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",2635 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
2626 "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",2636 "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
2627 "cpu": [2637 "cpu": [
2628 "arm"2638 "arm"
2629 ],2639 ],
...@@ -2637,9 +2647,9 @@...@@ -2637,9 +2647,9 @@
2637 }2647 }
2638 },2648 },
2639 "node_modules/vitest/node_modules/@esbuild/linux-arm64": {2649 "node_modules/vitest/node_modules/@esbuild/linux-arm64": {
2640 "version": "0.27.4",2650 "version": "0.27.7",
2641 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",2651 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
2642 "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",2652 "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
2643 "cpu": [2653 "cpu": [
2644 "arm64"2654 "arm64"
2645 ],2655 ],
...@@ -2653,9 +2663,9 @@...@@ -2653,9 +2663,9 @@
2653 }2663 }
2654 },2664 },
2655 "node_modules/vitest/node_modules/@esbuild/linux-ia32": {2665 "node_modules/vitest/node_modules/@esbuild/linux-ia32": {
2656 "version": "0.27.4",2666 "version": "0.27.7",
2657 "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",2667 "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
2658 "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",2668 "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
2659 "cpu": [2669 "cpu": [
2660 "ia32"2670 "ia32"
2661 ],2671 ],
...@@ -2669,9 +2679,9 @@...@@ -2669,9 +2679,9 @@
2669 }2679 }
2670 },2680 },
2671 "node_modules/vitest/node_modules/@esbuild/linux-loong64": {2681 "node_modules/vitest/node_modules/@esbuild/linux-loong64": {
2672 "version": "0.27.4",2682 "version": "0.27.7",
2673 "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",2683 "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
2674 "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",2684 "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
2675 "cpu": [2685 "cpu": [
2676 "loong64"2686 "loong64"
2677 ],2687 ],
...@@ -2685,9 +2695,9 @@...@@ -2685,9 +2695,9 @@
2685 }2695 }
2686 },2696 },
2687 "node_modules/vitest/node_modules/@esbuild/linux-mips64el": {2697 "node_modules/vitest/node_modules/@esbuild/linux-mips64el": {
2688 "version": "0.27.4",2698 "version": "0.27.7",
2689 "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",2699 "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
2690 "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",2700 "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
2691 "cpu": [2701 "cpu": [
2692 "mips64el"2702 "mips64el"
2693 ],2703 ],
...@@ -2701,9 +2711,9 @@...@@ -2701,9 +2711,9 @@
2701 }2711 }
2702 },2712 },
2703 "node_modules/vitest/node_modules/@esbuild/linux-ppc64": {2713 "node_modules/vitest/node_modules/@esbuild/linux-ppc64": {
2704 "version": "0.27.4",2714 "version": "0.27.7",
2705 "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",2715 "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
2706 "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",2716 "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
2707 "cpu": [2717 "cpu": [
2708 "ppc64"2718 "ppc64"
2709 ],2719 ],
...@@ -2717,9 +2727,9 @@...@@ -2717,9 +2727,9 @@
2717 }2727 }
2718 },2728 },
2719 "node_modules/vitest/node_modules/@esbuild/linux-riscv64": {2729 "node_modules/vitest/node_modules/@esbuild/linux-riscv64": {
2720 "version": "0.27.4",2730 "version": "0.27.7",
2721 "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",2731 "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
2722 "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",2732 "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
2723 "cpu": [2733 "cpu": [
2724 "riscv64"2734 "riscv64"
2725 ],2735 ],
...@@ -2733,9 +2743,9 @@...@@ -2733,9 +2743,9 @@
2733 }2743 }
2734 },2744 },
2735 "node_modules/vitest/node_modules/@esbuild/linux-s390x": {2745 "node_modules/vitest/node_modules/@esbuild/linux-s390x": {
2736 "version": "0.27.4",2746 "version": "0.27.7",
2737 "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",2747 "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
2738 "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",2748 "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
2739 "cpu": [2749 "cpu": [
2740 "s390x"2750 "s390x"
2741 ],2751 ],
...@@ -2749,9 +2759,9 @@...@@ -2749,9 +2759,9 @@
2749 }2759 }
2750 },2760 },
2751 "node_modules/vitest/node_modules/@esbuild/linux-x64": {2761 "node_modules/vitest/node_modules/@esbuild/linux-x64": {
2752 "version": "0.27.4",2762 "version": "0.27.7",
2753 "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",2763 "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
2754 "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",2764 "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
2755 "cpu": [2765 "cpu": [
2756 "x64"2766 "x64"
2757 ],2767 ],
...@@ -2765,9 +2775,9 @@...@@ -2765,9 +2775,9 @@
2765 }2775 }
2766 },2776 },
2767 "node_modules/vitest/node_modules/@esbuild/netbsd-arm64": {2777 "node_modules/vitest/node_modules/@esbuild/netbsd-arm64": {
2768 "version": "0.27.4",2778 "version": "0.27.7",
2769 "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",2779 "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
2770 "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",2780 "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
2771 "cpu": [2781 "cpu": [
2772 "arm64"2782 "arm64"
2773 ],2783 ],
...@@ -2781,9 +2791,9 @@...@@ -2781,9 +2791,9 @@
2781 }2791 }
2782 },2792 },
2783 "node_modules/vitest/node_modules/@esbuild/netbsd-x64": {2793 "node_modules/vitest/node_modules/@esbuild/netbsd-x64": {
2784 "version": "0.27.4",2794 "version": "0.27.7",
2785 "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",2795 "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
2786 "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",2796 "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
2787 "cpu": [2797 "cpu": [
2788 "x64"2798 "x64"
2789 ],2799 ],
...@@ -2797,9 +2807,9 @@...@@ -2797,9 +2807,9 @@
2797 }2807 }
2798 },2808 },
2799 "node_modules/vitest/node_modules/@esbuild/openbsd-arm64": {2809 "node_modules/vitest/node_modules/@esbuild/openbsd-arm64": {
2800 "version": "0.27.4",2810 "version": "0.27.7",
2801 "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",2811 "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
2802 "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",2812 "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
2803 "cpu": [2813 "cpu": [
2804 "arm64"2814 "arm64"
2805 ],2815 ],
...@@ -2813,9 +2823,9 @@...@@ -2813,9 +2823,9 @@
2813 }2823 }
2814 },2824 },
2815 "node_modules/vitest/node_modules/@esbuild/openbsd-x64": {2825 "node_modules/vitest/node_modules/@esbuild/openbsd-x64": {
2816 "version": "0.27.4",2826 "version": "0.27.7",
2817 "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",2827 "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
2818 "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",2828 "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
2819 "cpu": [2829 "cpu": [
2820 "x64"2830 "x64"
2821 ],2831 ],
...@@ -2829,9 +2839,9 @@...@@ -2829,9 +2839,9 @@
2829 }2839 }
2830 },2840 },
2831 "node_modules/vitest/node_modules/@esbuild/sunos-x64": {2841 "node_modules/vitest/node_modules/@esbuild/sunos-x64": {
2832 "version": "0.27.4",2842 "version": "0.27.7",
2833 "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",2843 "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
2834 "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",2844 "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
2835 "cpu": [2845 "cpu": [
2836 "x64"2846 "x64"
2837 ],2847 ],
...@@ -2845,9 +2855,9 @@...@@ -2845,9 +2855,9 @@
2845 }2855 }
2846 },2856 },
2847 "node_modules/vitest/node_modules/@esbuild/win32-arm64": {2857 "node_modules/vitest/node_modules/@esbuild/win32-arm64": {
2848 "version": "0.27.4",2858 "version": "0.27.7",
2849 "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",2859 "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
2850 "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",2860 "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
2851 "cpu": [2861 "cpu": [
2852 "arm64"2862 "arm64"
2853 ],2863 ],
...@@ -2861,9 +2871,9 @@...@@ -2861,9 +2871,9 @@
2861 }2871 }
2862 },2872 },
2863 "node_modules/vitest/node_modules/@esbuild/win32-ia32": {2873 "node_modules/vitest/node_modules/@esbuild/win32-ia32": {
2864 "version": "0.27.4",2874 "version": "0.27.7",
2865 "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",2875 "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
2866 "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",2876 "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
2867 "cpu": [2877 "cpu": [
2868 "ia32"2878 "ia32"
2869 ],2879 ],
...@@ -2877,9 +2887,9 @@...@@ -2877,9 +2887,9 @@
2877 }2887 }
2878 },2888 },
2879 "node_modules/vitest/node_modules/@esbuild/win32-x64": {2889 "node_modules/vitest/node_modules/@esbuild/win32-x64": {
2880 "version": "0.27.4",2890 "version": "0.27.7",
2881 "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",2891 "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
2882 "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",2892 "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
2883 "cpu": [2893 "cpu": [
2884 "x64"2894 "x64"
2885 ],2895 ],
package.json+1
...@@ -5,6 +5,7 @@...@@ -5,6 +5,7 @@
5 "@clo/markodown": "npm:@paperclover/markodown@1.0.0",5 "@clo/markodown": "npm:@paperclover/markodown@1.0.0",
6 "@marko/compiler": "5.39.62",6 "@marko/compiler": "5.39.62",
7 "@types/node": "24.x",7 "@types/node": "24.x",
8 "@types/wawoff2": "^1.0.2",
8 "dashjs": "5.0.3",9 "dashjs": "5.0.3",
9 "dprint": "0.53.1",10 "dprint": "0.53.1",
10 "esbuild": "0.25.5",11 "esbuild": "0.25.5",
src/global.css+20-4
...@@ -1,6 +1,10 @@...@@ -1,6 +1,10 @@
1@font-face {1@font-face {
2 font-family: "recu";2 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");
4 font-weight: 400 750;8 font-weight: 400 750;
5 font-style: normal;9 font-style: normal;
6 font-display: swap;10 font-display: swap;
...@@ -10,7 +14,11 @@...@@ -10,7 +14,11 @@
10}14}
11@font-face {15@font-face {
12 font-family: "rmo";16 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");
14 font-weight: 400 800;22 font-weight: 400 800;
15 font-style: normal;23 font-style: normal;
16 font-display: swap;24 font-display: swap;
...@@ -20,7 +28,11 @@...@@ -20,7 +28,11 @@
20}28}
21@font-face {29@font-face {
22 font-family: "rmo";30 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");
24 font-weight: 400 800;36 font-weight: 400 800;
25 font-style: normal;37 font-style: normal;
26 font-display: swap;38 font-display: swap;
...@@ -42,7 +54,11 @@...@@ -42,7 +54,11 @@
42}54}
43@font-face {55@font-face {
44 font-family: "recu";56 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");
46 font-weight: 350;62 font-weight: 350;
47 font-style: normal;63 font-style: normal;
48 font-display: swap;64 font-display: swap;
src/q+a/questions.css+10-2
...@@ -1,6 +1,10 @@...@@ -1,6 +1,10 @@
1@font-face {1@font-face {
2 font-family: "recu";2 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");
4 font-weight: 400 800;8 font-weight: 400 800;
5 font-style: normal;9 font-style: normal;
6 font-display: swap;10 font-display: swap;
...@@ -10,7 +14,11 @@...@@ -10,7 +14,11 @@
10}14}
11@font-face {15@font-face {
12 font-family: recu;16 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");
14 font-weight: 400 800;22 font-weight: 400 800;
15 font-style: normal;23 font-style: normal;
16 font-display: swap;24 font-display: swap;