// @ts-check import { GifNodeType } from "./constants.js"; import crel, { fragment } from "../common/crel.js"; import getOwn from "../common/getOwn.js"; import { areBytesEqual } from "../common/bytes.js"; import pluralize from "../common/pluralize.js"; /** @typedef {import("../../types/gif.d.ts").GifNode} GifNode */ const textEncoder = new TextEncoder(); const VERSION_87 = textEncoder.encode("87a"); const VERSION_89 = textEncoder.encode("89a"); /** * @param {(string | Node)[]} children */ const p = (...children) => crel("p", {}, ...children); /** * @param {Uint8Array} bytes * @returns {number} */ const readGifUint = (bytes) => { if (bytes.length !== 2) throw new Error("Expected 2 bytes"); return (bytes[1] << 8) | bytes[0]; }; /** * @typedef {object} NodeUi * @prop {string} title * @prop {HTMLElement | DocumentFragment} description */ /** @type {Record NodeUi>} */ const NODE_UI_FNS = { [GifNodeType.root]: () => ({ title: "GIF file", description: fragment(), }), // Header [GifNodeType.header]: () => ({ title: "GIF header", description: fragment( p('GIFs start with a 6-byte header. This is typically the string "GIF87a" or "GIF89a", encoded as ASCII.'), ), }), [GifNodeType.headerSignature]: () => ({ title: "GIF signature", description: p('GIFs start with the string "GIF", encoded as ASCII.'), }), [GifNodeType.headerVersion]: ({ bytes }) => { /** @type {string} */ let end; if (areBytesEqual(bytes, VERSION_87)) { end = "87a"; } else if (areBytesEqual(bytes, VERSION_89)) { end = "89a"; } else { end = "something unexpected! This might not be a valid GIF file"; } return { title: "GIF version", description: p( `The version of the GIF format. This is typically "87a" or "89a". In this case, the version is ${end}.`, ), }; }, // Logical screen descriptor [GifNodeType.logicalScreenDescriptor]: () => ({ title: "Logical Screen Descriptor", description: p( "The logical screen descriptor contains information about the overall GIF, such as its dimensions.", ), }), [GifNodeType.logicalScreenWidth]: ({ bytes }) => ({ title: "Logical Screen Width", description: p( `The width of the image stored as a 16-bit little endian unsigned integer. In this case, the width is ${ pluralize(readGifUint(bytes), "byte") }.`, ), }), [GifNodeType.logicalScreenHeight]: ({ bytes }) => ({ title: "Logical Screen Height", description: p( `The height of the image stored as a 16-bit little endian unsigned integer. In this case, the height is ${ pluralize(readGifUint(bytes), "byte") }.`, ), }), [GifNodeType.logicalScreenDescriptorPackedFields]: () => ({ title: "Logical Screen Descriptor packed fields", description: p("TODO"), }), [GifNodeType.logicalScreenBackgroundColorIndex]: () => ({ title: "Background Color Index", description: p("TODO"), }), [GifNodeType.logicalScreenPixelAspectRatio]: () => ({ title: "Pixel Aspect Ratio", description: p("TODO"), }), }; /** * @param {GifNode} node * @returns {NodeUi} */ export default (node) => { const uiFn = getOwn(NODE_UI_FNS, node.type); if (!uiFn) { throw new Error(`Found a node (${node.type}) with no matching UI function`); } return uiFn(node); };