satteri 0.9.4 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +11 -1
  2. package/dist/binding.browser.d.ts +1 -1
  3. package/dist/binding.browser.js +1 -1
  4. package/dist/binding.d.ts +1 -1
  5. package/dist/binding.js +1 -1
  6. package/dist/byte-writer.d.ts +4 -0
  7. package/dist/byte-writer.js +13 -4
  8. package/dist/child-stub.d.ts +7 -3
  9. package/dist/child-stub.js +18 -7
  10. package/dist/command-buffer.d.ts +36 -50
  11. package/dist/command-buffer.js +102 -49
  12. package/dist/compile.d.ts +80 -32
  13. package/dist/compile.js +295 -70
  14. package/dist/freeze.d.ts +2 -0
  15. package/dist/freeze.js +10 -0
  16. package/dist/generated/wire-constants.d.ts +3 -2
  17. package/dist/generated/wire-constants.js +4 -2
  18. package/dist/hast/child-stub.d.ts +2 -3
  19. package/dist/hast/child-stub.js +18 -11
  20. package/dist/hast/element-props.d.ts +1 -1
  21. package/dist/hast/element-props.js +14 -6
  22. package/dist/hast/hast-materializer.d.ts +6 -2
  23. package/dist/hast/hast-materializer.js +48 -134
  24. package/dist/hast/hast-reader.d.ts +7 -13
  25. package/dist/hast/hast-reader.js +76 -21
  26. package/dist/hast/hast-visitor.d.ts +55 -5
  27. package/dist/hast/hast-visitor.js +320 -154
  28. package/dist/index.d.ts +6 -6
  29. package/dist/index.js +1 -1
  30. package/dist/lazy-child-resolver.d.ts +20 -13
  31. package/dist/lazy-child-resolver.js +66 -38
  32. package/dist/materializer-cache.d.ts +42 -0
  33. package/dist/materializer-cache.js +192 -0
  34. package/dist/mdast/child-stub.d.ts +6 -3
  35. package/dist/mdast/child-stub.js +71 -8
  36. package/dist/mdast/generated/layout.d.ts +2 -3
  37. package/dist/mdast/generated/layout.js +24 -28
  38. package/dist/mdast/generated/node-types.d.ts +4 -0
  39. package/dist/mdast/generated/node-types.js +25 -0
  40. package/dist/mdast/mdast-materializer.d.ts +12 -3
  41. package/dist/mdast/mdast-materializer.js +83 -78
  42. package/dist/mdast/mdast-reader.d.ts +15 -0
  43. package/dist/mdast/mdast-reader.js +55 -10
  44. package/dist/mdast/mdast-visitor.d.ts +79 -32
  45. package/dist/mdast/mdast-visitor.js +305 -122
  46. package/dist/op-stream.d.ts +5 -12
  47. package/dist/op-stream.js +6 -21
  48. package/dist/plugin.d.ts +36 -10
  49. package/dist/plugin.js +50 -2
  50. package/dist/string-pool.d.ts +13 -0
  51. package/dist/string-pool.js +91 -0
  52. package/dist/types.d.ts +44 -1
  53. package/dist/visitor-shared.d.ts +27 -4
  54. package/dist/visitor-shared.js +36 -10
  55. package/dist/wire-read.js +3 -1
  56. package/index.d.ts +130 -7
  57. package/index.js +89 -68
  58. package/package.json +24 -19
  59. package/dist/lazy-props.d.ts +0 -10
  60. package/dist/lazy-props.js +0 -50
  61. package/satteri_napi.wasi-browser.js +0 -83
  62. package/satteri_napi.wasi.cjs +0 -134
  63. package/wasi-worker-browser.mjs +0 -36
  64. package/wasi-worker.mjs +0 -63
package/README.md CHANGED
@@ -31,9 +31,19 @@ import { mdxToJs } from "satteri";
31
31
  const { code } = mdxToJs("# Hello\n\n<MyComponent />");
32
32
  ```
33
33
 
34
+ ### Markdown to JS
35
+
36
+ Like `mdxToJs`, but the source is plain Markdown: `{...}` expressions, JSX tags, and `import`/`export` lines are ordinary text instead of MDX syntax.
37
+
38
+ ```ts
39
+ import { markdownToJs } from "satteri";
40
+
41
+ const { code } = markdownToJs("# Hello\n\n{not an expression}");
42
+ ```
43
+
34
44
  ### With plugins
35
45
 
36
- Both functions accept `mdastPlugins` (operate on the Markdown AST) and `hastPlugins` (operate on the HTML AST). A plugin is an object with a `name` and a visitor per node type; `defineMdastPlugin` / `defineHastPlugin` add type inference.
46
+ All three functions accept `mdastPlugins` (operate on the Markdown AST) and `hastPlugins` (operate on the HTML AST). A plugin is an object with a `name` and a visitor per node type; `defineMdastPlugin` / `defineHastPlugin` add type inference.
37
47
 
38
48
  ```ts
39
49
  import { markdownToHtml, defineMdastPlugin } from "satteri";
