From 907e9083b681531d3be2f8ba0cafc63dede6e61d Mon Sep 17 00:00:00 2001 From: clover caruso Date: Fri, 27 Mar 2026 01:18:43 -0700 Subject: [PATCH] feat: embedded opentype fonts --- framework/css.ts | 2 +- framework/font.ts | 315 +++++++++++++++++--------------------- framework/font/ttf2eot.ts | 301 ++++++++++++++++++++++++++++++++++++ framework/lib/sitegen.ts | 2 +- lib/async.ts | 31 ++++ lib/mime.ts | 4 +- lib/readme.changes.md | 7 + package-lock.json | 166 ++++++++++---------- package.json | 1 + src/global.css | 24 ++- src/q+a/questions.css | 12 +- 11 files changed, 599 insertions(+), 266 deletions(-) create mode 100644 framework/font/ttf2eot.ts diff --git a/framework/css.ts b/framework/css.ts index 217ab1a681dde88b5417f2e54c7b950080201b69..52f6eb1648f1792981d8607a2a592d7db3189f2d 100644 --- a/framework/css.ts +++ b/framework/css.ts @@ -81,7 +81,7 @@ export async function bundleCssFiles( const build = await esbuild.build({ bundle: true, entryPoints: ["$input$"], - external: ["*.woff2", "*.ttf", "*.png", "*.jpeg"], + external: ["*.woff2", "*.eot", "*.eot?#iefix", "*.ttf", "*.png", "*.jpeg"], metafile: true, minify: !dev, plugins: [ diff --git a/framework/font.ts b/framework/font.ts index d05701f0469920f4874fa8935344da1fdee819c3..7381a3684268bc704ea8c6a05bafe3216ef12282 100644 --- a/framework/font.ts +++ b/framework/font.ts @@ -10,15 +10,6 @@ export async function buildFonts(p: progress.Ref, fonts: sg.Font[]) { }); } - function getHash(input: string, args: string[]) { - return crypto - .createHash("sha256") - .update(`${input}${args.join(" ")}`) - .digest("hex") - .slice(0, 16) - .toLowerCase(); - } - function getVariationAxes(vars: sg.FontVars) { return Object.fromEntries( Object.entries(vars).map(([key, value]) => [ @@ -31,36 +22,56 @@ export async function buildFonts(p: progress.Ref, fonts: sg.Font[]) { await Promise.all(fonts.map((font) => incr.work(node, async (io, font) => { using node = io.start(font.name); - const baseFile = await fetchFont(font.name, font.sources); - const baseBuffer = await fs.readFile(baseFile); + const baseFile = await fetchFont(node, font.name, font.sources); + await using baseBuffer = async.deferred(fs.readFile(baseFile)); await Promise.all(font.subsets.map(async (subset) => { const args = getArgs(subset.vars ?? {}); - const subsetText = fontRangesToText( - Array.isArray(subset.unicodes) ? subset.unicodes : [subset.unicodes], - ); - using _ = node.start(args.join(" ")); - const woff = await subsetFontBuffer(baseBuffer, subsetText, { - targetFormat: "woff2", - variationAxes: subset.vars - ? getVariationAxes(subset.vars) - : undefined, - sourceName: baseFile, - assetName: subset.asset.replace("[ext]", "woff2"), - }); + const unicodes = Array.isArray(subset.unicodes) ? subset.unicodes : [subset.unicodes]; + const charCount = unicodes.reduce((count, range) => { + if (typeof range === "string") return count + [...range].length; + if (typeof range === "number") return count + 1; + return count + (range.end - range.start + 1); + }, 0); + const variationAxes = subset.vars ? getVariationAxes(subset.vars) : undefined; + using _ = node.start("subset: " + [...args, `chars=${charCount}`].join(" ")); + const [woff, ttf] = await Promise.all([ + subsetFontBuffer(await baseBuffer, unicodes, { + targetFormat: "woff2", + variationAxes, + sourceName: baseFile, + assetName: subset.asset.replace("[ext]", "woff2"), + }), + subsetFontBuffer(await baseBuffer, unicodes, { + targetFormat: "sfnt", + variationAxes, + sourceName: baseFile, + assetName: subset.asset.replace("[ext]", "ttf"), + }), + ]); + const eot = ttf2eot(ttf); await io.writeAsset({ pathname: subset.asset.replace("[ext]", "woff2"), buffer: woff, }); + await io.writeAsset({ + pathname: subset.asset.replace("[ext]", "ttf"), + buffer: ttf, + }); + await io.writeAsset({ + pathname: subset.asset.replace("[ext]", "eot"), + buffer: eot, + }); })); }, font) )); } -export async function fetchFont(name: string, sources: string[]) { +export async function fetchFont(ref: progress.Ref, name: string, sources: string[]) { + using _ = ref.start("fetch"); const errs = []; for (const source of sources) { - const cacheName = path.join("", name + path.extname(source)); + const cacheName = name + path.extname(source); if (fs.existsSync(cacheName)) return cacheName; if (source.startsWith("https://")) { @@ -106,11 +117,9 @@ export async function fetchFont(name: string, sources: string[]) { path.posix.join(home, ".local/share/fonts"), ]; for (const base of bases) { - const found = fs.readDirRecOptionalSync(base) - .find((file) => path.basename(file) === source); - if (found) { - return path.join(base, found); - } + const files = fs.readDirRecOptionalSync(base); + const found = files.find((file) => path.basename(file) === source); + if (found) return path.join(base, found); } errs.push( new Error( @@ -123,46 +132,20 @@ export async function fetchFont(name: string, sources: string[]) { throw new AggregateError(errs, `Failed to fetch the ${name} font`); } -function fontRangesToText(ranges: sg.FontRange[]): string { - const chars = new Set(); - ranges.forEach((range) => { - if (typeof range === "string") { - for (const char of range) { - chars.add(char.codePointAt(0) || 0); - } - } else if (typeof range === "number") { - chars.add(range); - } else { - for (let cp = range.start; cp <= range.end; cp++) chars.add(cp); - } - }); - return Array.from(chars) - .sort((a, b) => a - b) - .map((cp) => String.fromCodePoint(cp)) - .join(""); -} - function normalizeAxisTag(tag: string) { const lower = tag.toLowerCase(); return lower === "wght" || lower === "slnt" ? lower : tag; } -function detectFontFormat(buffer: Uint8Array): "sfnt" | "woff2" { +async function convertToSfnt(buffer: Uint8Array) { const signature = String.fromCharCode(...buffer.subarray(0, 4)); - if (signature === "wOF2") return "woff2"; if ( signature === "true" || signature === "OTTO" || signature === "\x00\x01\x00\x00" ) { - return "sfnt"; + return buffer; } - throw new Error(`Unsupported font format signature ${JSON.stringify(signature)}`); -} - -async function convertToSfnt(buffer: Uint8Array) { - const format = detectFontFormat(buffer); - if (format === "sfnt") return buffer; return new Uint8Array(await wawoff2.decompress(buffer)); } @@ -174,18 +157,29 @@ async function convertFromSfnt( return new Uint8Array(await wawoff2.compress(buffer)); } -function getSubsetContext(opt: { - sourceName?: string; - assetName?: string; - variationAxes?: VariationAxes; - targetFormat: "sfnt" | "woff2"; - text: string; -}) { - const fields = [ +const subsetQueue = new queue.PriorityQueue(1); +const subsetFontBuffer = subsetQueue.wrap(subsetFontBufferUnqueued); + +async function subsetFontBufferUnqueued( + originalFont: Uint8Array, + unicodes: sg.FontRange[], + opt: { + targetFormat: "sfnt" | "woff2"; + variationAxes?: VariationAxes; + sourceName?: string; + assetName?: string; + }, +) { + const charCount = unicodes.reduce((count, range) => { + if (typeof range === "string") return count + [...range].length; + if (typeof range === "number") return count + 1; + return count + (range.end - range.start + 1); + }, 0); + const ctx = [ + `${charCount} chars`, opt.sourceName ? `source=${opt.sourceName}` : "", opt.assetName ? `asset=${opt.assetName}` : "", `target=${opt.targetFormat}`, - `textLength=${opt.text.length}`, opt.variationAxes ? `axes=${ Object.entries(opt.variationAxes).map(([key, value]) => @@ -195,120 +189,90 @@ function getSubsetContext(opt: { ).join(",") }` : "", - ].filter(Boolean); - return fields.join(" "); -} - -async function subsetFontBuffer( - originalFont: Uint8Array, - text: string, - opt: { - targetFormat: "sfnt" | "woff2"; - variationAxes?: VariationAxes; - sourceName?: string; - assetName?: string; - }, -) { - return await subsetFontQueue(async () => { - const ctx = getSubsetContext({ ...opt, text }); - if (typeof text !== "string") { - throw new Error(`Subset text must be a string. ${ctx}`); - } + ].filter(Boolean).join(" "); + + const { harfbuzzJsWasm, getHeapU8 } = await loadAndInitializeHarfbuzz(); + const sfntFont = await convertToSfnt(originalFont); + + const input = harfbuzzJsWasm.hb_subset_input_create_or_fail(); + if (input === 0) throw new Error(`Failed to create HarfBuzz subset input. ${ctx}`); + + const fontBuffer = harfbuzzJsWasm.malloc(sfntFont.byteLength); + getHeapU8().set(new Uint8Array(sfntFont), fontBuffer); - const { harfbuzzJsWasm, getHeapU8 } = await loadAndInitializeHarfbuzz(); - const sfntFont = await convertToSfnt(originalFont); + const blob = harfbuzzJsWasm.hb_blob_create( + fontBuffer, + sfntFont.byteLength, + 2, + 0, + 0, + ); + const face = harfbuzzJsWasm.hb_face_create(blob, 0); + harfbuzzJsWasm.hb_blob_destroy(blob); + + try { + const layoutFeatures = harfbuzzJsWasm.hb_subset_input_set(input, 6); + ASSERT(layoutFeatures, `Failed to set layout features. ${ctx}`); + harfbuzzJsWasm.hb_set_clear(layoutFeatures); + harfbuzzJsWasm.hb_set_invert(layoutFeatures); + + const inputUnicodes = harfbuzzJsWasm.hb_subset_input_unicode_set(input); + for (const range of unicodes) { + if (typeof range === "string") { + for (const char of range) harfbuzzJsWasm.hb_set_add(inputUnicodes, char.codePointAt(0) || 0); + } else if (typeof range === "number") { + harfbuzzJsWasm.hb_set_add(inputUnicodes, range); + } else { + for (let cp = range.start; cp <= range.end; cp++) harfbuzzJsWasm.hb_set_add(inputUnicodes, cp); + } + } - const input = harfbuzzJsWasm.hb_subset_input_create_or_fail(); - if (input === 0) { - throw new Error(`Failed to create HarfBuzz subset input. ${ctx}`); + if (opt.variationAxes) { + for (const [axisName, value] of Object.entries(opt.variationAxes)) { + if (typeof value === "number") { + ASSERT(harfbuzzJsWasm.hb_subset_input_pin_axis_location( + input, + face, + hbTag(axisName), + value, + )); + } else { + ASSERT(harfbuzzJsWasm.hb_subset_input_set_axis_range( + input, + face, + hbTag(axisName), + value.min, + value.max, + value.default ?? Number.NaN, + )); + } + } } - const fontBuffer = harfbuzzJsWasm.malloc(sfntFont.byteLength); - getHeapU8().set(new Uint8Array(sfntFont), fontBuffer); - - const blob = harfbuzzJsWasm.hb_blob_create( - fontBuffer, - sfntFont.byteLength, - 2, - 0, - 0, - ); - const face = harfbuzzJsWasm.hb_face_create(blob, 0); - harfbuzzJsWasm.hb_blob_destroy(blob); + const subset = harfbuzzJsWasm.hb_subset_or_fail(face, input); + ASSERT(subset, `HarfBuzz failed to subset font. ${ctx}`); try { - const layoutFeatures = harfbuzzJsWasm.hb_subset_input_set(input, 6); - harfbuzzJsWasm.hb_set_clear(layoutFeatures); - harfbuzzJsWasm.hb_set_invert(layoutFeatures); - - const inputUnicodes = harfbuzzJsWasm.hb_subset_input_unicode_set(input); - for (const char of text) { - harfbuzzJsWasm.hb_set_add(inputUnicodes, char.codePointAt(0) || 0); - } - - if (opt.variationAxes) { - for (const [axisName, value] of Object.entries(opt.variationAxes)) { - if (typeof value === "number") { - if ( - !harfbuzzJsWasm.hb_subset_input_pin_axis_location( - input, - face, - hbTag(axisName), - value, - ) - ) { - throw new Error( - `Failed to pin variation axis ${axisName}=${value}. ${ctx}`, - ); - } - } else if ( - !harfbuzzJsWasm.hb_subset_input_set_axis_range( - input, - face, - hbTag(axisName), - value.min, - value.max, - value.default ?? Number.NaN, - ) - ) { - throw new Error( - `Failed to set variation axis range ${axisName}=[${value.min}, ${value.max}]${ - value.default == null ? "" : ` default=${value.default}` - }. ${ctx}`, - ); - } - } - } - - const subset = harfbuzzJsWasm.hb_subset_or_fail(face, input); - if (subset === 0) { - throw new Error(`HarfBuzz failed to subset font. ${ctx}`); - } - + const result = harfbuzzJsWasm.hb_face_reference_blob(subset); try { - const result = harfbuzzJsWasm.hb_face_reference_blob(subset); - try { - const offset = harfbuzzJsWasm.hb_blob_get_data(result, 0); - const subsetByteLength = harfbuzzJsWasm.hb_blob_get_length(result); - if (subsetByteLength === 0) { - throw new Error(`Subset font was empty. ${ctx}`); - } - const subsetFont = Uint8Array.from( - getHeapU8().subarray(offset, offset + subsetByteLength), - ); - return await convertFromSfnt(subsetFont, opt.targetFormat); - } finally { - harfbuzzJsWasm.hb_blob_destroy(result); - } + const offset = harfbuzzJsWasm.hb_blob_get_data(result, 0); + const subsetByteLength = harfbuzzJsWasm.hb_blob_get_length(result); + ASSERT(subsetByteLength > 0, `Subset font was empty. ${ctx}`); + const subsetFont = Uint8Array.from( + getHeapU8().subarray(offset, offset + subsetByteLength), + ); + return await convertFromSfnt(subsetFont, opt.targetFormat); } finally { - harfbuzzJsWasm.hb_face_destroy(subset); + harfbuzzJsWasm.hb_blob_destroy(result); } } finally { - harfbuzzJsWasm.hb_subset_input_destroy(input); - harfbuzzJsWasm.hb_face_destroy(face); - harfbuzzJsWasm.free(fontBuffer); + harfbuzzJsWasm.hb_face_destroy(subset); } - }); + } finally { + harfbuzzJsWasm.hb_subset_input_destroy(input); + harfbuzzJsWasm.hb_face_destroy(face); + harfbuzzJsWasm.free(fontBuffer); + } } type VariationAxisRange = { min: number; max: number; default?: number }; @@ -367,25 +331,20 @@ const loadAndInitializeHarfbuzz = async.once(async () => { }; }); -let subsetFontLock = Promise.resolve(); -function subsetFontQueue(fn: () => Promise) { - const run = subsetFontLock.then(fn, fn); - subsetFontLock = run.then(() => undefined, () => undefined); - return run; -} - function hbTag(str: string) { return str.split("").reduce((a, ch) => (a << 8) + ch.charCodeAt(0), 0); } import * as sg from "#sitegen"; import * as fs from "#sitegen/fs"; +import { ASSERT } from "@clo/lib/assert"; import * as async from "@clo/lib/async"; import * as error from "@clo/lib/error"; import * as progress from "@clo/lib/progress"; -import * as crypto from "node:crypto"; +import * as queue from "@clo/lib/queue"; import { readFile } from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; import * as wawoff2 from "wawoff2"; +import { ttf2eot } from "./font/ttf2eot.ts"; import * as incr from "./incremental.ts"; diff --git a/framework/font/ttf2eot.ts b/framework/font/ttf2eot.ts new file mode 100644 index 0000000000000000000000000000000000000000..4051d8f2cb0974605eef0813ba3083b2a2338ea6 --- /dev/null +++ b/framework/font/ttf2eot.ts @@ -0,0 +1,301 @@ +/** + * ESM port of https://github.com/fontello/ttf2eot + * @module + */ + +/** + * Offsets in EOT file structure. Refer to EOTPrefix in OpenTypeUtilities.cpp + */ +const offsets = { + length: 0, + fontLength: 4, + version: 8, + charset: 26, + magic: 34, + fontPanose: 16, + italic: 27, + weight: 28, + unicodeRange: 36, + codepageRange: 52, + checksumAdjustment: 60, +}; + +/** + * Offsets in different SFNT (TTF) structures. See OpenTypeUtilities.cpp + */ +const sfntOffsets = { + // sfntHeader: + numTables: 4, + + // TableDirectoryEntry + tableTag: 0, + tableOffset: 8, + tableLength: 12, + + // OS2Table + os2Weight: 4, + os2FontPanose: 32, + os2UnicodeRange: 42, + os2FsSelection: 62, + os2CodepageRange: 78, + + // headTable + headChecksumAdjustment: 8, + + // nameTable + nametableFormat: 0, + nametableCount: 2, + nametableStringOffset: 4, + + // nameRecord + namePlatformId: 0, + nameEncodingId: 2, + nameLanguageId: 4, + nameNameId: 6, + nameLength: 8, + nameOffset: 10, +}; + +/** + * Sizes of structures + */ +const sizeof = { + sfntTableEntry: 16, + sfntHeader: 12, + sfntNametable: 6, + sfntNametableEntry: 12, + eotPrefix: 82, +}; + +/** + * Magic numbers + */ +const magic = { + version: 0x00020001, + magic: 0x504c, + charset: 1, + english: 0x0409, +}; + +interface SfntTableEntry { + tag: string; + offset: number; + length: number; +} + +interface NameTable { + format: number; + count: number; + stringOffset: number; +} + +interface NameRecord { + platformID: number; + encodingID: number; + languageID: number; + nameID: number; + length: number; + offset: number; +} + +function dataView(bytes: Uint8Array): DataView { + return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); +} + +function readUint8(bytes: Uint8Array, offset = 0): number { + return dataView(bytes).getUint8(offset); +} + +function readUint16BE(bytes: Uint8Array, offset = 0): number { + return dataView(bytes).getUint16(offset, false); +} + +function readUint32BE(bytes: Uint8Array, offset = 0): number { + return dataView(bytes).getUint32(offset, false); +} + +function writeUint8(bytes: Uint8Array, value: number, offset: number): void { + dataView(bytes).setUint8(offset, value); +} + +function writeUint16LE(bytes: Uint8Array, value: number, offset: number): void { + dataView(bytes).setUint16(offset, value, true); +} + +function writeUint32LE(bytes: Uint8Array, value: number, offset: number): void { + dataView(bytes).setUint32(offset, value, true); +} + +function concat(parts: Uint8Array[]): Uint8Array { + let length = 0; + for (const part of parts) length += part.length; + + const out = new Uint8Array(length); + let offset = 0; + for (const part of parts) { + out.set(part, offset); + offset += part.length; + } + return out; +} + +function readTag(bytes: Uint8Array, offset: number): string { + return String.fromCharCode( + bytes[offset]!, + bytes[offset + 1]!, + bytes[offset + 2]!, + bytes[offset + 3]!, + ); +} + +/** + * Utility function to convert buffer of utf16be chars to buffer of utf16le + * chars prefixed with length and suffixed with zero + */ +function strbuf(str: Uint8Array): Uint8Array { + const arr = new Uint8Array(str.length + 4); + + writeUint16LE(arr, str.length, 0); + + for (let i = 0; i < str.length; i += 2) { + writeUint16LE(arr, readUint16BE(str, i), i + 2); + } + + writeUint16LE(arr, 0, arr.length - 2); + + return arr; +} + +/** + * Convert a TTF font -> EOT font for use in older Internet Explorer versions. + * + * NOTE: The font's FullName MUST begin with FamilyName. For example, if + * FamilyName is `fontello`, then FullName should be `fontello regular` and so on. + */ +export function ttf2eot(arr: Uint8Array): Uint8Array { + arr = arr.slice(); + + const out = new Uint8Array(sizeof.eotPrefix); + + writeUint32LE(out, arr.length, offsets.fontLength); + writeUint32LE(out, magic.version, offsets.version); + writeUint8(out, magic.charset, offsets.charset); + writeUint16LE(out, magic.magic, offsets.magic); + + let familyName: Uint8Array = new Uint8Array([0]); + let subfamilyName: Uint8Array = new Uint8Array([0]); + let fullName: Uint8Array = new Uint8Array([0]); + let versionString: Uint8Array = new Uint8Array([0]); + + let haveOs2 = false; + let haveName = false; + let haveHead = false; + + const numTables = readUint16BE(arr, sfntOffsets.numTables); + + for (let i = 0; i < numTables; ++i) { + const data = arr.subarray(sizeof.sfntHeader + i * sizeof.sfntTableEntry); + const tableEntry: SfntTableEntry = { + tag: readTag(data, sfntOffsets.tableTag), + offset: readUint32BE(data, sfntOffsets.tableOffset), + length: readUint32BE(data, sfntOffsets.tableLength), + }; + + const table = arr.subarray(tableEntry.offset, tableEntry.offset + tableEntry.length); + + if (tableEntry.tag === "OS/2") { + haveOs2 = true; + const os2Version = readUint16BE(table); + + for (let j = 0; j < 10; ++j) { + writeUint8(out, readUint8(table, sfntOffsets.os2FontPanose + j), offsets.fontPanose + j); + } + + writeUint8(out, readUint16BE(table, sfntOffsets.os2FsSelection) & 0x01, offsets.italic); + writeUint32LE(out, readUint16BE(table, sfntOffsets.os2Weight), offsets.weight); + + for (let j = 0; j < 4; ++j) { + writeUint32LE( + out, + readUint32BE(table, sfntOffsets.os2UnicodeRange + j * 4), + offsets.unicodeRange + j * 4, + ); + } + + if (os2Version >= 1) { + for (let j = 0; j < 2; ++j) { + writeUint32LE( + out, + readUint32BE(table, sfntOffsets.os2CodepageRange + j * 4), + offsets.codepageRange + j * 4, + ); + } + } + } else if (tableEntry.tag === "head") { + haveHead = true; + writeUint32LE(out, readUint32BE(table, sfntOffsets.headChecksumAdjustment), offsets.checksumAdjustment); + } else if (tableEntry.tag === "name") { + haveName = true; + + const nameTable: NameTable = { + format: readUint16BE(table, sfntOffsets.nametableFormat), + count: readUint16BE(table, sfntOffsets.nametableCount), + stringOffset: readUint16BE(table, sfntOffsets.nametableStringOffset), + }; + + for (let j = 0; j < nameTable.count; ++j) { + const nameRecordBytes = table.subarray(sizeof.sfntNametable + j * sizeof.sfntNametableEntry); + const name: NameRecord = { + platformID: readUint16BE(nameRecordBytes, sfntOffsets.namePlatformId), + encodingID: readUint16BE(nameRecordBytes, sfntOffsets.nameEncodingId), + languageID: readUint16BE(nameRecordBytes, sfntOffsets.nameLanguageId), + nameID: readUint16BE(nameRecordBytes, sfntOffsets.nameNameId), + length: readUint16BE(nameRecordBytes, sfntOffsets.nameLength), + offset: readUint16BE(nameRecordBytes, sfntOffsets.nameOffset), + }; + + if (name.platformID === 3 && name.encodingID === 1 && name.languageID === magic.english) { + const s = strbuf(table.subarray( + nameTable.stringOffset + name.offset, + nameTable.stringOffset + name.offset + name.length, + )); + + switch (name.nameID) { + case 1: + familyName = s; + break; + case 2: + subfamilyName = s; + break; + case 4: + fullName = s; + break; + case 5: + versionString = s; + break; + } + } + } + } + if (haveOs2 && haveName && haveHead) break; + } + + if (!(haveOs2 && haveName && haveHead)) { + throw new Error("Required section not found"); + } + + // Create final buffer with the the same array type as input one. + const eot = concat([ + out, + familyName, + subfamilyName, + versionString, + fullName, + new Uint8Array([0, 0]), + arr, + ]); + + writeUint32LE(eot, eot.length, offsets.length); // Calculate overall length + + return eot; +} diff --git a/framework/lib/sitegen.ts b/framework/lib/sitegen.ts index 0e092f13f7c70bf61c67f403e8dd19c30118a950..88a0d1c839f6b1b848c559725a65cfc59e6449ce 100644 --- a/framework/lib/sitegen.ts +++ b/framework/lib/sitegen.ts @@ -44,7 +44,7 @@ export interface Font { vars?: Record; layoutFeatures?: string[]; unicodes: FontRange | FontRange[]; - /** Include [ext] to autofill 'woff2' */ + /** example: `/myfont.[ext]` */ asset: string; }>; } diff --git a/lib/async.ts b/lib/async.ts index c70b76a05f791da172c4952129286d13751579f5..3de0ecc3d3aa3e20c3fb875bd528c6fcfb9ae47c 100644 --- a/lib/async.ts +++ b/lib/async.ts @@ -261,5 +261,36 @@ export function delay(ms: number): Cancelable { ); } +export interface Deferred extends AsyncDisposable, PromiseLike { +} + +/** + * Return something that can be awaited later without worry of an unhandled + * rejection. By disposing it, the promise exception will always be reported. + * + * ```ts + * // incorrect, possible unhandled rejection between a and b. + * const a = promise(); + * const b = await promise(); + * const c = await promise(b, await a); + * // correct + * await using a = async.deferred(promise()); + * const b = await promise(); + * const c = await promise(b, await a); + * ``` + */ +export function deferred(promise: Promise): Deferred { + let handled = false; + promise.catch(() => {}); // potential error is re-thrown on asyncDispose + return { + async [Symbol.asyncDispose]() { + if (!handled) await promise; + }, + then(onFufilled, onRejected) { + return promise.finally(() => (handled = true)).then(onFufilled, onRejected); + }, + }; +} + import { ASSERT } from "./assert.ts"; import * as ts from "./ts.ts"; diff --git a/lib/mime.ts b/lib/mime.ts index 36212bf4def3d9e0cdbdacb558709a322e26bce3..fe7caeda2cd466eb8f8b56c6a2aa32130eedd261 100644 --- a/lib/mime.ts +++ b/lib/mime.ts @@ -1,6 +1,5 @@ /** - * a small mime type lookup table library. - * + * a small mime type library. * @module */ @@ -56,6 +55,7 @@ const extensions = new Map( + ".dmg" + "#" + "application/octet-stream" + "\n" + ".doc" + "#" + "application/msword" + "\n" + ".docx" + "#" + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + "\n" + + ".eot" + "#" + "application/vnd.ms-fontobject" + "\n" + ".epub" + "#" + "application/epub+zip" + "\n" + ".exe" + "#" + "application/x-msdownload" + "\n" + ".flv" + "#" + "video/x-flv" + "\n" diff --git a/lib/readme.changes.md b/lib/readme.changes.md index cee72aa4178ffc95f9b849c1873d01ac476b22cc..f0ffa07e537909f385cab8a82b5ea565c8fb659c 100644 --- a/lib/readme.changes.md +++ b/lib/readme.changes.md @@ -1,5 +1,12 @@ # notable changes in clover's typescript library +## v5 + +### features + +- `mime`'s database contains `.eot` for embedded opentype fonts. +- `async.deferred` + ## v4 ### breaking diff --git a/package-lock.json b/package-lock.json index cd7e10249eaa86a9f35546a6b7a03a09e48bca35..1ccbd263d5efa344cb0a579268a0df2eff3ca4ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "@clo/markodown": "npm:@paperclover/markodown@1.0.0", "@marko/compiler": "5.39.62", "@types/node": "24.x", + "@types/wawoff2": "^1.0.2", "dashjs": "5.0.3", "dprint": "0.53.1", "esbuild": "0.25.5", @@ -571,9 +572,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "cpu": [ "arm64" ], @@ -1116,6 +1117,15 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/wawoff2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/wawoff2/-/wawoff2-1.0.2.tgz", + "integrity": "sha512-UVq4NxMuBywz5gklafg1HcENrK02trh/q+ifyvO6xktflGXRtf11GYsia3/l5nxg2edFQRlnUT6ivnk898WDiw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@vitest/expect": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", @@ -2493,9 +2503,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], @@ -2509,9 +2519,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], @@ -2525,9 +2535,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], @@ -2541,9 +2551,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], @@ -2557,9 +2567,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -2573,9 +2583,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], @@ -2589,9 +2599,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], @@ -2605,9 +2615,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], @@ -2621,9 +2631,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], @@ -2637,9 +2647,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], @@ -2653,9 +2663,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], @@ -2669,9 +2679,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], @@ -2685,9 +2695,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], @@ -2701,9 +2711,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], @@ -2717,9 +2727,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], @@ -2733,9 +2743,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], @@ -2749,9 +2759,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -2765,9 +2775,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "cpu": [ "arm64" ], @@ -2781,9 +2791,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], @@ -2797,9 +2807,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "cpu": [ "arm64" ], @@ -2813,9 +2823,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], @@ -2829,9 +2839,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], @@ -2845,9 +2855,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], @@ -2861,9 +2871,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], @@ -2877,9 +2887,9 @@ } }, "node_modules/vitest/node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index d109714a0bd994a63842216c89372ac1e114405b..a4a145bfde4af5eb984eb2c11d408f7c3ee536f0 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "@clo/markodown": "npm:@paperclover/markodown@1.0.0", "@marko/compiler": "5.39.62", "@types/node": "24.x", + "@types/wawoff2": "^1.0.2", "dashjs": "5.0.3", "dprint": "0.53.1", "esbuild": "0.25.5", diff --git a/src/global.css b/src/global.css index 820be21ef2c35f45730d4075e35eec89ba575a32..b263baf4115615ec9f832909f3e288bb87450847 100644 --- a/src/global.css +++ b/src/global.css @@ -1,6 +1,10 @@ @font-face { font-family: "recu"; - src: url("/recultramin.woff2") format("woff2"); + src: url("/recultramin.eot"); + src: + url("/recultramin.eot?#iefix") format("embedded-opentype"), + url("/recultramin.woff2") format("woff2"), + url("/recultramin.ttf") format("truetype"); font-weight: 400 750; font-style: normal; font-display: swap; @@ -10,7 +14,11 @@ } @font-face { font-family: "rmo"; - src: url("/recmono.woff2") format("woff2"); + src: url("/recmono.eot"); + src: + url("/recmono.eot?#iefix") format("embedded-opentype"), + url("/recmono.woff2") format("woff2"), + url("/recmono.ttf") format("truetype"); font-weight: 400 800; font-style: normal; font-display: swap; @@ -20,7 +28,11 @@ } @font-face { font-family: "rmo"; - src: url("/recmonorest.woff2") format("woff2"); + src: url("/recmonorest.eot"); + src: + url("/recmonorest.eot?#iefix") format("embedded-opentype"), + url("/recmonorest.woff2") format("woff2"), + url("/recmonorest.ttf") format("truetype"); font-weight: 400 800; font-style: normal; font-display: swap; @@ -42,7 +54,11 @@ } @font-face { font-family: "recu"; - src: url("/recthin.woff2") format("woff2"); + src: url("/recthin.eot"); + src: + url("/recthin.eot?#iefix") format("embedded-opentype"), + url("/recthin.woff2") format("woff2"), + url("/recthin.ttf") format("truetype"); font-weight: 350; font-style: normal; font-display: swap; diff --git a/src/q+a/questions.css b/src/q+a/questions.css index fe443e3e9aa54569c2d62d5c8fd73218292dc6fd..296dc3e07120b57fedcfb4c6f4b2d6c0a9316e7e 100644 --- a/src/q+a/questions.css +++ b/src/q+a/questions.css @@ -1,6 +1,10 @@ @font-face { font-family: "recu"; - src: url("/recqa.woff2") format("woff2"); + src: url("/recqa.eot"); + src: + url("/recqa.eot?#iefix") format("embedded-opentype"), + url("/recqa.woff2") format("woff2"), + url("/recqa.ttf") format("truetype"); font-weight: 400 800; font-style: normal; font-display: swap; @@ -10,7 +14,11 @@ } @font-face { font-family: recu; - src: url("/recexotic.woff2") format("woff2"); + src: url("/recexotic.eot"); + src: + url("/recexotic.eot?#iefix") format("embedded-opentype"), + url("/recexotic.woff2") format("woff2"), + url("/recexotic.ttf") format("truetype"); font-weight: 400 800; font-style: normal; font-display: swap; -- 2.54.0