/** * Minimal `.tbl` writer, shared by the fixture generator and its checker. * * Tooling only: the engine reads TBL files, it never writes them. It lives here so * the fixture generator and the construction check encode a table the same way, * instead of two copies drifting apart. */ /** One entry: a string, or null for an unused index. */ export type TblEntry = string | null /** * Encode a classic `.tbl`: crc, entry count, index table, then * `u16 characterCount` + UTF-16LE strings. * * @param entries - the entries, in index order. * @returns the encoded bytes. */ export function encodeTbl(entries: readonly TblEntry[]): Uint8Array { const headerBytes = 4 const indexBytes = entries.length * 2 const blocks: { index: number; bytes: Uint8Array }[] = [] let cursor = headerBytes + indexBytes for (const [index, entry] of entries.entries()) { if (entry === null) continue // The count is in characters; an astral character is two UTF-16 units. let characters = 0 for (const character of entry) characters += character.codePointAt(0)! > 0xffff ? 2 : 1 const body = new Uint8Array(characters * 2) const view = new DataView(body.buffer) let units = 0 for (const character of entry) { const code = character.codePointAt(0)! if (code > 0xffff) { const adjusted = code - 0x10000 view.setUint16(units * 2, 0xd800 + (adjusted >> 10), true) view.setUint16((units + 1) * 2, 0xdc00 + (adjusted & 0x3ff), true) units += 2 } else { view.setUint16(units * 2, code, true) units += 1 } } const bytes = new Uint8Array([characters & 0xff, (characters >> 8) & 0xff, ...body]) blocks.push({ index, bytes }) cursor += bytes.byteLength } const out = new Uint8Array(cursor) const view = new DataView(out.buffer) view.setUint16(0, 0x1234, true) view.setUint16(2, entries.length, true) let at = headerBytes + indexBytes for (const block of blocks) { view.setUint16(headerBytes + block.index * 2, at, true) out.set(block.bytes, at) at += block.bytes.byteLength } return out }