@@ -1 +1 @@
1
- export { applyCommandsAndConvertToHastHandle, applyCommandsToHandle, applyCommandsToMdastHandle, compileHandle, compileMdx, convertMdastToHastHandle, createHastHandle, createMdastHandle, createMdxHastHandle, createMdxMdastHandle, dropHandle, getHandleSource, getMdastFrontmatter, getNodeData, mdastTextContentHandle, parseEsm, parseExpression, parseToHtml, renderHandle, serializeHandle, setNodeData, textContentHandle, walkHandle, walkMdastHandle, } from "../browser.js";
1
+ export { applyCommandsAndCompileHandle, applyCommandsAndConvertToHastHandle, applyCommandsAndRenderHandle, applyCommandsToHandle, applyCommandsToMdastHandle, applyMdastCommandsAndConvertAndCompile, applyMdastCommandsAndConvertAndRender, compileHandle, compileMdx, convertMdastToHastHandle, createHastHandle, createHastHandleFromHtml, createHastHandleWithFrontmatter, createMdastHandle, createMdxHastHandle, createMdxHastHandleWithFrontmatter, createMdxMdastHandle, dropHandle, getHandleSource, getMdastFrontmatter, getNodeData, markdownToHtmlFast, markdownToJsFast, mdastTextContentHandle, mdxToJsFast, parseEsm, parseExpression, parseToHtml, renderHandle, serializeHandle, setNodeData, textContentHandle, walkHandle, walkMdastHandle, } from "../browser.js";
@@ -1,2 +1,2 @@
1
1
  // @ts-nocheck — WASM browser binding has no type declarations
