import { restorePhantomSpaces } from "../phantom.js"; import { readPosition } from "../wire-read.js"; import { MDX_ATTR_BOOLEAN_PROP, MDX_ATTR_LITERAL_PROP, MDX_ATTR_EXPRESSION_PROP, MDX_ATTR_SPREAD, } from "../op-stream.js"; import { decodeElementProp } from "./element-props.js"; import { NAME_TO_TYPE } from "./generated/node-types.js"; import { ARENA_MAGIC, KIND_HAST, FIELD, HEADER } from "../generated/arena-layout.js"; export const HAST_ROOT = NAME_TO_TYPE.root; export const HAST_ELEMENT = NAME_TO_TYPE.element; export const HAST_TEXT = NAME_TO_TYPE.text; export const HAST_COMMENT = NAME_TO_TYPE.comment; export const HAST_DOCTYPE = NAME_TO_TYPE.doctype; export const HAST_RAW = NAME_TO_TYPE.raw; export const HAST_MDX_JSX_ELEMENT = NAME_TO_TYPE.mdxJsxFlowElement; export const HAST_MDX_JSX_TEXT_ELEMENT = NAME_TO_TYPE.mdxJsxTextElement; export const HAST_MDX_FLOW_EXPRESSION = NAME_TO_TYPE.mdxFlowExpression; export const HAST_MDX_ESM = NAME_TO_TYPE.mdxjsEsm; export const HAST_MDX_TEXT_EXPRESSION = NAME_TO_TYPE.mdxTextExpression; export class HastReader { #view; #header; #textDecoder; #stringPoolCache = null; constructor(buffer) { if (buffer instanceof Uint8Array) { this.#view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); } else { this.#view = new DataView(buffer); } this.#textDecoder = new TextDecoder("utf-8"); this.#header = this.#readHeader(); } #readHeader() { const v = this.#view; const magic = v.getUint32(HEADER.magic, true); if (magic !== ARENA_MAGIC) { throw new Error(`Invalid HAST buffer: bad magic 0x${magic.toString(16)}`); } const kind = v.getUint32(HEADER.kind, true); if (kind !== KIND_HAST) { throw new Error(`HastReader was handed a buffer of kind ${kind} (expected ${KIND_HAST}). ` + `MDAST and HAST node types overlap; reading the wrong kind decodes garbage.`); } return { nodeStructSize: v.getUint32(HEADER.node_struct_size, true), nodeCount: v.getUint32(HEADER.node_count, true), nodesOffset: v.getUint32(HEADER.nodes_offset, true), childrenCount: v.getUint32(HEADER.children_count, true), childrenOffset: v.getUint32(HEADER.children_offset, true), typeDataLen: v.getUint32(HEADER.type_data_len, true), typeDataOffset: v.getUint32(HEADER.type_data_offset, true), stringPoolLen: v.getUint32(HEADER.string_pool_len, true), stringPoolOffset: v.getUint32(HEADER.string_pool_offset, true), nodeDataCount: v.getUint32(HEADER.node_data_count, true), nodeDataOffset: v.getUint32(HEADER.node_data_offset, true), }; } #nodeDataTable = null; /** * Per-node JSON `data` blob (set via `Arena::set_node_data` on the Rust * side). Returns `null` when the node has no entry. Lazy-builds a * `Map` on first call so a materialization pass on a * data-heavy tree stays O(nodes) rather than O(nodes × entries). */ getNodeData(nodeId) { if (this.#header.nodeDataCount === 0) return null; if (this.#nodeDataTable === null) { this.#nodeDataTable = new Map(); const v = this.#view; let pos = this.#header.nodeDataOffset; for (let i = 0; i < this.#header.nodeDataCount; i++) { const id = v.getUint32(pos, true); pos += 4; const len = v.getUint32(pos, true); pos += 4; const slice = new Uint8Array(this.#view.buffer, this.#view.byteOffset + pos, len); this.#nodeDataTable.set(id, this.#textDecoder.decode(slice)); pos += len; } } return this.#nodeDataTable.get(nodeId) ?? null; } get nodeCount() { return this.#header.nodeCount; } get header() { return { ...this.#header }; } /** The full string pool (original input + interning heap). Not the document * source as written; for that, read `ctx.source` from a plugin. */ getStringPool() { if (this.#stringPoolCache === null) { const { stringPoolOffset, stringPoolLen } = this.#header; const bytes = new Uint8Array(this.#view.buffer, this.#view.byteOffset + stringPoolOffset, stringPoolLen); this.#stringPoolCache = this.#textDecoder.decode(bytes); } return this.#stringPoolCache; } /** Read a substring from the string pool by byte offset and length. */ getString(offset, len) { if (len === 0) return ""; const { stringPoolOffset } = this.#header; const bytes = new Uint8Array(this.#view.buffer, this.#view.byteOffset + stringPoolOffset + offset, len); return this.#textDecoder.decode(bytes); } /** Get position data for a node. */ getPosition(nodeId) { const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize; return readPosition(this.#view, base + FIELD.start_offset); } /** Whether the node carries a source position (line 0 + offset 0 is the * synthesized-node sentinel), without decoding the three position objects. */ hasPosition(nodeId) { const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize; const v = this.#view; return (v.getUint32(base + FIELD.start_line, true) !== 0 || v.getUint32(base + FIELD.start_offset, true) !== 0); } /** Get the node_type byte for a given node ID. */ getNodeType(nodeId) { const { nodesOffset, nodeStructSize } = this.#header; return this.#view.getUint8(nodesOffset + nodeId * nodeStructSize + FIELD.node_type); } /** Get the parent id for a given node (0xffffffff at the root). */ getParentId(nodeId) { const { nodesOffset, nodeStructSize } = this.#header; return this.#view.getUint32(nodesOffset + nodeId * nodeStructSize + FIELD.parent, true); } /** Get child node IDs for a given node. */ getChildIds(nodeId) { const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize; const v = this.#view; const childrenStart = v.getUint32(base + FIELD.children_start, true); const childrenCount = v.getUint32(base + FIELD.children_count, true); if (childrenCount === 0) return []; const { childrenOffset } = this.#header; const ids = []; for (let i = 0; i < childrenCount; i++) { ids.push(v.getUint32(childrenOffset + (childrenStart + i) * 4, true)); } return ids; } /** Push child node IDs directly onto a stack array (reverse order for depth-first). */ pushChildIds(nodeId, stack) { const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize; const v = this.#view; const childrenStart = v.getUint32(base + FIELD.children_start, true); const childrenCount = v.getUint32(base + FIELD.children_count, true); if (childrenCount === 0) return; const { childrenOffset } = this.#header; for (let i = childrenCount - 1; i >= 0; i--) { stack.push(v.getUint32(childrenOffset + (childrenStart + i) * 4, true)); } } /** Get the raw type_data bytes for a node. */ getTypeData(nodeId) { const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize; const v = this.#view; const dataOffset = v.getUint32(base + FIELD.data_offset, true); const dataLen = v.getUint32(base + FIELD.data_len, true); if (dataLen === 0) return new Uint8Array(0); return new Uint8Array(this.#view.buffer, this.#view.byteOffset + this.#header.typeDataOffset + dataOffset, dataLen); } /** Read a StringRef (offset: u32 LE, len: u32 LE) from a byte array at byteOffset. */ #readStringRef(data, byteOffset) { const view = new DataView(data.buffer, data.byteOffset + byteOffset); return { offset: view.getUint32(0, true), len: view.getUint32(4, true), }; } /** * Get element data for a HAST_ELEMENT node. * * Element type_data layout: * [tag_name: StringRef(8B)][prop_count: u32(4B)][_pad: u32(4B)] = 16-byte header * then prop_count * 20 bytes: * [name: StringRef(8B)][value_type: u8(1B)][_pad: [u8;3](3B)][value: StringRef(8B)] */ getElementData(nodeId) { const data = this.getTypeData(nodeId); if (data.length < 16) { return { tagName: "", properties: [] }; } const tagRef = this.#readStringRef(data, 0); const tagName = this.getString(tagRef.offset, tagRef.len); const view = new DataView(data.buffer, data.byteOffset + 8); const propCount = view.getUint32(0, true); const properties = []; for (let i = 0; i < propCount; i++) { const base = 16 + i * 20; const nameRef = this.#readStringRef(data, base); const name = this.getString(nameRef.offset, nameRef.len); const valueType = data[base + 8]; const valueRef = this.#readStringRef(data, base + 12); const valueStr = this.getString(valueRef.offset, valueRef.len); properties.push({ name, value: decodeElementProp(valueType, valueStr) }); } return { tagName, properties }; } /** * Get MDX JSX element data: name and attributes. * * MDX JSX element type_data layout: * [name: StringRef(8B)][attr_count: u32(4B)][_pad: u32(4B)] = 16-byte header * then attr_count * 20 bytes: * [kind: u8(1B)][_pad: [u8;3](3B)][name: StringRef(8B)][value: StringRef(8B)] */ getMdxJsxElementData(nodeId) { const data = this.getTypeData(nodeId); if (data.length < 16) { return { name: null, attributes: [] }; } const nameRef = this.#readStringRef(data, 0); const name = nameRef.len > 0 ? this.getString(nameRef.offset, nameRef.len) : null; const view = new DataView(data.buffer, data.byteOffset + 8); const attrCount = view.getUint32(0, true); const attributes = []; for (let i = 0; i < attrCount; i++) { const base = 16 + i * 20; const kind = data[base]; const attrNameRef = this.#readStringRef(data, base + 4); const attrValueRef = this.#readStringRef(data, base + 12); switch (kind) { case MDX_ATTR_BOOLEAN_PROP: attributes.push({ type: "mdxJsxAttribute", name: this.getString(attrNameRef.offset, attrNameRef.len), value: null, }); break; case MDX_ATTR_LITERAL_PROP: attributes.push({ type: "mdxJsxAttribute", name: this.getString(attrNameRef.offset, attrNameRef.len), value: this.getString(attrValueRef.offset, attrValueRef.len), }); break; case MDX_ATTR_EXPRESSION_PROP: attributes.push({ type: "mdxJsxAttribute", name: this.getString(attrNameRef.offset, attrNameRef.len), value: { type: "mdxJsxAttributeValueExpression", value: restorePhantomSpaces(this.getString(attrValueRef.offset, attrValueRef.len)), }, }); break; case MDX_ATTR_SPREAD: attributes.push({ type: "mdxJsxExpressionAttribute", value: restorePhantomSpaces(this.getString(attrValueRef.offset, attrValueRef.len)), }); break; } } return { name, attributes }; } /** * Get the string value for HAST_TEXT, HAST_COMMENT, or HAST_RAW nodes. * These store a single StringRef (8 bytes) as their type_data. */ getTextValue(nodeId) { const data = this.getTypeData(nodeId); if (data.length < 8) return ""; const ref = this.#readStringRef(data, 0); return this.getString(ref.offset, ref.len); } }