IOweb/node_modules/satteri/dist/mdast/mdast-reader.js
2026-07-03 15:07:38 -05:00

333 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<id, string>` 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);
}
}