import { restorePhantomSpaces } from "../phantom.js"; import { readPosition } from "../wire-read.js"; import { decodeColumnAlign } from "./column-align.js"; import { NodeTypeName } from "./generated/node-types.js"; import { ARENA_MAGIC, KIND_MDAST, FIELD, HEADER } from "../generated/arena-layout.js"; export { NodeType, NodeTypeName } from "./generated/node-types.js"; export class MdastReader { #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 buffer: bad magic 0x${magic.toString(16)}, expected 0x${ARENA_MAGIC.toString(16)}`); } const kind = v.getUint32(HEADER.kind, true); if (kind !== KIND_MDAST) { throw new Error(`MdastReader was handed a buffer of kind ${kind} (expected ${KIND_MDAST}). ` + `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). Lazy-builds a `Map` on first call so materialization * of 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; } 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); } getNode(nodeId) { const { nodesOffset, nodeStructSize, nodeCount } = this.#header; if (nodeId >= nodeCount) { throw new RangeError(`Node ID ${nodeId} out of range (count: ${nodeCount})`); } const base = nodesOffset + nodeId * nodeStructSize; const v = this.#view; const type = v.getUint8(base + FIELD.node_type); const position = readPosition(v, base + FIELD.start_offset); return { id: v.getUint32(base + FIELD.id, true), type, typeName: NodeTypeName[type] ?? `Unknown(${type})`, parent: v.getUint32(base + FIELD.parent, true), position, childrenStart: v.getUint32(base + FIELD.children_start, true), childrenCount: v.getUint32(base + FIELD.children_count, true), dataOffset: v.getUint32(base + FIELD.data_offset, true), dataLen: v.getUint32(base + FIELD.data_len, true), }; } /** Fast path: read only the type byte for a node. */ getNodeType(nodeId) { const { nodesOffset, nodeStructSize } = this.#header; return this.#view.getUint8(nodesOffset + nodeId * nodeStructSize + FIELD.node_type); } /** Fast path: read only the parent id for a node (0xffffffff at the root). */ getParentId(nodeId) { const { nodesOffset, nodeStructSize } = this.#header; return this.#view.getUint32(nodesOffset + nodeId * nodeStructSize + FIELD.parent, true); } getChildIds(nodeId) { const { nodesOffset, nodeStructSize, childrenOffset } = this.#header; const base = nodesOffset + nodeId * 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 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 { nodesOffset, nodeStructSize, childrenOffset } = this.#header; const base = nodesOffset + nodeId * 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; for (let i = childrenCount - 1; i >= 0; i--) { stack.push(v.getUint32(childrenOffset + (childrenStart + i) * 4, true)); } } 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(v.buffer, v.byteOffset + this.#header.typeDataOffset + dataOffset, dataLen); } /** Read a StringRef (offset: u32 LE, len: u32 LE) from type data. */ readStringRef(typeData, byteOffset = 0) { const view = new DataView(typeData.buffer, typeData.byteOffset + byteOffset); return { offset: view.getUint32(0, true), len: view.getUint32(4, true), }; } /** * StringRef value. Valid for Text, InlineCode, Html, Yaml, Toml nodes. * These store a single StringRef as their type data. */ getTextValue(nodeId) { const data = this.getTypeData(nodeId); const ref = this.readStringRef(data); return this.getString(ref.offset, ref.len); } /** * ListData #[repr(C)]: start(0..4), ordered(4), spread(5), _pad(6..8). * Valid for List nodes. */ getListData(nodeId) { const data = this.getTypeData(nodeId); const view = new DataView(data.buffer, data.byteOffset); return { start: view.getUint32(0, true), ordered: data[4] !== 0, spread: data[5] !== 0, }; } /** * ListItemData #[repr(C)]: checked(0), spread(1). * checked: 0=unchecked, 1=checked, 2=not-a-task-item. */ getListItemData(nodeId) { const data = this.getTypeData(nodeId); const checkedByte = data[0]; return { checked: checkedByte === 2 ? null : checkedByte === 1, spread: data[1] !== 0, }; } /** * TableData #[repr(C)]: align_count(0..4), then align_count bytes. * Alignment bytes: 0=none, 1=left, 2=right, 3=center. */ getTableAlign(nodeId) { const data = this.getTypeData(nodeId); if (data.length < 4) return []; const view = new DataView(data.buffer, data.byteOffset); const count = view.getUint32(0, true); const result = []; for (let i = 0; i < count; i++) { result.push(decodeColumnAlign(data[4 + i])); } return result; } /** * MdxJsxElementData: name StringRef (0..8). len===0 means fragment. */ getMdxJsxElementName(nodeId) { const data = this.getTypeData(nodeId); const nameRef = this.readStringRef(data, 0); return nameRef.len > 0 ? this.getString(nameRef.offset, nameRef.len) : null; } /** * MDX JSX element data: name + attributes. * * 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)] * * Attribute kinds: 0=boolean, 1=literal, 2=expression, 3=spread */ getMdxJsxElementData(nodeId) { const data = this.getTypeData(nodeId); if (data.length < 16) { return { name: this.getMdxJsxElementName(nodeId), 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 0: // BooleanProp attributes.push({ type: "mdxJsxAttribute", name: this.getString(attrNameRef.offset, attrNameRef.len), value: null, }); break; case 1: // LiteralProp attributes.push({ type: "mdxJsxAttribute", name: this.getString(attrNameRef.offset, attrNameRef.len), value: this.getString(attrValueRef.offset, attrValueRef.len), }); break; case 2: // ExpressionProp attributes.push({ type: "mdxJsxAttribute", name: this.getString(attrNameRef.offset, attrNameRef.len), value: { type: "mdxJsxAttributeValueExpression", value: restorePhantomSpaces(this.getString(attrValueRef.offset, attrValueRef.len)), }, }); break; case 3: // Spread attributes.push({ type: "mdxJsxExpressionAttribute", value: restorePhantomSpaces(this.getString(attrValueRef.offset, attrValueRef.len)), }); break; } } return { name, attributes }; } /** * DirectiveData layout: * [name: StringRef(8B)][attr_count: u32(4B)][_pad: u32(4B)] = 16-byte header * then attr_count × 16 bytes: * [key: StringRef(8B)][value: StringRef(8B)] */ getDirectiveData(nodeId) { const data = this.getTypeData(nodeId); if (data.length < 16) { return { name: "", attributes: {} }; } const nameRef = this.readStringRef(data, 0); const name = this.getString(nameRef.offset, nameRef.len); 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 * 16; const keyRef = this.readStringRef(data, base); const valRef = this.readStringRef(data, base + 8); const key = this.getString(keyRef.offset, keyRef.len); const val = this.getString(valRef.offset, valRef.len); attributes[key] = val; } return { name, attributes }; } /** * Walk the tree depth-first. Return false from visitor to skip children. */ walk(visitor, rootId = 0) { const stack = [rootId]; while (stack.length > 0) { const nodeId = stack.pop(); const nodeType = this.getNodeType(nodeId); const result = visitor(nodeId, nodeType); if (result !== false) { const childIds = this.getChildIds(nodeId); for (let i = childIds.length - 1; i >= 0; i--) { stack.push(childIds[i]); } } } } /** Walk depth-first with full node objects (slower, but convenient). */ walkFull(visitor, rootId = 0) { this.walk((nodeId) => visitor(this.getNode(nodeId)), rootId); } }