@toaq-oss/omni-mdx 0.1.11 → 0.1.13

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.
package/README.md CHANGED
@@ -1,221 +1,140 @@
1
1
  # @toaq-oss/omni-mdx
2
2
 
3
- The React/Next.js visual rendering engine of the TOAQ-oss MDX ecosystem.
3
+ **The high-performance MDX engine.** A unified React & Next.js rendering layer powered by a dual Rust backend (Native Node.js + WebAssembly).
4
4
 
5
5
  [![GitHub](https://img.shields.io/badge/GitHub-TOAQ--oss-181717?logo=github)](https://github.com/toaq-oss)
6
-
7
-
8
- This package consumes the ultra-fast Abstract Syntax Tree (AST) generated by our Rust core in WebAssembly (@toaq-oss/core-parser) and transforms it into interactive, secure, and highly customizable React components.
6
+ [![Documentation](https://img.shields.io/badge/Docs-omni--core.org-blue)](https://omni-core.org/mdx)
9
7
 
10
8
  ---
11
9
 
12
- ## Why this exists
13
-
14
- Most MDX pipelines (next-mdx-remote, @next/mdx, contentlayer) run at the JS level and require client-side hydration for math, syntax highlighting, and custom components. This means:
10
+ ## Why Omni-MDX?
15
11
 
16
- - A large JS bundle sent to every visitor
17
- - Math rendered on the client (flash of unstyled content)
18
- - Custom components that can't be pure Server Components
12
+ Traditional MDX pipelines are often slow or require complex client-side hydration. `omni-mdx` solves this by moving the heavy lifting to Rust, providing a seamless bridge between raw content and React components.
19
13
 
20
- `@toaq-oss/omni-mdx` solves this by moving the parse step into Rust and the render step into React Server Components. The result is **zero JS for content** everything is HTML by the time it reaches the browser.
21
-
22
- The WASM build is available as a fallback for Edge runtimes or environments where native addons are not supported.
14
+ - **Dual-Engine Architecture:** Uses native `.node` binaries on the server for maximum speed and **WASM** in the browser for instant live previews.
15
+ - **Zero-Hydration Math:** LaTeX is pre-parsed by Rust and rendered via KaTeX with zero layout shift.
16
+ - **RSC Optimized:** Built from the ground up for Next.js App Router and Server Components.
17
+ - **Non-Blocking:** Offloads parsing from the Node.js main thread to Rust, keeping your server responsive even under heavy load.
23
18
 
24
19
  ---
25
20
 
26
- ## Performance & Architecture
27
-
28
- `@toaq-oss/omni-mdx` is designed for scale. While traditional parsers block the Node.js main thread, our parser offloads the heavy lifting to a native Rust core via WebAssembly or N-API.
21
+ ## 📖 Documentation
29
22
 
30
- **1. Non-Blocking by Design**
31
- Parsing complex documents or extracting data from thousands of files for ML datasets won't freeze your server. Node.js remains completely free to handle other HTTP requests while Rust does the work in the background.
23
+ For full guides, API references, and advanced configuration, visit:
24
+ 👉 **[omni-core.org/mdx](https://omni-core.org/mdx)**
32
25
 
33
- **2. Faster than the Standard**
34
- In a strict end-to-end benchmark (parsing raw text to an exploitable JSX AST) over 1,000 iterations of a complex document:
35
- - `@toaq-oss/omni-mdx` is consistently **~30% faster** than the official `@mdx-js/mdx` compiler.
36
-
37
- **3. Zero Client JS**
38
- Because the AST is generated on the server and maps directly to React Server Components, the client receives 0 bytes of parsing logic.
26
+ ---
39
27
 
40
- ## Installation
28
+ ## 📦 Installation
41
29
 
42
30
  ```bash
43
31
  npm install @toaq-oss/omni-mdx
44
- # KaTeX is optional only needed if your content has math
32
+ # KaTeX is required for math rendering
45
33
  npm install katex
46
34
  ```
47
35
 
48
- Add the KaTeX stylesheet to your `layout.tsx`:
49
-
50
- ```tsx
51
- import "katex/dist/katex.min.css";
36
+ ## Next.js Configuration
37
+ To enable the WebAssembly engine for client-side rendering, update your `next.config.js`:
38
+ ```typescript
39
+ /** @type {import('next').NextConfig} */
40
+ const nextConfig = {
41
+ webpack: (config) => {
42
+ config.experiments = { ...config.experiments, asyncWebAssembly: true };
43
+ config.module.rules.push({
44
+ test: /\.wasm$/,
45
+ type: "asset/resource",
46
+ });
47
+ return config;
48
+ },
49
+ };
50
+
51
+ export default nextConfig;
52
52
  ```
53
53
 
54
54
  ---
55
55
 
56
- ## Usage
56
+ ## 🚀 Usage
57
+ ### 1. Server-Side Rendering (RSC)
58
+ Recommended for documentation, blogs, and research papers.
57
59
 
58
- ### Server Component (recommended)
60
+ > Full example available here :
61
+ > * Basic setup: [TOAQ-oss/omni-core-sandox](https://github.com/TOAQ-oss/omni-mdx-sandbox/tree/main/next/basic-setup)
62
+ > * Advanced rendering : [TOAQ-oss/omni-core-sandox](https://github.com/TOAQ-oss/omni-mdx-sandbox/tree/main/next/advanced-rendering)
59
63
 
60
64
  ```tsx
61
65
  import { parseMdx, MDXServerRenderer } from "@toaq-oss/omni-mdx/server";
62
- import { Note, Details } from "@/components/mdx";
63
-
64
- const COMPONENTS = { Note, Details };
65
-
66
- export default async function ArticlePage({ params }) {
67
- const content = await getArticleContent(params.slug);
68
- const ast = await parseMdx(content);
69
-
70
- return <MDXServerRenderer ast={ast} components={COMPONENTS} />;
71
- }
72
- ```
73
-
74
- That's it. No `getStaticProps`, no serialisation workarounds, no client bundle for the content.
75
-
76
- ### Static generation
66
+ import { MyComponent } from "@/components/mdx";
77
67
 
78
- Because `parseMdx` is async and runs on the server, it works naturally with `generateStaticParams`:
79
-
80
- ```tsx
81
- export async function generateStaticParams() {
82
- const slugs = await getAllSlugs();
83
- return slugs.map(slug => ({ slug }));
84
- }
68
+ export default async function Page({ content }) {
69
+ // Parsed via Native Rust Addon (.node)
70
+ const ast = await parseMdx(content);
85
71
 
86
- export default async function ArticlePage({ params }) {
87
- const content = await getArticleContent(params.slug);
88
- const ast = await parseMdx(content);
89
- return <MDXServerRenderer ast={ast} components={COMPONENTS} />;
72
+ return (
73
+ <MDXServerRenderer
74
+ ast={ast}
75
+ components={{ MyComponent }}
76
+ />
77
+ );
90
78
  }
91
79
  ```
92
80
 
93
- At `next build`, Next.js calls `parseMdx` once per article and pre-renders everything to static HTML. The Rust parser never runs in the browser.
81
+ ### 2. Live Client Editor (WASM)
82
+ Perfect for real-time previews or CMS interfaces.
94
83
 
95
- ### Live editor (client-side)
96
-
97
- For live MDX previews where the content changes in the browser:
84
+ > Full example available here : [TOAQ-oss/omni-core-sandox](https://github.com/TOAQ-oss/omni-mdx-sandbox/tree/main/next/client-rendering)
98
85
 
99
86
  ```tsx
100
87
  "use client";
101
- import { MDXClientRenderer } from "@toaq-oss/omni-mdx/client";
102
-
103
- export function LivePreview({ ast, components }) {
104
- return <MDXClientRenderer ast={ast} components={components} katex />;
88
+ import { useState } from "react";
89
+ import { parseMdx, MDXClientRenderer } from "@toaq-oss/omni-mdx/client";
90
+
91
+ export default function Editor() {
92
+ const [ast, setAst] = useState(null);
93
+
94
+ const handleChange = async (text) => {
95
+ // Runs 100% locally in the browser via WebAssembly
96
+ const result = await parseMdx(text);
97
+ setAst(result);
98
+ };
99
+
100
+ return (
101
+ <div>
102
+ <textarea onChange={(e) => handleChange(e.target.value)} />
103
+ <MDXClientRenderer ast={ast} />
104
+ </div>
105
+ );
105
106
  }
106
107
  ```
107
108
 
108
- The `ast` prop should be computed server-side and passed down, or computed client-side by calling a Route Handler that runs `parseMdx`.
109
-
110
- ---
111
-
112
- ## Import map
113
-
114
- |Import path|What you get|Where to use|
115
- |----|---|----|
116
- |`@toaq-oss/omni-mdx`|Types + `MDX_COMPONENTS` registry|Anywhere|
117
- |`@toaq-oss/omni-mdx/server`|`parseMdx`, `MDXServerRenderer`, `MDXParseError` |Server Components only|
118
- |`@toaq-oss/omni-mdx/client`|`MDXClientRenderer`, `MDXErrorBoundary`|Client Components only|
119
-
120
109
  ---
121
-
122
- ## Custom components
123
-
124
- Register components by name — the key matches the JSX tag in the MDX source:
125
-
126
- ```tsx
127
- // MDX source:
128
- // <Note type="warning" title="Heads up">
129
- // Be careful with **this**.
130
- // </Note>
131
-
132
- import { Note } from "@/components/Note"; // can be a Server Component
133
- import { Details } from "@/components/Details"; // can be a Server Component
134
- import { Chart } from "@/components/Chart"; // "use client" inside
135
-
136
- const COMPONENTS = { Note, Details, Chart };
137
-
138
- const ast = await parseMdx(content);
139
- return <MDXServerRenderer ast={ast} components={COMPONENTS} />;
140
- ```
141
-
142
- Server Components in the registry are rendered on the server with zero client JS. Client Components are hydrated normally.
110
+ |Environment|Backend|Entry Point|
111
+ |:---|:---|:---|
112
+ |**Server** (Node.js)|Native Addon (`.node`)|`@toaq-oss/omni-mdx/server`|
113
+ |**Client** (Browser)|WebAssembly (`.wasm`)|`@toaq-oss/omni-mdx/client`|
114
+ |**Edge** (Vercel)|Native Addon (`.wasm`)|`@toaq-oss/omni-mdx/client`|
143
115
 
144
116
  ---
145
117
 
146
- ## Error handling
147
-
148
- ### Parse errors
149
-
118
+ ## 🧩 Features
119
+ ### 📐 Professional Math
120
+ Math is handled via KaTeX. Simply include the CSS in your `layout.tsx`:
150
121
  ```tsx
151
- import { parseMdx, MDXParseError } from "@toaq-oss/omni-mdx/server";
152
-
153
- try {
154
- const ast = await parseMdx(content);
155
- } catch (e) {
156
- if (e instanceof MDXParseError) {
157
- console.error("MDX syntax error:", e.message);
158
- console.error("Source:", e.source);
159
- }
160
- }
122
+ import "katex/dist/katex.min.css";
161
123
  ```
124
+ * **Inline:** $E=mc^2$
125
+ * **Block:** $$\int f(x)dx$$
162
126
 
163
- ### Component render errors
164
-
165
- In `MDXClientRenderer`, every custom component is automatically wrapped in `MDXErrorBoundary`. If a component throws, the error is isolated — the rest of the document continues to render.
166
-
167
- You can also use `MDXErrorBoundary` directly:
168
-
127
+ ### 🎨 Custom Components
128
+ Register any React component (Server or Client) to handle custom tags:
169
129
  ```tsx
170
- import { MDXErrorBoundary } from "@toaq-oss/omni-mdx/client";
171
-
172
- <MDXErrorBoundary componentName="Chart">
173
- <Chart data={maybeNull} />
174
- </MDXErrorBoundary>
130
+ const components = {
131
+ Callout: ({ children }) => <div className="p-4 bg-blue-50">{children}</div>,
132
+ VocalDataset: dynamic(() => import('./VocalDataset'), { ssr: false })
133
+ };
175
134
  ```
176
135
 
177
136
  ---
178
-
179
- ## Math
180
-
181
- Math is handled entirely by the Rust parser no remark-math or rehype-katex needed.
182
-
183
- | Syntax | Output |
184
- |---|---|
185
- | `$E = mc^2$` | `<span class="math math-inline" data-math="…">` |
186
- | `$$…$$` | `<div class="math math-display" data-math="…">` |
187
-
188
- KaTeX hydrates the `data-math` attributes on the client via `MDXClientRenderer`, or you can use any KaTeX auto-render script in your layout.
189
-
190
- ---
191
-
192
- ## Runtime support
193
-
194
- | Runtime | Native `.node` | WASM fallback |
195
- |-----------------|:--------------:|:-------------:|
196
- | Node.js 18+ | ✅ | ✅ |
197
- | Next.js (RSC) | ✅ | ✅ |
198
- | Edge runtime | ❌ | ✅ |
199
- | Browser | ❌ | ✅ |
200
-
201
- The package auto-detects the environment and loads the appropriate backend. No configuration required.
202
-
203
- ---
204
-
205
- ## Building the native addon
206
-
207
- The `.node` files are pre-built for common platforms. To build for your platform:
208
-
209
- ```bash
210
- cd core-parser
211
- napi build --platform --release --features node --no-js
212
- cp toaq-parser-core.*.node ../packages/mdx-next/native/
213
- ```
214
-
215
- To build the WASM fallback:
216
-
217
- ```bash
218
- cd core-parser
219
- wasm-pack build --target bundler --features wasm
220
- mv pkg/* ../packages/mdx-next/wasm/
221
- ```
137
+ ## 🤝 Contributing
138
+ This package is part of the TOAQ open-source ecosystem.
139
+ * **Core Parser (Rust):** [TOAQ-oss/omni-mdx-core](https://github.com/TOAQ-oss/omni-mdx-core)
140
+ * **Reporting Issues:** Please use the GitHub issue tracker for bugs or feature requests.
package/dist/client.cjs CHANGED
@@ -38,7 +38,8 @@ __export(omni_mdx_core_exports, {
38
38
  initSync: () => initSync,
39
39
  parse_mdx_to_json: () => parse_mdx_to_json,
40
40
  parse_mdx_to_json_pretty: () => parse_mdx_to_json_pretty,
41
- parse_mdx_version: () => parse_mdx_version
41
+ parse_mdx_version: () => parse_mdx_version,
42
+ parse_to_binary: () => parse_to_binary
42
43
  });
43
44
  function parse_mdx_to_json(input) {
44
45
  let deferred3_0;
@@ -94,6 +95,17 @@ function parse_mdx_version() {
94
95
  wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
95
96
  }
96
97
  }
98
+ function parse_to_binary(mdx_input) {
99
+ const ptr0 = passArray8ToWasm0(mdx_input, wasm.__wbindgen_malloc);
100
+ const len0 = WASM_VECTOR_LEN;
101
+ const ret = wasm.parse_to_binary(ptr0, len0);
102
+ if (ret[3]) {
103
+ throw takeFromExternrefTable0(ret[2]);
104
+ }
105
+ var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
106
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
107
+ return v2;
108
+ }
97
109
  function __wbg_get_imports() {
98
110
  const import0 = {
99
111
  __proto__: null,
@@ -101,6 +113,10 @@ function __wbg_get_imports() {
101
113
  const ret = Error(getStringFromWasm0(arg0, arg1));
102
114
  return ret;
103
115
  },
116
+ __wbindgen_cast_0000000000000001: function(arg0, arg1) {
117
+ const ret = getStringFromWasm0(arg0, arg1);
118
+ return ret;
119
+ },
104
120
  __wbindgen_init_externref_table: function() {
105
121
  const table = wasm.__wbindgen_externrefs;
106
122
  const offset = table.grow(4);
@@ -116,6 +132,10 @@ function __wbg_get_imports() {
116
132
  "./omni_mdx_core_bg.js": import0
117
133
  };
118
134
  }
135
+ function getArrayU8FromWasm0(ptr, len) {
136
+ ptr = ptr >>> 0;
137
+ return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
138
+ }
119
139
  function getStringFromWasm0(ptr, len) {
120
140
  ptr = ptr >>> 0;
121
141
  return decodeText(ptr, len);
@@ -126,6 +146,12 @@ function getUint8ArrayMemory0() {
126
146
  }
127
147
  return cachedUint8ArrayMemory0;
128
148
  }
149
+ function passArray8ToWasm0(arg, malloc) {
150
+ const ptr = malloc(arg.length * 1, 1) >>> 0;
151
+ getUint8ArrayMemory0().set(arg, ptr / 1);
152
+ WASM_VECTOR_LEN = arg.length;
153
+ return ptr;
154
+ }
129
155
  function passStringToWasm0(arg, malloc, realloc) {
130
156
  if (realloc === void 0) {
131
157
  const buf = cachedTextEncoder.encode(arg);
@@ -276,6 +302,7 @@ var client_exports = {};
276
302
  __export(client_exports, {
277
303
  MDXClientRenderer: () => MDXClientRenderer,
278
304
  MDXErrorBoundary: () => MDXErrorBoundary,
305
+ MdxBinaryDecoder: () => MdxBinaryDecoder,
279
306
  parseMdx: () => parseMdxClient
280
307
  });
281
308
  module.exports = __toCommonJS(client_exports);
@@ -472,6 +499,111 @@ function MDXClientRenderer({
472
499
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "omni-mdx-root", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MDXClientContent, { ast, components }) });
473
500
  }
474
501
 
502
+ // src/utils/binaryDecoder.ts
503
+ var NODE_TEXT = 1;
504
+ var NODE_ELEMENT = 2;
505
+ var ATTR_TEXT = 16;
506
+ var ATTR_EXPRESSION = 17;
507
+ var ATTR_BOOLEAN = 18;
508
+ var ATTR_AST = 19;
509
+ var MdxBinaryDecoder = class {
510
+ constructor(buffer) {
511
+ this.offset = 0;
512
+ this.decoder = new TextDecoder("utf-8");
513
+ this.stringCache = /* @__PURE__ */ new Map();
514
+ this.buffer = buffer;
515
+ this.view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
516
+ }
517
+ decode() {
518
+ const rootCount = this.readU32();
519
+ const nodes = [];
520
+ for (let i = 0; i < rootCount; i++) {
521
+ nodes.push(this.decodeNode());
522
+ }
523
+ return nodes;
524
+ }
525
+ decodeNode() {
526
+ const type = this.readU8();
527
+ if (type === NODE_TEXT) {
528
+ return {
529
+ node_type: "text",
530
+ content: this.readStringU32()
531
+ };
532
+ }
533
+ if (type === NODE_ELEMENT) {
534
+ const node_type = this.readStringU16();
535
+ const self_closing = this.readU8() === 1;
536
+ const attrCount = this.readU16();
537
+ let attributes = void 0;
538
+ if (attrCount > 0) {
539
+ attributes = {};
540
+ for (let i = 0; i < attrCount; i++) {
541
+ const key = this.readStringU16();
542
+ const attrKind = this.readU8();
543
+ if (attrKind === ATTR_TEXT) {
544
+ attributes[key] = { kind: "text", value: this.readStringU32() };
545
+ } else if (attrKind === ATTR_EXPRESSION) {
546
+ attributes[key] = { kind: "expression", value: this.readStringU32() };
547
+ } else if (attrKind === ATTR_BOOLEAN) {
548
+ attributes[key] = { kind: "boolean" };
549
+ } else if (attrKind === ATTR_AST) {
550
+ const subNodeCount = this.readU32();
551
+ const subNodes = [];
552
+ for (let j = 0; j < subNodeCount; j++) {
553
+ subNodes.push(this.decodeNode());
554
+ }
555
+ attributes[key] = { kind: "ast", value: subNodes };
556
+ }
557
+ }
558
+ }
559
+ const childCount = this.readU32();
560
+ const children = [];
561
+ if (childCount > 0) {
562
+ for (let i = 0; i < childCount; i++) {
563
+ children.push(this.decodeNode());
564
+ }
565
+ }
566
+ return {
567
+ node_type,
568
+ self_closing,
569
+ attributes,
570
+ children
571
+ };
572
+ }
573
+ throw new Error(`[Omni-Core] Unknown binary opcode: ${type} at offset ${this.offset}`);
574
+ }
575
+ readU8() {
576
+ const val = this.view.getUint8(this.offset);
577
+ this.offset += 1;
578
+ return val;
579
+ }
580
+ readU16() {
581
+ const val = this.view.getUint16(this.offset, true);
582
+ this.offset += 2;
583
+ return val;
584
+ }
585
+ readU32() {
586
+ const val = this.view.getUint32(this.offset, true);
587
+ this.offset += 4;
588
+ return val;
589
+ }
590
+ readStringU16() {
591
+ const len = this.readU16();
592
+ const str = this.decoder.decode(this.buffer.subarray(this.offset, this.offset + len));
593
+ this.offset += len;
594
+ let cached = this.stringCache.get(str);
595
+ if (cached) return cached;
596
+ this.stringCache.set(str, str);
597
+ return str;
598
+ }
599
+ readStringU32() {
600
+ const len = this.readU32();
601
+ const str = this.decoder.decode(this.buffer.subarray(this.offset, this.offset + len));
602
+ this.offset += len;
603
+ return str;
604
+ }
605
+ };
606
+
475
607
  // src/parse.client.ts
476
608
  var import_meta2 = {};
477
609
  var _parseClient = null;
@@ -482,15 +614,17 @@ async function getClientParser() {
482
614
  const wasmUrl = new URL("./omni_mdx_core_bg.wasm", import_meta2.url);
483
615
  await wasm2.default(wasmUrl);
484
616
  }
485
- _parseClient = (mdx) => wasm2.parse_mdx_to_json(mdx);
617
+ _parseClient = (mdx) => wasm2.parse_to_binary(mdx);
486
618
  return _parseClient;
487
619
  }
488
620
  async function parseMdxClient(mdx) {
489
621
  if (typeof window === "undefined") return [];
490
622
  try {
491
623
  const parse = await getClientParser();
492
- const json = parse(mdx);
493
- return JSON.parse(json);
624
+ const inputBuffer = typeof mdx === "string" ? new TextEncoder().encode(mdx) : mdx;
625
+ const binaryAst = parse(inputBuffer);
626
+ const decoder = new MdxBinaryDecoder(binaryAst);
627
+ return decoder.decode();
494
628
  } catch (err) {
495
629
  console.error("[omni-mdx] WASM client parse error:", err);
496
630
  return [];