155 lines
5.7 KiB
JavaScript
155 lines
5.7 KiB
JavaScript
/**
|
|
* Binary command buffer for efficient JS→Rust mutation serialization.
|
|
*
|
|
* Simple mutations (remove, setProperty) are encoded as compact binary commands.
|
|
* Structural mutations (insert, replace, …) carry one of two payload kinds:
|
|
* compiled op-streams (`PAYLOAD_OPSTREAM`, replayed straight into the arena —
|
|
* see op-stream.ts) for declarative content, or raw markdown/HTML strings
|
|
* (re-parsed by Rust) for the `{raw}`/`{rawHtml}` escape hatches.
|
|
*
|
|
* All multi-byte integers are little-endian to match native x86/ARM layout and
|
|
* avoid byte-swapping on the Rust side.
|
|
*/
|
|
import { ByteWriter } from "./byte-writer.js";
|
|
import { PROP_STRING, PROP_BOOL_TRUE, PROP_BOOL_FALSE, PROP_SPACE_SEP, PROP_INT, PROP_NULL, } from "./op-stream.js";
|
|
import { CMD_REMOVE, CMD_INSERT_BEFORE, CMD_INSERT_AFTER, CMD_PREPEND_CHILD, CMD_APPEND_CHILD, CMD_WRAP, CMD_REPLACE, CMD_SET_PROPERTY, CMD_SET_CHILDREN, PAYLOAD_RAW_MARKDOWN, PAYLOAD_RAW_HTML, PAYLOAD_OPSTREAM, } from "./generated/wire-constants.js";
|
|
export function classifyReturn(value) {
|
|
if (value === undefined || value === null)
|
|
return "no_change";
|
|
const v = value;
|
|
if (typeof v.raw === "string")
|
|
return "raw_markdown";
|
|
if (typeof v.rawHtml === "string")
|
|
return "raw_html";
|
|
if (typeof v.type === "string")
|
|
return "structured_node";
|
|
throw new Error("Invalid return value from visitor: must have raw, rawHtml, or type");
|
|
}
|
|
const INITIAL_SIZE = 4096;
|
|
export class CommandBuffer extends ByteWriter {
|
|
constructor() {
|
|
super(INITIAL_SIZE);
|
|
}
|
|
removeNode(nodeId) {
|
|
this.ensure(5);
|
|
this.buf[this.n++] = CMD_REMOVE;
|
|
this.writeU32(nodeId);
|
|
}
|
|
/** Unified set-property for both MDAST and HAST nodes. */
|
|
setProperty(nodeId, key, value) {
|
|
let valueType;
|
|
let str;
|
|
if (value === null || value === undefined) {
|
|
valueType = PROP_NULL;
|
|
str = "";
|
|
}
|
|
else if (value === true) {
|
|
valueType = PROP_BOOL_TRUE;
|
|
str = "";
|
|
}
|
|
else if (value === false) {
|
|
valueType = PROP_BOOL_FALSE;
|
|
str = "";
|
|
}
|
|
else if (typeof value === "number") {
|
|
valueType = PROP_INT;
|
|
str = String(value);
|
|
}
|
|
else if (Array.isArray(value)) {
|
|
valueType = PROP_SPACE_SEP;
|
|
str = value.join(" ");
|
|
}
|
|
else {
|
|
valueType = PROP_STRING;
|
|
str = String(value);
|
|
}
|
|
// 1(cmd) + 4(nodeId) + 1(valueType); name and value are length-prefixed strings
|
|
this.ensure(6);
|
|
this.buf[this.n++] = CMD_SET_PROPERTY;
|
|
this.writeU32(nodeId);
|
|
this.buf[this.n++] = valueType;
|
|
this.utf8WithU32Len(key);
|
|
this.utf8WithU32Len(str);
|
|
}
|
|
insertBefore(nodeId, newNode) {
|
|
this.writeStructuralCommand(CMD_INSERT_BEFORE, nodeId, newNode);
|
|
}
|
|
insertAfter(nodeId, newNode) {
|
|
this.writeStructuralCommand(CMD_INSERT_AFTER, nodeId, newNode);
|
|
}
|
|
prependChild(nodeId, newNode) {
|
|
this.writeStructuralCommand(CMD_PREPEND_CHILD, nodeId, newNode);
|
|
}
|
|
appendChild(nodeId, newNode) {
|
|
this.writeStructuralCommand(CMD_APPEND_CHILD, nodeId, newNode);
|
|
}
|
|
wrapNode(nodeId, parentNode) {
|
|
this.writeStructuralCommand(CMD_WRAP, nodeId, parentNode);
|
|
}
|
|
replace(nodeId, newNode) {
|
|
this.writeStructuralCommand(CMD_REPLACE, nodeId, newNode);
|
|
}
|
|
/** Header (cmd + nodeId + payloadType) followed by a length-prefixed string payload. */
|
|
writePayloadCommand(cmd, nodeId, payloadType, s) {
|
|
this.ensure(6);
|
|
this.buf[this.n++] = cmd;
|
|
this.writeU32(nodeId);
|
|
this.buf[this.n++] = payloadType;
|
|
this.utf8WithU32Len(s);
|
|
}
|
|
replaceOpstream(nodeId, ops) {
|
|
this.writeOpstreamCommand(CMD_REPLACE, nodeId, ops);
|
|
}
|
|
/** Replace a node's child list (root-wrapped `ops`) while keeping the node. */
|
|
setChildrenOpstream(nodeId, ops) {
|
|
this.writeOpstreamCommand(CMD_SET_CHILDREN, nodeId, ops);
|
|
}
|
|
insertBeforeOpstream(nodeId, ops) {
|
|
this.writeOpstreamCommand(CMD_INSERT_BEFORE, nodeId, ops);
|
|
}
|
|
insertAfterOpstream(nodeId, ops) {
|
|
this.writeOpstreamCommand(CMD_INSERT_AFTER, nodeId, ops);
|
|
}
|
|
prependChildOpstream(nodeId, ops) {
|
|
this.writeOpstreamCommand(CMD_PREPEND_CHILD, nodeId, ops);
|
|
}
|
|
appendChildOpstream(nodeId, ops) {
|
|
this.writeOpstreamCommand(CMD_APPEND_CHILD, nodeId, ops);
|
|
}
|
|
wrapNodeOpstream(nodeId, ops) {
|
|
this.writeOpstreamCommand(CMD_WRAP, nodeId, ops);
|
|
}
|
|
writeOpstreamCommand(cmd, nodeId, ops) {
|
|
this.ensure(10 + ops.length);
|
|
this.buf[this.n++] = cmd;
|
|
this.writeU32(nodeId);
|
|
this.buf[this.n++] = PAYLOAD_OPSTREAM;
|
|
this.writeU32(ops.length);
|
|
this.buf.set(ops, this.n);
|
|
this.n += ops.length;
|
|
}
|
|
/** Return a Uint8Array view of the written bytes (no copy). */
|
|
getBuffer() {
|
|
return this.take();
|
|
}
|
|
/** Reset for reuse, releasing the old buffer (handed-out views stay intact). */
|
|
reset() {
|
|
if (this.n === 0)
|
|
return;
|
|
this.release();
|
|
}
|
|
/** Raw structural content (`{raw}`/`{rawHtml}` escape hatches). Declarative
|
|
* nodes go through the `*Opstream` methods, not here. */
|
|
writeStructuralCommand(cmd, nodeId, node) {
|
|
const v = node;
|
|
if (typeof v.raw === "string") {
|
|
this.writePayloadCommand(cmd, nodeId, PAYLOAD_RAW_MARKDOWN, v.raw);
|
|
}
|
|
else if (typeof v.rawHtml === "string") {
|
|
this.writePayloadCommand(cmd, nodeId, PAYLOAD_RAW_HTML, v.rawHtml);
|
|
}
|
|
else {
|
|
throw new Error("CommandBuffer: structural content must be {raw} or {rawHtml}");
|
|
}
|
|
}
|
|
}
|