2
- export { applyCommandsAndConvertToHastHandle, applyCommandsToHandle, applyCommandsToMdastHandle, compileHandle, compileMdx, convertMdastToHastHandle, createHastHandle, createMdastHandle, createMdxHastHandle, createMdxMdastHandle, dropHandle, getHandleSource, getMdastFrontmatter, getNodeData, mdastTextContentHandle, parseEsm, parseExpression, parseToHtml, renderHandle, serializeHandle, setNodeData, textContentHandle, walkHandle, walkMdastHandle, } from "../browser.js";
2
+ export { applyCommandsAndCompileHandle, applyCommandsAndConvertToHastHandle, applyCommandsAndRenderHandle, applyCommandsToHandle, applyCommandsToMdastHandle, applyMdastCommandsAndConvertAndCompile, applyMdastCommandsAndConvertAndRender, compileHandle, compileMdx, convertMdastToHastHandle, createHastHandle, createHastHandleFromHtml, createHastHandleWithFrontmatter, createMdastHandle, createMdxHastHandle, createMdxHastHandleWithFrontmatter, createMdxMdastHandle, dropHandle, getHandleSource, getMdastFrontmatter, getNodeData, markdownToHtmlFast, markdownToJsFast, mdastTextContentHandle, mdxToJsFast, parseEsm, parseExpression, parseToHtml, renderHandle, serializeHandle, setNodeData, textContentHandle, walkHandle, walkMdastHandle, } from "../browser.js";
package/dist/binding.d.ts CHANGED
@@ -1 +1 @@
1
- export { applyCommandsAndConvertToHastHandle, applyCommandsToHandle, applyCommandsToMdastHandle, compileHandle, compileMdx, convertMdastToHastHandle, createHastHandle, createMdastHandle, createMdxHastHandle, createMdxMdastHandle, dropHandle, getHandleSource, getMdastFrontmatter, getNodeData, mdastTextContentHandle, parseEsm, parseExpression, parseToHtml, renderHandle, serializeHandle, setNodeData, textContentHandle, walkHandle, walkMdastHandle, } from "../index.js";
1
+ export { applyCommandsAndCompileHandle, applyCommandsAndConvertToHastHandle, applyCommandsAndRenderHandle, applyCommandsToHandle, applyCommandsToMdastHandle, applyMdastCommandsAndConvertAndCompile, applyMdastCommandsAndConvertAndRender, compileHandle, compileMdx, convertMdastToHastHandle, createHastHandle, createHastHandleFromHtml, createHastHandleWithFrontmatter, createMdastHandle, createMdxHastHandle, createMdxHastHandleWithFrontmatter, createMdxMdastHandle, dropHandle, getHandleSource, getMdastFrontmatter, getNodeData, markdownToHtmlFast, markdownToJsFast, mdastTextContentHandle, mdxToJsFast, parseEsm, parseExpression, parseToHtml, renderHandle, serializeHandle, setNodeData, textContentHandle, walkHandle, walkMdastHandle, } from "../index.js";
package/dist/binding.js CHANGED
@@ -1 +1 @@
1
- export { applyCommandsAndConvertToHastHandle, applyCommandsToHandle, applyCommandsToMdastHandle, compileHandle, compileMdx, convertMdastToHastHandle, createHastHandle, createMdastHandle, createMdxHastHandle, createMdxMdastHandle, dropHandle, getHandleSource, getMdastFrontmatter, getNodeData, mdastTextContentHandle, parseEsm, parseExpression, parseToHtml, renderHandle, serializeHandle, setNodeData, textContentHandle, walkHandle, walkMdastHandle, } from "../index.js";
1
+ export { applyCommandsAndCompileHandle, applyCommandsAndConvertToHastHandle, applyCommandsAndRenderHandle, applyCommandsToHandle, applyCommandsToMdastHandle, applyMdastCommandsAndConvertAndCompile, applyMdastCommandsAndConvertAndRender, compileHandle, compileMdx, convertMdastToHastHandle, createHastHandle, createHastHandleFromHtml, createHastHandleWithFrontmatter, createMdastHandle, createMdxHastHandle, createMdxHastHandleWithFrontmatter, createMdxMdastHandle, dropHandle, getHandleSource, getMdastFrontmatter, getNodeData, markdownToHtmlFast, markdownToJsFast, mdastTextContentHandle, mdxToJsFast, parseEsm, parseExpression, parseToHtml, renderHandle, serializeHandle, setNodeData, textContentHandle, walkHandle, walkMdastHandle, } from "../index.js";
@@ -16,6 +16,7 @@ export declare class ByteWriter {
16
16
  constructor(initialSize: number);
17
17
  /** Number of bytes written so far. */
18
18
  get length(): number;
19
+ get capacity(): number;
19
20
  /** Reset for reuse; the grown buffer is retained so steady state is alloc-free. */
20
21
  reset(): void;
21
22
  /** View of the bytes written so far (no copy; valid until the next write or reset). */
@@ -28,4 +29,7 @@ export declare class ByteWriter {
28
29
  protected writeU32(v: number): void;
29
30
  /** u32 byte length + UTF-8 bytes (self-ensuring). */
30
31
  protected utf8WithU32Len(s: string): void;
32
+ /** Backpatch a u32 (LE) at `pos` in the already-written region; the cursor
33
+ * is left untouched. */
34
+ protected patchU32(pos: number, v: number): void;
31
35
  }
@@ -29,6 +29,9 @@ export class ByteWriter {
29
29
  get length() {
30
30
  return this.n;
31
31
  }
32
+ get capacity() {
33
+ return this.buf.length;
34
+ }
32
35
  /** Reset for reuse; the grown buffer is retained so steady state is alloc-free. */
33
36
  reset() {
34
37
  this.n = 0;
@@ -97,10 +100,16 @@ export class ByteWriter {
97
100
  const lenPos = this.n;
98
101
  this.n += 4;
99
102
  const written = encoder.encodeInto(s, this.buf.subarray(this.n)).written;
100
- this.buf[lenPos] = written & 255;
101
- this.buf[lenPos + 1] = (written >> 8) & 255;
102
- this.buf[lenPos + 2] = (written >> 16) & 255;
103
- this.buf[lenPos + 3] = (written >>> 24) & 255;
103
+ this.patchU32(lenPos, written);
104
104
  this.n += written;
105
105
  }
106
+ /** Backpatch a u32 (LE) at `pos` in the already-written region; the cursor
107
+ * is left untouched. */
108
+ patchU32(pos, v) {
109
+ const buf = this.buf;
110
+ buf[pos] = v & 255;
111
+ buf[pos + 1] = (v >> 8) & 255;
112
+ buf[pos + 2] = (v >> 16) & 255;
113
+ buf[pos + 3] = (v >>> 24) & 255;
114
+ }
106
115
  }
@@ -5,13 +5,17 @@
5
5
  * Passthrough children (the common replaceNode shape) thus compile to one-word
6
6
  * refs without ever serializing the arena.
7
7
  */
8
+ export type StubDescriptorEntry = readonly [string, PropertyDescriptor];
8
9
  /**
9
- * Descriptor template for one node type's stub fields. Own enumerable getters
10
+ * Descriptor entries for one node type's stub fields. Own enumerable getters
10
11
  * so a spread copy carries correct values. `position`/`data` ride on every
11
12
  * stub; they may yield `undefined` where a materialized node omits the key —
12
- * accepted drift, invisible to `toEqual`.
13
+ * accepted drift, invisible to `toEqual`. Installed one `defineProperty` at a
14
+ * time: `Object.defineProperties` re-validates the whole map per call and
15
+ * costs nearly double.
13
16
  */
14
- export declare function stubDescriptors(fields: readonly string[]): PropertyDescriptorMap;
17
+ export declare function stubDescriptors(fields: readonly string[]): readonly StubDescriptorEntry[];
18
+ export declare function installStubDescriptors(host: object, entries: readonly StubDescriptorEntry[]): void;
15
19
  /** Node tags are dense small ints, so the per-child stub loop indexes flat
16
20
  * arrays instead of paying Map/dictionary lookups. */
17
21
  export declare function flatByTag<T>(table: Readonly<Record<number, T>>): readonly (T | undefined)[];
@@ -20,11 +20,12 @@ function fieldGetter(key) {
20
20
  REAL_NODES.set(this, real);
21
21
  }
22
22
  const value = real[key];
23
+ // Mirrors the deep-frozen node it forwards to; nothing redefines a stub field after this.
23
24
  Object.defineProperty(this, key, {
24
25
  value,
25
- writable: true,
26
+ writable: false,
26
27
  enumerable: true,
27
- configurable: true,
28
+ configurable: false,
28
29
  });
29
30
  return value;
30
31
  };
@@ -33,17 +34,27 @@ function fieldGetter(key) {
33
34
  return getter;
34
35
  }
35
36
  /**
36
- * Descriptor template for one node type's stub fields. Own enumerable getters
37
+ * Descriptor entries for one node type's stub fields. Own enumerable getters
37
38
  * so a spread copy carries correct values. `position`/`data` ride on every
38
39
  * stub; they may yield `undefined` where a materialized node omits the key —
39
- * accepted drift, invisible to `toEqual`.
40
+ * accepted drift, invisible to `toEqual`. Installed one `defineProperty` at a
41
+ * time: `Object.defineProperties` re-validates the whole map per call and
42
+ * costs nearly double.
40
43
  */
41
44
  export function stubDescriptors(fields) {
42
- const map = {};
45
+ const entries = [];
43
46
  for (const key of [...fields, "position", "data"]) {
44
- map[key] = { get: fieldGetter(key), enumerable: true, configurable: true };
47
+ entries.push([key, { get: fieldGetter(key), enumerable: true, configurable: true }]);
48
+ }
49
+ return entries;
50
+ }
51
+ export function installStubDescriptors(host, entries) {
52
+ for (let i = 0; i < entries.length; i++) {
53
+ const e = entries[i];
54
+ if (e === undefined)
55
+ continue;
56
+ Object.defineProperty(host, e[0], e[1]);
45
57
  }
46
- return map;
47
58
  }
48
59
  /** Node tags are dense small ints, so the per-child stub loop indexes flat
49
60
  * arrays instead of paying Map/dictionary lookups. */
@@ -10,65 +10,51 @@
10
10
  * All multi-byte integers are little-endian to match native x86/ARM layout and
11
11
  * avoid byte-swapping on the Rust side.
12
12
  */
13
- import { ByteWriter } from "./byte-writer.js";
13
+ import { OpWriter } from "./op-stream.js";
14
14
  import type { MdastNode } from "./types.js";
15
15
  type ReturnClass = "no_change" | "raw_markdown" | "raw_html" | "structured_node";
16
+ /** Input to the structural mutators (`replace`, `insertBefore`, …). */
17
+ type StructuralContent = MdastNode | {
18
+ raw: string;
19
+ mdxExpressions?: boolean;
20
+ } | {
21
+ rawHtml: string;
22
+ };
16
23
  export declare function classifyReturn(value: unknown): ReturnClass;
17
- /** Structural commands that carry a subtree payload. Each has an `${op}Opstream`
18
- * twin (declarative content) and a raw twin (`{raw}`/`{rawHtml}` escape hatch). */
24
+ /** Backings above this are dropped on release so one huge compile does not retain megabytes forever. */
25
+ export declare const COMMAND_BUFFER_RETAIN_MAX: number;
26
+ export declare function acquireCommandBuffer(): CommandBuffer;
27
+ export declare function releaseCommandBuffer(buf: CommandBuffer): void;
28
+ /** Structural commands that carry a subtree payload emitted in place via `emitOpstreamCommand`. */
19
29
  export type StructuralOp = "replace" | "insertBefore" | "insertAfter" | "prependChild" | "appendChild" | "wrapNode";
20
- export declare class CommandBuffer extends ByteWriter {
30
+ export declare const STRUCTURAL_CMD: Record<StructuralOp, number>;
31
+ export declare class CommandBuffer extends OpWriter {
32
+ #private;
21
33
  constructor();
34
+ reset(): void;
35
+ /** Emit a structural command whose opstream payload is written by `emit`
36
+ * via the inherited op methods. If `emit` returns false or throws, the
37
+ * buffer is rolled back to the command start so the Rust decoder never
38
+ * sees a half-written command. Returns `emit`'s verdict. */
39
+ emitOpstreamCommand(cmd: number, nodeId: number, emit: () => boolean): boolean;
22
40
  removeNode(nodeId: number): void;
23
- /** Unified set-property for both MDAST and HAST nodes. */
41
+ /** Unified set-property for both MDAST and HAST nodes.
42
+ *
43
+ * Hot path: uses `encodeInto` to write UTF-8 straight into the buffer (no
44
+ * per-call `Uint8Array`), reserving the worst-case length up front and
45
+ * backfilling the length prefix once the byte count is known. */
24
46
  setProperty(nodeId: number, key: string, value: unknown): void;
25
- insertBefore(nodeId: number, newNode: MdastNode | {
26
- raw: string;
27
- } | {
28
- rawHtml: string;
29
- }): void;
30
- insertAfter(nodeId: number, newNode: MdastNode | {
31
- raw: string;
32
- } | {
33
- rawHtml: string;
34
- }): void;
35
- prependChild(nodeId: number, newNode: MdastNode | {
36
- raw: string;
37
- } | {
38
- rawHtml: string;
39
- }): void;
40
- appendChild(nodeId: number, newNode: MdastNode | {
41
- raw: string;
42
- } | {
43
- rawHtml: string;
44
- }): void;
45
- wrapNode(nodeId: number, parentNode: MdastNode | {
46
- raw: string;
47
- } | {
48
- rawHtml: string;
49
- }): void;
50
- replace(nodeId: number, newNode: MdastNode | {
51
- raw: string;
52
- } | {
53
- rawHtml: string;
54
- }): void;
55
- /** Header (cmd + nodeId + payloadType) followed by a length-prefixed string payload. */
56
- private writePayloadCommand;
57
- replaceOpstream(nodeId: number, ops: Uint8Array): void;
58
- /** Replace a node's child list (root-wrapped `ops`) while keeping the node. */
59
- setChildrenOpstream(nodeId: number, ops: Uint8Array): void;
60
- insertBeforeOpstream(nodeId: number, ops: Uint8Array): void;
61
- insertAfterOpstream(nodeId: number, ops: Uint8Array): void;
62
- prependChildOpstream(nodeId: number, ops: Uint8Array): void;
63
- appendChildOpstream(nodeId: number, ops: Uint8Array): void;
64
- wrapNodeOpstream(nodeId: number, ops: Uint8Array): void;
65
- private writeOpstreamCommand;
47
+ insertBefore(nodeId: number, newNode: StructuralContent): void;
48
+ insertAfter(nodeId: number, newNode: StructuralContent): void;
49
+ prependChild(nodeId: number, newNode: StructuralContent): void;
50
+ appendChild(nodeId: number, newNode: StructuralContent): void;
51
+ wrapNode(nodeId: number, parentNode: StructuralContent): void;
52
+ replace(nodeId: number, newNode: StructuralContent): void;
53
+ /** Header (cmd + nodeId + PAYLOAD_RAW + flags) followed by a length-prefixed string. */
54
+ private writeRawCommand;
66
55
  /** Return a Uint8Array view of the written bytes (no copy). */
67
56
  getBuffer(): Uint8Array;
68
- /** Reset for reuse, releasing the old buffer (handed-out views stay intact). */
69
- reset(): void;
70
- /** Raw structural content (`{raw}`/`{rawHtml}` escape hatches). Declarative
71
- * nodes go through the `*Opstream` methods, not here. */
57
+ /** Encode the raw-string escape hatch; declarative nodes use `*Opstream` instead. */
72
58
  private writeStructuralCommand;
73
59
  }
74
60
  export {};
@@ -10,9 +10,9 @@
10
10
  * All multi-byte integers are little-endian to match native x86/ARM layout and
11
11
  * avoid byte-swapping on the Rust side.
12
12
  */
13
- import { ByteWriter } from "./byte-writer.js";
13
+ import { OpWriter } from "./op-stream.js";
14
14
  import { PROP_STRING, PROP_BOOL_TRUE, PROP_BOOL_FALSE, PROP_SPACE_SEP, PROP_INT, PROP_NULL, } from "./op-stream.js";
15
- 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";
15
+ 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, RAW_LITERAL_BRACES, PAYLOAD_OPSTREAM, } from "./generated/wire-constants.js";
16
16
  export function classifyReturn(value) {
17
17
  if (value === undefined || value === null)
18
18
  return "no_change";
@@ -26,17 +26,105 @@ export function classifyReturn(value) {
26
26
  throw new Error("Invalid return value from visitor: must have raw, rawHtml, or type");
27
27
  }
28
28
  const INITIAL_SIZE = 4096;
29
- export class CommandBuffer extends ByteWriter {
29
+ /** Free list recycling `CommandBuffer` instances (and their grown backings)
30
+ * across plugin passes. Safe to retain the backing because `mergeAndReset`
31
+ * copies the bytes out before a buffer is released, so no view outlives a
32
+ * pass. Cap bounds the pool for processes that briefly burst high. */
33
+ const COMMAND_BUFFER_POOL_MAX = 8;
34
+ /** Backings above this are dropped on release so one huge compile does not retain megabytes forever. */
35
+ export const COMMAND_BUFFER_RETAIN_MAX = 1 << 20;
36
+ const commandBufferPool = [];
37
+ export function acquireCommandBuffer() {
38
+ return commandBufferPool.pop() ?? new CommandBuffer();
39
+ }
40
+ export function releaseCommandBuffer(buf) {
41
+ if (commandBufferPool.length >= COMMAND_BUFFER_POOL_MAX)
42
+ return;
43
+ if (buf.capacity > COMMAND_BUFFER_RETAIN_MAX)
44
+ return;
45
+ buf.reset();
46
+ commandBufferPool.push(buf);
47
+ }
48
+ export const STRUCTURAL_CMD = {
49
+ replace: CMD_REPLACE,
50
+ insertBefore: CMD_INSERT_BEFORE,
51
+ insertAfter: CMD_INSERT_AFTER,
52
+ prependChild: CMD_PREPEND_CHILD,
53
+ appendChild: CMD_APPEND_CHILD,
54
+ wrapNode: CMD_WRAP,
55
+ };
56
+ export class CommandBuffer extends OpWriter {
57
+ /** Set while a structural payload is being emitted in place; command
58
+ * methods must not interleave bytes into it. */
59
+ #inOpstream = false;
30
60
  constructor() {
31
61
  super(INITIAL_SIZE);
32
62
  }
63
+ reset() {
64
+ this.#inOpstream = false;
65
+ super.reset();
66
+ }
67
+ #assertNotEncoding() {
68
+ if (this.#inOpstream) {
69
+ throw new Error("reentrant command emission: a node getter or toJSON invoked a context mutation while a structural payload was being encoded");
70
+ }
71
+ }
72
+ /** Emit a structural command whose opstream payload is written by `emit`
73
+ * via the inherited op methods. If `emit` returns false or throws, the
74
+ * buffer is rolled back to the command start so the Rust decoder never
75
+ * sees a half-written command. Returns `emit`'s verdict. */
76
+ emitOpstreamCommand(cmd, nodeId, emit) {
77
+ const commandStart = this.n;
78
+ const lenPos = this.#beginOpstream(cmd, nodeId);
79
+ // ok starts false so a throwing emit still hits the abort in finally
80
+ let ok = false;
81
+ try {
82
+ ok = emit();
83
+ }
84
+ finally {
85
+ if (!ok)
86
+ this.#abortOpstream(commandStart);
87
+ }
88
+ if (ok)
89
+ this.#endOpstream(lenPos);
90
+ return ok;
91
+ }
92
+ /** Open a structural command whose opstream payload is emitted in place via
93
+ * the inherited op methods; returns the backpatch position for
94
+ * `#endOpstream`. Pair with `#abortOpstream` on failure. */
95
+ #beginOpstream(cmd, nodeId) {
96
+ this.#assertNotEncoding();
97
+ this.#inOpstream = true;
98
+ this.ensure(10);
99
+ this.buf[this.n++] = cmd;
100
+ this.writeU32(nodeId);
101
+ this.buf[this.n++] = PAYLOAD_OPSTREAM;
102
+ const lenPos = this.n;
103
+ this.n += 4;
104
+ return lenPos;
105
+ }
106
+ #endOpstream(lenPos) {
107
+ this.#inOpstream = false;
108
+ this.patchU32(lenPos, this.n - (lenPos + 4));
109
+ }
110
+ /** Roll back an in-progress opstream command (unencodable content). */
111
+ #abortOpstream(commandStart) {
112
+ this.#inOpstream = false;
113
+ this.n = commandStart;
114
+ }
33
115
  removeNode(nodeId) {
116
+ this.#assertNotEncoding();
34
117
  this.ensure(5);
35
118
  this.buf[this.n++] = CMD_REMOVE;
36
119
  this.writeU32(nodeId);
37
120
  }
