@draftbase/renderer 0.5.3 → 0.5.5
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/dist/MDXErrorBoundary.d.ts +3 -3
- package/dist/MDXErrorBoundary.js +3 -3
- package/dist/reactNative.d.ts +7 -1
- package/dist/reactNative.js +165 -33
- package/dist/reactNative.test.js +14 -17
- package/package.json +2 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Component, type ReactNode } from "react";
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from "react";
|
|
2
2
|
interface MDXErrorBoundaryProps {
|
|
3
3
|
children: ReactNode;
|
|
4
4
|
fallback: ReactNode;
|
|
5
|
-
onError?: (error: unknown) => void;
|
|
5
|
+
onError?: (error: unknown, errorInfo: ErrorInfo) => void;
|
|
6
6
|
}
|
|
7
7
|
interface MDXErrorBoundaryState {
|
|
8
8
|
hasError: boolean;
|
|
@@ -12,7 +12,7 @@ export declare class MDXErrorBoundary extends Component<MDXErrorBoundaryProps, M
|
|
|
12
12
|
static getDerivedStateFromError(): {
|
|
13
13
|
hasError: boolean;
|
|
14
14
|
};
|
|
15
|
-
componentDidCatch(error: unknown): void;
|
|
15
|
+
componentDidCatch(error: unknown, errorInfo: ErrorInfo): void;
|
|
16
16
|
render(): ReactNode;
|
|
17
17
|
}
|
|
18
18
|
export {};
|
package/dist/MDXErrorBoundary.js
CHANGED
|
@@ -7,9 +7,9 @@ export class MDXErrorBoundary extends Component {
|
|
|
7
7
|
static getDerivedStateFromError() {
|
|
8
8
|
return { hasError: true };
|
|
9
9
|
}
|
|
10
|
-
componentDidCatch(error) {
|
|
11
|
-
console.error("MDX content failed to render", error);
|
|
12
|
-
this.props.onError?.(error);
|
|
10
|
+
componentDidCatch(error, errorInfo) {
|
|
11
|
+
console.error("MDX content failed to render", error, errorInfo.componentStack);
|
|
12
|
+
this.props.onError?.(error, errorInfo);
|
|
13
13
|
}
|
|
14
14
|
render() {
|
|
15
15
|
return this.state.hasError ? this.props.fallback : this.props.children;
|
package/dist/reactNative.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MDXComponents } from "mdx/types";
|
|
2
2
|
import type { ComponentType } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import type { FailedMDX } from "./core.js";
|
|
4
4
|
import { type ReactNativePrimitives, type ReactNativeStyleOptions } from "./reactNativeComponents.js";
|
|
5
5
|
export interface CompiledMDX {
|
|
6
6
|
ok: true;
|
|
@@ -12,6 +12,12 @@ export type { FailedMDX };
|
|
|
12
12
|
/**
|
|
13
13
|
* Wires up a React Native `compileMDX` with default Text/View/Image mappings for every standard markdown element
|
|
14
14
|
* (and `EntryLink`), so a project sets up its RN primitives once instead of mapping every tag on every call.
|
|
15
|
+
*
|
|
16
|
+
* Parses MDX to an AST and walks it directly instead of compiling to JS and `evaluate()`-ing it — Hermes (React
|
|
17
|
+
* Native's JS engine) has no `Function`/`eval` support, which `@mdx-js/mdx`'s `evaluate()` requires. This also
|
|
18
|
+
* means only literal string JSX attributes are supported (e.g. `<EntryLink id="x">`) — `{expression}` attributes
|
|
19
|
+
* and `{expression}` content would need the same eval this exists to avoid, and aren't used by lesson content.
|
|
20
|
+
*
|
|
15
21
|
* A tag/component the source references but nothing maps (a typo'd custom component, a raw HTML tag RN has no
|
|
16
22
|
* native view for) is caught at render — logged via `console.error` and replaced with the raw source as
|
|
17
23
|
* plain text, instead of crashing the screen.
|
package/dist/reactNative.js
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
import { unified } from "unified";
|
|
2
|
+
import remarkParse from "remark-parse";
|
|
3
|
+
import remarkGfm from "remark-gfm";
|
|
4
|
+
import remarkMdx from "remark-mdx";
|
|
1
5
|
import * as runtime from "react/jsx-runtime";
|
|
2
|
-
import { compileMDXCore } from "./core.js";
|
|
3
6
|
import { MDXErrorBoundary } from "./MDXErrorBoundary.js";
|
|
4
7
|
import { buildReactNativeComponents, } from "./reactNativeComponents.js";
|
|
8
|
+
const processor = unified().use(remarkParse).use(remarkGfm).use(remarkMdx);
|
|
5
9
|
/**
|
|
6
10
|
* Wires up a React Native `compileMDX` with default Text/View/Image mappings for every standard markdown element
|
|
7
11
|
* (and `EntryLink`), so a project sets up its RN primitives once instead of mapping every tag on every call.
|
|
12
|
+
*
|
|
13
|
+
* Parses MDX to an AST and walks it directly instead of compiling to JS and `evaluate()`-ing it — Hermes (React
|
|
14
|
+
* Native's JS engine) has no `Function`/`eval` support, which `@mdx-js/mdx`'s `evaluate()` requires. This also
|
|
15
|
+
* means only literal string JSX attributes are supported (e.g. `<EntryLink id="x">`) — `{expression}` attributes
|
|
16
|
+
* and `{expression}` content would need the same eval this exists to avoid, and aren't used by lesson content.
|
|
17
|
+
*
|
|
8
18
|
* A tag/component the source references but nothing maps (a typo'd custom component, a raw HTML tag RN has no
|
|
9
19
|
* native view for) is caught at render — logged via `console.error` and replaced with the raw source as
|
|
10
20
|
* plain text, instead of crashing the screen.
|
|
@@ -12,32 +22,34 @@ import { buildReactNativeComponents, } from "./reactNativeComponents.js";
|
|
|
12
22
|
export function createReactNativeRenderer(primitives, styleOptions) {
|
|
13
23
|
const defaultComponents = buildReactNativeComponents(primitives, styleOptions);
|
|
14
24
|
async function compileMDX(source) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
25
|
+
let tree;
|
|
26
|
+
try {
|
|
27
|
+
tree = processor.parse(source);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
error: new Error(`MDX parse failed (${source.length} chars): ${describeError(error)}`, {
|
|
33
|
+
cause: error,
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// Reference-style links/images (`[text][ref]` + a `[ref]: url` definition anywhere in the
|
|
38
|
+
// doc) resolve against whichever `definition` node shares their identifier — collect them
|
|
39
|
+
// once per parse instead of re-scanning the tree for every reference.
|
|
40
|
+
const definitions = {};
|
|
41
|
+
for (const node of tree.children ?? []) {
|
|
42
|
+
if (node.type === "definition" && node.identifier && node.url) {
|
|
43
|
+
definitions[node.identifier] = node.url;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function Root(props) {
|
|
47
|
+
const components = { ...defaultComponents, ...props.components };
|
|
48
|
+
return renderChildren(tree.children ?? [], { components, definitions, source });
|
|
49
|
+
}
|
|
38
50
|
const SafeContent = (props) => runtime.jsx(MDXErrorBoundary, {
|
|
39
51
|
fallback: runtime.jsx(primitives.Text, { children: source }),
|
|
40
|
-
children: runtime.jsx(
|
|
52
|
+
children: runtime.jsx(Root, props),
|
|
41
53
|
});
|
|
42
54
|
return {
|
|
43
55
|
ok: true,
|
|
@@ -46,12 +58,132 @@ export function createReactNativeRenderer(primitives, styleOptions) {
|
|
|
46
58
|
}
|
|
47
59
|
return { compileMDX };
|
|
48
60
|
}
|
|
49
|
-
function
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
function renderChildren(nodes, ctx) {
|
|
62
|
+
const rendered = nodes.map((node, index) => renderNode(node, ctx, index)).filter(isRenderable);
|
|
63
|
+
// Mirrors @mdx-js's compiled output: a single top-level node renders directly, only 2+
|
|
64
|
+
// siblings need a Fragment wrapper.
|
|
65
|
+
if (rendered.length === 1)
|
|
66
|
+
return rendered[0];
|
|
67
|
+
return runtime.jsx(runtime.Fragment, { children: rendered });
|
|
68
|
+
}
|
|
69
|
+
function textChild(nodes, ctx) {
|
|
70
|
+
const rendered = nodes.map((node, index) => renderNode(node, ctx, index)).filter(isRenderable);
|
|
71
|
+
return rendered.length === 1 ? rendered[0] : rendered;
|
|
72
|
+
}
|
|
73
|
+
function isRenderable(node) {
|
|
74
|
+
return node !== null && node !== undefined;
|
|
75
|
+
}
|
|
76
|
+
// A single bad node (a typo'd/missing custom component, an mdast node type this walker doesn't
|
|
77
|
+
// know) shouldn't blank out an otherwise-good document — catch here, at the smallest node that
|
|
78
|
+
// failed, and swap in its literal source text instead of losing every sibling around it.
|
|
79
|
+
function renderNode(node, ctx, key) {
|
|
80
|
+
try {
|
|
81
|
+
return renderNodeUnsafe(node, ctx, key);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const label = node.name ? `<${node.name}>` : node.type;
|
|
85
|
+
const wrapped = new Error(`MDX node "${label}" failed to render: ${describeError(error)}`, {
|
|
86
|
+
cause: error,
|
|
87
|
+
});
|
|
88
|
+
console.error(wrapped.message, error);
|
|
89
|
+
const raw = node.position
|
|
90
|
+
? ctx.source.slice(node.position.start.offset, node.position.end.offset)
|
|
91
|
+
: `<${label}>`;
|
|
92
|
+
try {
|
|
93
|
+
return jsx(ctx.components.p, { children: raw }, key);
|
|
94
|
+
}
|
|
95
|
+
catch (fallbackError) {
|
|
96
|
+
throw new Error(`MDX node "${label}" failed to render and had no fallback component`, {
|
|
97
|
+
cause: fallbackError,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function describeError(error) {
|
|
103
|
+
return error instanceof Error ? error.message : String(error);
|
|
104
|
+
}
|
|
105
|
+
function renderNodeUnsafe(node, ctx, key) {
|
|
106
|
+
const { components } = ctx;
|
|
107
|
+
switch (node.type) {
|
|
108
|
+
case "paragraph":
|
|
109
|
+
return jsx(components.p, { children: textChild(node.children ?? [], ctx) }, key);
|
|
110
|
+
case "heading":
|
|
111
|
+
return jsx(components[`h${node.depth}`], { children: textChild(node.children ?? [], ctx) }, key);
|
|
112
|
+
case "strong":
|
|
113
|
+
return jsx(components.strong, { children: textChild(node.children ?? [], ctx) }, key);
|
|
114
|
+
case "emphasis":
|
|
115
|
+
return jsx(components.em, { children: textChild(node.children ?? [], ctx) }, key);
|
|
116
|
+
case "delete":
|
|
117
|
+
return jsx(components.del, { children: textChild(node.children ?? [], ctx) }, key);
|
|
118
|
+
case "inlineCode":
|
|
119
|
+
return jsx(components.code, { children: node.value }, key);
|
|
120
|
+
case "code":
|
|
121
|
+
return jsx(components.pre, { children: jsx(components.code, { children: node.value }, 0) }, key);
|
|
122
|
+
case "list":
|
|
123
|
+
return jsx(node.ordered ? components.ol : components.ul, { children: (node.children ?? []).map((item, i) => renderNode(item, ctx, i)) }, key);
|
|
124
|
+
case "listItem":
|
|
125
|
+
return jsx(components.li, { children: textChild(node.children ?? [], ctx) }, key);
|
|
126
|
+
case "blockquote":
|
|
127
|
+
return jsx(components.blockquote, { children: (node.children ?? []).map((child, i) => renderNode(child, ctx, i)) }, key);
|
|
128
|
+
case "thematicBreak":
|
|
129
|
+
return jsx(components.hr, {}, key);
|
|
130
|
+
case "table":
|
|
131
|
+
return renderTable(node, ctx, key);
|
|
132
|
+
case "link":
|
|
133
|
+
return jsx(components.a, { href: node.url, children: textChild(node.children ?? [], ctx) }, key);
|
|
134
|
+
case "image":
|
|
135
|
+
return jsx(components.img, { src: node.url, alt: node.alt ?? undefined }, key);
|
|
136
|
+
case "linkReference":
|
|
137
|
+
return jsx(components.a, {
|
|
138
|
+
href: node.identifier ? ctx.definitions[node.identifier] : undefined,
|
|
139
|
+
children: textChild(node.children ?? [], ctx),
|
|
140
|
+
}, key);
|
|
141
|
+
case "imageReference":
|
|
142
|
+
return jsx(components.img, {
|
|
143
|
+
src: node.identifier ? ctx.definitions[node.identifier] : undefined,
|
|
144
|
+
alt: node.alt ?? undefined,
|
|
145
|
+
}, key);
|
|
146
|
+
case "definition":
|
|
147
|
+
// Consumed up front into `ctx.definitions`; renders nothing on its own.
|
|
148
|
+
return null;
|
|
149
|
+
case "text":
|
|
150
|
+
return node.value;
|
|
151
|
+
case "break":
|
|
152
|
+
return "\n";
|
|
153
|
+
case "mdxJsxFlowElement":
|
|
154
|
+
case "mdxJsxTextElement":
|
|
155
|
+
return renderJsx(node, ctx, key);
|
|
156
|
+
default:
|
|
157
|
+
throw new Error(`Unsupported MDX content: "${node.type}" node`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function renderJsx(node, ctx, key) {
|
|
161
|
+
const Component = node.name ? ctx.components[node.name] : undefined;
|
|
162
|
+
if (!Component)
|
|
163
|
+
throw new Error(`Unknown MDX component: <${node.name ?? "?"}>`);
|
|
164
|
+
const props = {};
|
|
165
|
+
for (const attr of node.attributes ?? []) {
|
|
166
|
+
if (attr.type === "mdxJsxAttribute" &&
|
|
167
|
+
attr.name &&
|
|
168
|
+
(attr.value === null || typeof attr.value === "string")) {
|
|
169
|
+
props[attr.name] = attr.value ?? true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return jsx(Component, { ...props, children: textChild(node.children ?? [], ctx) }, key);
|
|
173
|
+
}
|
|
174
|
+
function renderTable(node, ctx, key) {
|
|
175
|
+
const { components } = ctx;
|
|
176
|
+
const [headerRow, ...bodyRows] = node.children ?? [];
|
|
177
|
+
const cells = (row, CellTag) => (row?.children ?? []).map((cell, i) => jsx(CellTag, { children: textChild(cell.children ?? [], ctx) }, i));
|
|
178
|
+
return jsx(components.table, {
|
|
179
|
+
children: [
|
|
180
|
+
jsx(components.thead, { children: jsx(components.tr, { children: cells(headerRow, components.th) }, 0) }, 0),
|
|
181
|
+
jsx(components.tbody, {
|
|
182
|
+
children: bodyRows.map((row, i) => jsx(components.tr, { children: cells(row, components.td) }, i)),
|
|
183
|
+
}, 1),
|
|
184
|
+
],
|
|
185
|
+
}, key);
|
|
186
|
+
}
|
|
187
|
+
function jsx(Component, props, key) {
|
|
188
|
+
return runtime.jsx(Component, props, key);
|
|
57
189
|
}
|
package/dist/reactNative.test.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { createReactNativeRenderer } from "./reactNative.js";
|
|
4
|
-
import { MDXErrorBoundary } from "./MDXErrorBoundary.js";
|
|
5
4
|
// Fakes standing in for react-native's Text/View/Image — see reactNativeComponents.test.ts.
|
|
6
5
|
const Text = () => null;
|
|
7
6
|
const View = () => null;
|
|
@@ -30,9 +29,9 @@ test("React Native smoke test: renders standard markdown through Text/View with
|
|
|
30
29
|
assert.equal(heading.type, Text);
|
|
31
30
|
assert.equal(heading.props.style.fontSize, 28);
|
|
32
31
|
});
|
|
33
|
-
test("React Native
|
|
32
|
+
test("React Native swaps only the unknown tag for its raw source, logs to console, and keeps rendering its siblings", async () => {
|
|
34
33
|
const { compileMDX } = createReactNativeRenderer({ Text, View, Image });
|
|
35
|
-
const result = await compileMDX("<Callout>hi</Callout
|
|
34
|
+
const result = await compileMDX("# Heading\n\n<Callout>hi</Callout>\n\nMore text");
|
|
36
35
|
assert.equal(result.ok, true);
|
|
37
36
|
if (!result.ok)
|
|
38
37
|
return;
|
|
@@ -40,22 +39,20 @@ test("React Native falls back to the raw source and logs to console instead of c
|
|
|
40
39
|
const logged = [];
|
|
41
40
|
console.error = (...args) => logged.push(args);
|
|
42
41
|
try {
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Object.assign(boundary.state, MDXErrorBoundary.getDerivedStateFromError());
|
|
55
|
-
}
|
|
56
|
-
const fallback = resolve(boundary.render());
|
|
42
|
+
// MDXErrorBoundary never triggers here — the failure is caught per-node inside compileMDX,
|
|
43
|
+
// not left to bubble up and blank the whole document.
|
|
44
|
+
const element = resolve(result.Content({}));
|
|
45
|
+
const children = element.props.children;
|
|
46
|
+
assert.equal(children.length, 3);
|
|
47
|
+
assert.equal(resolve(children[0]).type, Text); // heading
|
|
48
|
+
// <Callout>hi</Callout> parses as an inline JSX child of its own paragraph, so the fallback
|
|
49
|
+
// (also Text-wrapped) sits one level inside that paragraph rather than replacing it outright.
|
|
50
|
+
const paragraph = resolve(children[1]);
|
|
51
|
+
assert.equal(paragraph.type, Text);
|
|
52
|
+
const fallback = resolve(paragraph.props.children);
|
|
57
53
|
assert.equal(fallback.type, Text);
|
|
58
54
|
assert.equal(fallback.props.children, "<Callout>hi</Callout>");
|
|
55
|
+
assert.equal(resolve(children[2]).type, Text); // "More text" paragraph
|
|
59
56
|
}
|
|
60
57
|
finally {
|
|
61
58
|
console.error = originalConsoleError;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@draftbase/renderer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "Framework-agnostic MDX renderer for Draftbase content (React, Next.js RSC, React Native, Astro, Vite, Vue)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"draftbase",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@mdx-js/mdx": "^3.1.1",
|
|
61
61
|
"remark-gfm": "^4.0.0",
|
|
62
|
+
"remark-mdx": "^3.1.1",
|
|
62
63
|
"remark-parse": "^11.0.0",
|
|
63
64
|
"remark-rehype": "^11.1.2",
|
|
64
65
|
"rehype-raw": "^7.0.0",
|