import { Type } from "@sinclair/typebox"; import { TypeCompiler } from "@sinclair/typebox/compiler"; import type { Op } from "./ops.ts"; import type { RenderOptions, RenderResult, Term } from "./term.ts"; /* ── Range helpers (match bit-packing in pack()) ──────────────────── */ const u8 = Type.Integer({ minimum: 0, maximum: 255 }); const u16 = Type.Integer({ minimum: 0, maximum: 65535 }); /* RGBA color packed as (a << 24 | r << 16 | g << 8 | b). When alpha >= 128, * bit 31 is set and JavaScript interprets the value as a negative int32. * Accept both signed and unsigned representations of the same bit pattern. */ const rgba = Type.Integer({ minimum: -0x80000000, maximum: 0xFFFFFFFF }); /* ── Sizing axis (discriminated union) ────────────────────────────── */ const Fit = Type.Object({ type: Type.Literal("fit"), min: Type.Optional(Type.Number()), max: Type.Optional(Type.Number()), }); const Grow = Type.Object({ type: Type.Literal("grow"), min: Type.Optional(Type.Number()), max: Type.Optional(Type.Number()), }); const Percent = Type.Object({ type: Type.Literal("percent"), value: Type.Number(), }); const Fixed = Type.Object({ type: Type.Literal("fixed"), value: Type.Number(), }); const SizingAxis = Type.Union([Fit, Grow, Percent, Fixed]); /* ── Sub-objects ──────────────────────────────────────────────────── */ const Padding = Type.Object({ left: Type.Optional(u8), right: Type.Optional(u8), top: Type.Optional(u8), bottom: Type.Optional(u8), }); const Layout = Type.Object({ width: Type.Optional(SizingAxis), height: Type.Optional(SizingAxis), padding: Type.Optional(Padding), gap: Type.Optional(u16), direction: Type.Optional( Type.Union([Type.Literal("ltr"), Type.Literal("ttb")]), ), alignX: Type.Optional(u8), alignY: Type.Optional(u8), }); const CornerRadius = Type.Object({ tl: Type.Optional(u8), tr: Type.Optional(u8), bl: Type.Optional(u8), br: Type.Optional(u8), }); const Border = Type.Object({ color: rgba, left: Type.Optional(u8), right: Type.Optional(u8), top: Type.Optional(u8), bottom: Type.Optional(u8), }); const Clip = Type.Object({ horizontal: Type.Optional(Type.Boolean()), vertical: Type.Optional(Type.Boolean()), }); const Floating = Type.Object({ x: Type.Optional(Type.Number()), y: Type.Optional(Type.Number()), parent: Type.Optional(Type.Integer({ minimum: 0 })), attachTo: Type.Optional(u8), attachPoints: Type.Optional(u8), zIndex: Type.Optional(u16), }); /* ── Op types (discriminated on `id`) ─────────────────────────────── */ const CloseElement = Type.Object({ id: Type.Literal(0x04) }); const OpenElement = Type.Object({ id: Type.Literal(0x02), name: Type.String(), layout: Type.Optional(Layout), bg: Type.Optional(rgba), cornerRadius: Type.Optional(CornerRadius), border: Type.Optional(Border), clip: Type.Optional(Clip), floating: Type.Optional(Floating), }); const TextOp = Type.Object({ id: Type.Literal(0x03), content: Type.String(), color: Type.Optional(rgba), fontSize: Type.Optional(u8), fontId: Type.Optional(u8), wrap: Type.Optional(u8), attrs: Type.Optional(u8), }); const Ops = Type.Array(Type.Union([OpenElement, TextOp, CloseElement])); /* ── Compiled validator ───────────────────────────────────────────── */ const compiled = TypeCompiler.Compile(Ops); export function validate(ops: unknown): ops is Op[] { return compiled.Check(ops); } export function assert(ops: unknown): asserts ops is Op[] { if (!compiled.Check(ops)) { let errors = [...compiled.Errors(ops)]; let msg = errors .slice(0, 5) .map((e) => `${e.path}: ${e.message}`) .join("\n"); throw new TypeError(`Invalid ops:\n${msg}`); } } /* ── Term wrapper ─────────────────────────────────────────────────── */ export function validated(term: Term): Term { return { render(ops: Op[], options?: RenderOptions): RenderResult { assert(ops); return term.render(ops, options); }, }; }