38
- /** Unified set-property for both MDAST and HAST nodes. */
121
+ /** Unified set-property for both MDAST and HAST nodes.
122
+ *
123
+ * Hot path: uses `encodeInto` to write UTF-8 straight into the buffer (no
124
+ * per-call `Uint8Array`), reserving the worst-case length up front and
125
+ * backfilling the length prefix once the byte count is known. */
39
126
  setProperty(nodeId, key, value) {
127
+ this.#assertNotEncoding();
40
128
  let valueType;
41
129
  let str;
42
130
  if (value === null || value === undefined) {
@@ -89,64 +177,29 @@ export class CommandBuffer extends ByteWriter {
89
177
  replace(nodeId, newNode) {
90
178
  this.writeStructuralCommand(CMD_REPLACE, nodeId, newNode);
91
179
  }
92
- /** Header (cmd + nodeId + payloadType) followed by a length-prefixed string payload. */
93
- writePayloadCommand(cmd, nodeId, payloadType, s) {
94
- this.ensure(6);
180
+ /** Header (cmd + nodeId + PAYLOAD_RAW + flags) followed by a length-prefixed string. */
181
+ writeRawCommand(cmd, nodeId, flags, s) {
182
+ this.#assertNotEncoding();
183
+ this.ensure(7);
95
184
  this.buf[this.n++] = cmd;
96
185
  this.writeU32(nodeId);
97
- this.buf[this.n++] = payloadType;
186
+ this.buf[this.n++] = PAYLOAD_RAW;
187
+ this.buf[this.n++] = flags;
98
188
  this.utf8WithU32Len(s);
99
189
  }
100
- replaceOpstream(nodeId, ops) {
101
- this.writeOpstreamCommand(CMD_REPLACE, nodeId, ops);
102
- }
103
- /** Replace a node's child list (root-wrapped `ops`) while keeping the node. */
104
- setChildrenOpstream(nodeId, ops) {
105
- this.writeOpstreamCommand(CMD_SET_CHILDREN, nodeId, ops);
106
- }
107
- insertBeforeOpstream(nodeId, ops) {
108
- this.writeOpstreamCommand(CMD_INSERT_BEFORE, nodeId, ops);
109
- }
110
- insertAfterOpstream(nodeId, ops) {
111
- this.writeOpstreamCommand(CMD_INSERT_AFTER, nodeId, ops);
112
- }
113
- prependChildOpstream(nodeId, ops) {
114
- this.writeOpstreamCommand(CMD_PREPEND_CHILD, nodeId, ops);
115
- }
116
- appendChildOpstream(nodeId, ops) {
117
- this.writeOpstreamCommand(CMD_APPEND_CHILD, nodeId, ops);
118
- }
119
- wrapNodeOpstream(nodeId, ops) {
120
- this.writeOpstreamCommand(CMD_WRAP, nodeId, ops);
121
- }
122
- writeOpstreamCommand(cmd, nodeId, ops) {
123
- this.ensure(10 + ops.length);
124
- this.buf[this.n++] = cmd;
125
- this.writeU32(nodeId);
126
- this.buf[this.n++] = PAYLOAD_OPSTREAM;
127
- this.writeU32(ops.length);
128
- this.buf.set(ops, this.n);
129
- this.n += ops.length;
130
- }
131
190
  /** Return a Uint8Array view of the written bytes (no copy). */
132
191
  getBuffer() {
133
192
  return this.take();
134
193
  }
135
- /** Reset for reuse, releasing the old buffer (handed-out views stay intact). */
136
- reset() {
137
- if (this.n === 0)
138
- return;
139
- this.release();
140
- }
141
- /** Raw structural content (`{raw}`/`{rawHtml}` escape hatches). Declarative
142
- * nodes go through the `*Opstream` methods, not here. */
194
+ /** Encode the raw-string escape hatch; declarative nodes use `*Opstream` instead. */
143
195
  writeStructuralCommand(cmd, nodeId, node) {
144
196
  const v = node;
145
197
  if (typeof v.raw === "string") {
146
- this.writePayloadCommand(cmd, nodeId, PAYLOAD_RAW_MARKDOWN, v.raw);
198
+ const flags = v.mdxExpressions === false ? RAW_LITERAL_BRACES : 0;
199
+ this.writeRawCommand(cmd, nodeId, flags, v.raw);
147
200
  }
148
201
  else if (typeof v.rawHtml === "string") {
149
- this.writePayloadCommand(cmd, nodeId, PAYLOAD_RAW_HTML, v.rawHtml);
202
+ this.writeRawCommand(cmd, nodeId, RAW_LITERAL_BRACES, v.rawHtml);
150
203
  }
151
204
  else {
152
205
  throw new Error("CommandBuffer: structural content must be {raw} or {rawHtml}");
package/dist/compile.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MdastPluginInput, HastPluginInput } from "./plugin.js";
1
+ import type { MdastPluginList, HastPluginList } from "./plugin.js";
2
2
  import type { MdastNode, HastNode, Data } from "./types.js";
3
3
  /** Configuration for static subtree collapsing during MDX compilation. */
4
4
  export interface OptimizeStaticConfig {
@@ -110,6 +110,8 @@ export interface Features {
110
110
  subscript?: boolean;
111
111
  /** Obsidian-style wikilinks (`[[link]]`). Default: false. */
112
112
  wikilinks?: boolean;
113
+ /** Definition lists (a term line then a `: definition`). Default: false. */
114
+ definitionList?: boolean;
113
115
  /**
114
116
  * Smart punctuation à la SmartyPants. Default: false.
115
117
  *
@@ -119,10 +121,24 @@ export interface Features {
119
121
  * ```
120
122
  */
121
123
  smartPunctuation?: boolean | SmartPunctuationOptions;
124
+ /**
125
+ * Parse raw HTML embedded in Markdown into real HAST element nodes.
126
+ * Default: false.
127
+ *
128
+ * By default, inline and block HTML is kept as opaque `raw` nodes
129
+ * (re-emitted verbatim on stringify). With `rawHtml: true`, the tree is
130
+ * reparsed so raw HTML becomes structured `element`/`text`/`comment` nodes
131
+ * with normalized properties, including tags that open in one raw block
132
+ * and close in another. Positions are not preserved through the reparse.
133
+ */
134
+ rawHtml?: boolean;
122
135
  }
123
136
  export interface CompileOptions {
124
- mdastPlugins?: MdastPluginInput[];
125
- hastPlugins?: HastPluginInput[];
137
+ /** MDAST plugins, in run order. A nested array runs its own plugins, in
138
+ * their own order, at the array's position. */
139
+ mdastPlugins?: MdastPluginList;
140
+ /** HAST plugins, in run order. Nests like `mdastPlugins`. */
141
+ hastPlugins?: HastPluginList;
126
142
  features?: Features;
127
143
  /**
128
144
  * The document being processed, surfaced to plugins as `ctx.fileURL`. Must
@@ -140,12 +156,9 @@ export interface CompileOptions {
140
156
  data?: Data;
141
157
  }
142
158
  /**
143
- * MDX-only compile options.
144
- *
145
- * These are the fields specific to MDX compilation, separate from the shared
146
- * pipeline options in {@link CompileOptions}. Useful for wrappers (Vite/Rollup
147
- * plugins, framework integrations) that want to expose MDX-specific knobs
148
- * without re-exposing the shared pipeline fields.
159
+ * JS/JSX output options, accepted by both {@link mdxToJs} and
160
+ * {@link markdownToJs}. Exported on its own so wrappers can expose the codegen
161
+ * knobs without the shared pipeline fields.
149
162
  */
150
163
  export interface MdxOnlyOptions {
151
164
  optimizeStatic?: OptimizeStaticConfig;
@@ -195,6 +208,8 @@ export interface MdxOnlyOptions {
195
208
  }
196
209
  export interface MdxCompileOptions extends CompileOptions, MdxOnlyOptions {
197
210
  }
211
+ export interface MarkdownToJsOptions extends CompileOptions, MdxOnlyOptions {
212
+ }
198
213
  /** Frontmatter block extracted from the parsed Markdown/MDX source. */
199
214
  export interface Frontmatter {
200
215
  /** Delimiter syntax used for the block. */
@@ -220,27 +235,50 @@ export interface MdxToJsResult {
220
235
  /** Document-level data bag shared with plugins via `ctx.data`; the seeded `data` option if provided, else a fresh `{}`. */
221
236
  data: Data;
222
237
  }
238
+ /** Result of {@link markdownToJs}. */
239
+ export interface MarkdownToJsResult {
240
+ /** Compiled JavaScript module source. */
241
+ code: string;
242
+ /** Frontmatter block at the start of the document, or `null` if none. */
243
+ frontmatter: Frontmatter | null;
244
+ /** Document-level data bag shared with plugins via `ctx.data`; the seeded `data` option if provided, else a fresh `{}`. */
245
+ data: Data;
246
+ }
247
+ type CombineAsync<A> = "async" extends A ? "async" : "maybe" extends A ? "maybe" : "sync";
223
248
  type AnyFn = (...args: any[]) => unknown;
224
- type ReturnsPromise<F> = F extends AnyFn ? Extract<ReturnType<F>, Promise<unknown>> extends never ? false : true : false;
249
+ type ReturnsPromise<F> = F extends AnyFn ? [Extract<ReturnType<F>, Promise<unknown>>] extends [never] ? "sync" : [Exclude<ReturnType<F>, Promise<unknown>>] extends [never] ? "async" : "maybe" : "sync";
225
250
  type FieldIsAsync<V> = V extends AnyFn ? ReturnsPromise<V> : V extends {
226
251
  visit: infer F;
227
252
  } ? ReturnsPromise<F> : V extends ReadonlyArray<infer Item> ? Item extends {
228
253
  visit: infer F;
229
- } ? ReturnsPromise<F> : false : false;
254
+ } ? ReturnsPromise<F> : "sync" : "sync";
230
255
  type AnyVisitorAsync<P> = {
231
256
  [K in keyof P]-?: FieldIsAsync<NonNullable<P[K]>>;
232
257
  }[keyof P];
233
- type IsPluginAsync<P> = true extends AnyVisitorAsync<P> ? true : false;
234
- type ResolveInput<P> = P extends () => infer D ? D : P;
235
- type AnyInputAsync<Ps> = Ps extends ReadonlyArray<infer P> ? true extends IsPluginAsync<ResolveInput<P>> ? true : false : false;
236
- type OptionsAsync<O> = (O extends {
237
- mdastPlugins: infer Ps;
238
- } ? AnyInputAsync<Ps> : false) extends true ? true : (O extends {
239
- hastPlugins: infer Ps;
240
- } ? AnyInputAsync<Ps> : false) extends true ? true : false;
241
- type ResultFor<O, R> = OptionsAsync<O> extends true ? Promise<R> : R;
258
+ type IsPluginAsync<P> = P extends unknown ? CombineAsync<AnyVisitorAsync<P>> : never;
259
+ type ResolveInput<P, D extends readonly unknown[] = [0, 0, 0, 0, 0, 0, 0, 0]> = D extends readonly [
260
+ unknown,
261
+ ...infer Rest
262
+ ] ? P extends ReadonlyArray<infer Item> ? ResolveInput<Item, Rest> : P extends (...args: never[]) => infer Def ? ResolveInput<Def, Rest> : P : never;
263
+ type AnyInputAsync<Ps> = Ps extends ReadonlyArray<infer P> ? CombineAsync<IsPluginAsync<ResolveInput<P>>> : "sync";
264
+ type OptionsAsync<O extends CompileOptions> = CombineAsync<AnyInputAsync<NonNullable<O["mdastPlugins"]>> | AnyInputAsync<NonNullable<O["hastPlugins"]>>>;
265
+ type ResultFor<O extends CompileOptions, R> = {
266
+ sync: R;
267
+ async: Promise<R>;
268
+ maybe: R | Promise<R>;
269
+ }[OptionsAsync<O>];
270
+ export declare function markdownToHtml(source: string): MarkdownToHtmlResult;
242
271
  export declare function markdownToHtml<O extends CompileOptions>(source: string, options?: O): ResultFor<O, MarkdownToHtmlResult>;
272
+ export declare function mdxToJs(source: string): MdxToJsResult;
243
273
  export declare function mdxToJs<O extends MdxCompileOptions>(source: string, options?: O): ResultFor<O, MdxToJsResult>;
274
+ /**
275
+ * Compile plain Markdown to a JavaScript module: like {@link mdxToJs}, but
276
+ * without MDX syntax: `{...}` expressions, JSX tags, and `import`/`export`
277
+ * lines are ordinary Markdown. HTML has no JSX representation and is dropped;
278
+ * enable `features: { rawHtml: true }` to parse it into real elements instead.
279
+ */
280
+ export declare function markdownToJs(source: string): MarkdownToJsResult;
281
+ export declare function markdownToJs<O extends MarkdownToJsOptions>(source: string, options?: O): ResultFor<O, MarkdownToJsResult>;
244
282
  export interface EvaluateOptions extends Omit<MdxCompileOptions, "jsx" | "outputFormat"> {
245
283
  Fragment: unknown;
246
284
  jsx: (type: unknown, props: unknown, key?: unknown) => unknown;
@@ -260,20 +298,30 @@ export interface EvaluateOptions extends Omit<MdxCompileOptions, "jsx" | "output
260
298
  * ```
261
299
  */
262
300
  export declare function evaluate(source: string, options: EvaluateOptions): Record<string, unknown> | Promise<Record<string, unknown>>;
263
- /** Parse Markdown source into a materialized mdast tree. */
264
- export declare function markdownToMdast(source: string, options?: {
301
+ /** Options for the step-by-step tree functions. */
302
+ export interface TreeOptions {
265
303
  features?: Features;
266
- }): MdastNode;
304
+ /**
305
+ * Record `position` on every node. Default: `true`.
306
+ *
307
+ * A unist position is three objects per node and roughly half a materialized
308
+ * tree's memory, on top of the parser's line-index build. Pass `false` when
309
+ * nothing downstream reads `node.position`.
310
+ */
311
+ position?: boolean;
312
+ }
313
+ /** Parse Markdown source into a materialized mdast tree. */
314
+ export declare function markdownToMdast(source: string, options?: TreeOptions): MdastNode;
267
315
  /** Parse MDX source into a materialized mdast tree. */
268
- export declare function mdxToMdast(source: string, options?: {
269
- features?: Features;
270
- }): MdastNode;
316
+ export declare function mdxToMdast(source: string, options?: TreeOptions): MdastNode;
271
317
  /** Convert Markdown source to a materialized hast tree. */
272
- export declare function markdownToHast(source: string, options?: {
273
- features?: Features;
274
- }): HastNode;
318
+ export declare function markdownToHast(source: string, options?: TreeOptions): HastNode;
275
319
  /** Convert MDX source to a materialized hast tree. */
276
- export declare function mdxToHast(source: string, options?: {
277
- features?: Features;
278
- }): HastNode;
320
+ export declare function mdxToHast(source: string, options?: TreeOptions): HastNode;
321
+ /**
322
+ * Parse an HTML string into a materialized hast tree: a `root` whose children
323
+ * are the doctype (if any) and the implied `<html>` subtree. Only available
324
+ * in builds that include the `from-html` feature.
325
+ */
326
+ export declare function htmlToHast(html: string): HastNode;
279
327
  export {};