@draftbase/renderer 0.4.3 → 0.5.1
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/MDXContent.d.ts +6 -2
- package/dist/MDXContent.js +9 -4
- package/dist/MDXContent.test.js +20 -5
- package/dist/MDXErrorBoundary.d.ts +1 -0
- package/dist/MDXErrorBoundary.js +1 -0
- package/dist/cssText.d.ts +1 -0
- package/dist/cssText.js +2 -0
- package/dist/styles.css +19 -6
- package/dist/toHtml.d.ts +14 -2
- package/dist/toHtml.js +30 -12
- package/dist/toHtml.test.js +28 -0
- package/package.json +3 -3
package/dist/MDXContent.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface MDXContentProps {
|
|
|
19
19
|
/** Custom components available by name inside the MDX source, and/or overrides for standard markdown elements —
|
|
20
20
|
* required on non-DOM renderers such as React Native, which have no intrinsic `div`/`p`/`a` tags. */
|
|
21
21
|
components?: MDXComponents;
|
|
22
|
-
/** Skip the default `db-content` styling class. */
|
|
22
|
+
/** Skip the default `db-content` styling class and its inlined styles. */
|
|
23
23
|
unstyled?: boolean;
|
|
24
24
|
/** Extra class name(s) merged onto the wrapper element. */
|
|
25
25
|
className?: string;
|
|
@@ -29,9 +29,13 @@ export interface MDXContentProps {
|
|
|
29
29
|
/** Element/component used to render the fallback plain-text output when `source` fails to
|
|
30
30
|
* compile as MDX. Defaults to `"p"`; pass React Native's `Text`. */
|
|
31
31
|
errorTag?: ElementType;
|
|
32
|
+
/** Called (in addition to `console.error`) when `source` fails to compile, or a component
|
|
33
|
+
* throws while rendering. Either way the fallback plain-text output still renders instead of
|
|
34
|
+
* crashing. */
|
|
35
|
+
onError?: (error: unknown) => void;
|
|
32
36
|
}
|
|
33
37
|
/**
|
|
34
38
|
* Next.js Server Component wrapper around {@link compileMDX}. For non-RSC React, call `compileMDX` directly instead —
|
|
35
39
|
* `await` inside a component body only works as an RSC.
|
|
36
40
|
*/
|
|
37
|
-
export declare function MDXContent({ source, components, unstyled, className, wrapperTag, errorTag, }: MDXContentProps): Promise<ReactNode>;
|
|
41
|
+
export declare function MDXContent({ source, components, unstyled, className, wrapperTag, errorTag, onError, }: MDXContentProps): Promise<ReactNode>;
|
package/dist/MDXContent.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as runtime from "react/jsx-runtime";
|
|
3
3
|
import { compileMDXCore, makeDefaultEntryLink } from "./core.js";
|
|
4
4
|
import { wrapperClassName } from "./wrapperClassName.js";
|
|
5
5
|
import { MDXErrorBoundary } from "./MDXErrorBoundary.js";
|
|
6
|
+
import { CSS_TEXT } from "./cssText.js";
|
|
6
7
|
const defaultComponents = { EntryLink: makeDefaultEntryLink(runtime) };
|
|
7
8
|
/**
|
|
8
9
|
* Compiles raw MDX/markdown into a renderable React component — no DOM assumptions, safe for client effects or RSC.
|
|
@@ -15,17 +16,21 @@ export async function compileMDX(source) {
|
|
|
15
16
|
* Next.js Server Component wrapper around {@link compileMDX}. For non-RSC React, call `compileMDX` directly instead —
|
|
16
17
|
* `await` inside a component body only works as an RSC.
|
|
17
18
|
*/
|
|
18
|
-
export async function MDXContent({ source, components, unstyled, className, wrapperTag = "div", errorTag = "p", }) {
|
|
19
|
+
export async function MDXContent({ source, components, unstyled, className, wrapperTag = "div", errorTag = "p", onError, }) {
|
|
19
20
|
const Wrapper = wrapperTag;
|
|
20
21
|
const ErrorTag = errorTag;
|
|
21
22
|
const wrapperClass = wrapperClassName(unstyled, className);
|
|
23
|
+
// `href` + `precedence` make React DOM treat this as a de-duplicated, hoisted stylesheet
|
|
24
|
+
// resource (React 19+) instead of a plain inline tag repeated per render.
|
|
25
|
+
const styles = unstyled ? null : (_jsx("style", { href: "db-content-styles", precedence: "default", children: CSS_TEXT }));
|
|
22
26
|
const compiled = await compileMDX(source);
|
|
23
27
|
if (!compiled.ok) {
|
|
24
28
|
// Source isn't valid MDX/JSX (e.g. stray `<`/`{` in prose) — fail soft instead
|
|
25
29
|
// of crashing the page; render it as plain text so the copy still shows.
|
|
26
30
|
console.error("MDX compile failed, rendering as plain text", compiled.error);
|
|
27
|
-
|
|
31
|
+
onError?.(compiled.error);
|
|
32
|
+
return (_jsxs(_Fragment, { children: [styles, _jsx(Wrapper, { className: wrapperClass, children: _jsx(ErrorTag, { children: source }) })] }));
|
|
28
33
|
}
|
|
29
34
|
const { Content } = compiled;
|
|
30
|
-
return (_jsx(Wrapper, { className: wrapperClass, children: _jsx(MDXErrorBoundary, { fallback: _jsx(ErrorTag, { children: source }), children: _jsx(Content, { components: components }) }) }));
|
|
35
|
+
return (_jsxs(_Fragment, { children: [styles, _jsx(Wrapper, { className: wrapperClass, children: _jsx(MDXErrorBoundary, { fallback: _jsx(ErrorTag, { children: source }), onError: onError, children: _jsx(Content, { components: components }) }) })] }));
|
|
31
36
|
}
|
package/dist/MDXContent.test.js
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { MDXContent } from "./MDXContent.js";
|
|
4
|
-
// MDXContent is an async Server Component; call it directly (as React's RSC runtime would) and inspect
|
|
4
|
+
// MDXContent is an async Server Component; call it directly (as React's RSC runtime would) and inspect
|
|
5
|
+
// the returned element tree. It returns a fragment: [inlined <style> | null, the wrapper element].
|
|
6
|
+
function wrapperOf(fragment) {
|
|
7
|
+
const [, wrapper] = fragment.props.children;
|
|
8
|
+
return wrapper;
|
|
9
|
+
}
|
|
5
10
|
test("MDXContent wraps compiled MDX in the styled wrapper element", async () => {
|
|
6
|
-
const
|
|
11
|
+
const fragment = (await MDXContent({ source: "# Hello" }));
|
|
12
|
+
const [styles] = fragment.props.children;
|
|
13
|
+
assert.equal(styles.type, "style");
|
|
14
|
+
const element = wrapperOf(fragment);
|
|
7
15
|
assert.equal(element.type, "div");
|
|
8
16
|
assert.equal(element.props.className, "db-content");
|
|
9
17
|
});
|
|
10
|
-
test("unstyled drops the db-content class", async () => {
|
|
11
|
-
const
|
|
18
|
+
test("unstyled drops the db-content class and the inlined styles", async () => {
|
|
19
|
+
const fragment = (await MDXContent({
|
|
20
|
+
source: "# Hello",
|
|
21
|
+
unstyled: true,
|
|
22
|
+
}));
|
|
23
|
+
const [styles] = fragment.props.children;
|
|
24
|
+
assert.equal(styles, null);
|
|
25
|
+
const element = wrapperOf(fragment);
|
|
12
26
|
assert.equal(element.props.className, undefined);
|
|
13
27
|
});
|
|
14
28
|
test("falls back to plain text when the source fails to compile as MDX", async () => {
|
|
15
|
-
const
|
|
29
|
+
const fragment = (await MDXContent({ source: "<broken" }));
|
|
30
|
+
const element = wrapperOf(fragment);
|
|
16
31
|
const fallback = element.props.children;
|
|
17
32
|
assert.equal(fallback.type, "p");
|
|
18
33
|
assert.equal(fallback.props.children, "<broken");
|
package/dist/MDXErrorBoundary.js
CHANGED
|
@@ -9,6 +9,7 @@ export class MDXErrorBoundary extends Component {
|
|
|
9
9
|
}
|
|
10
10
|
componentDidCatch(error) {
|
|
11
11
|
console.error("MDX content failed to render", error);
|
|
12
|
+
this.props.onError?.(error);
|
|
12
13
|
}
|
|
13
14
|
render() {
|
|
14
15
|
return this.state.hasError ? this.props.fallback : this.props.children;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CSS_TEXT = "/* Slim default typography for Draftbase MDX content.\n Opt-in: import \"@draftbase/renderer/styles.css\".\n Disable per-render: <MDXContent unstyled /> or toHtml(source, { unstyled: true }).\n Theme via custom properties: --db-heading-color, --db-link-color, --db-accent-border,\n --db-target-bg. Override further: target \".db-content\" with higher-specificity or\n later-loaded rules. */\n\n.db-content {\n line-height: 1.6;\n color: inherit;\n overflow-wrap: break-word;\n}\n\n.db-content h1,\n.db-content h2,\n.db-content h3,\n.db-content h4,\n.db-content h5,\n.db-content h6 {\n color: var(--db-heading-color, inherit);\n}\n\n.db-content a {\n color: var(--db-link-color, inherit);\n}\n\n/* remark-gfm emits the footnotes label as .sr-only; define it so it stays hidden\n without the consumer needing Tailwind or their own utility. */\n.db-content .sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n.db-content > :first-child {\n margin-top: 0;\n}\n.db-content > :last-child {\n margin-bottom: 0;\n}\n\n.db-content h1,\n.db-content h2,\n.db-content h3,\n.db-content h4,\n.db-content h5,\n.db-content h6 {\n font-weight: 600;\n line-height: 1.25;\n margin: 1.5em 0 0.5em;\n}\n\n/* Highlights a footnote/citation anchor when the URL hash lands on it, e.g. #fn-1. */\n.db-content :target {\n background: var(--db-target-bg, rgba(127, 127, 127, 0.15));\n}\n\n.db-content h1 {\n font-size: 2.25em;\n font-weight: 700;\n line-height: 1.2;\n letter-spacing: -0.025em;\n margin-top: 1.25em;\n}\n\n.db-content h2 {\n font-size: 1.5em;\n line-height: 1.25;\n letter-spacing: -0.02em;\n margin-top: 1.75em;\n}\n\n.db-content h3 {\n font-size: 1.25em;\n line-height: 1.3;\n}\n\n.db-content h4 {\n font-size: 1.125em;\n line-height: 1.35;\n}\n\n.db-content h5 {\n font-size: 1em;\n line-height: 1.4;\n}\n\n.db-content h6 {\n font-size: 0.875em;\n line-height: 1.4;\n}\n\n@media (max-width: 640px) {\n .db-content h1 {\n font-size: 1.875em;\n }\n .db-content h2 {\n font-size: 1.25em;\n }\n .db-content h3 {\n font-size: 1.125em;\n }\n}\n\n.db-content p,\n.db-content ul,\n.db-content ol,\n.db-content blockquote,\n.db-content pre,\n.db-content table {\n margin: 0 0 1em;\n}\n\n.db-content ul,\n.db-content ol {\n padding-left: 1.5em;\n}\n\n.db-content ul {\n list-style-type: disc;\n}\n\n.db-content ol {\n list-style-type: decimal;\n}\n\n.db-content ul ul {\n list-style-type: circle;\n}\n\n.db-content ol ol {\n list-style-type: lower-alpha;\n}\n\n.db-content ul ul ul,\n.db-content ol ol ol {\n list-style-type: square;\n}\n\n.db-content li {\n margin: 0.25em 0;\n}\n.db-content li > ul,\n.db-content li > ol {\n margin: 0.25em 0 0;\n}\n.db-content li > p {\n margin: 0;\n}\n\n.db-content ul.contains-task-list {\n list-style: none;\n padding-left: 0.25em;\n}\n.db-content li.task-list-item {\n list-style: none;\n margin-left: 0;\n}\n.db-content li.task-list-item input[type=\"checkbox\"] {\n margin-right: 0.5em;\n}\n\n.db-content strong {\n font-weight: 600;\n}\n.db-content em {\n font-style: italic;\n}\n.db-content del {\n text-decoration: line-through;\n opacity: 0.75;\n}\n\n.db-content blockquote {\n margin-left: 0;\n padding-left: 1em;\n border-left: 3px solid var(--db-accent-border, currentColor);\n opacity: 0.85;\n}\n\n.db-content code {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 0.9em;\n padding: 0.15em 0.35em;\n background: rgba(127, 127, 127, 0.15);\n border-radius: 4px;\n}\n\n.db-content pre {\n padding: 1em;\n overflow-x: auto;\n background: rgba(127, 127, 127, 0.1);\n border-radius: 8px;\n}\n.db-content pre code {\n padding: 0;\n background: none;\n}\n\n/* display:block so wide tables scroll instead of overflowing the page on mobile;\n costs full-bleed stretch for narrow tables. */\n.db-content table {\n border-collapse: collapse;\n display: block;\n width: max-content;\n max-width: 100%;\n overflow-x: auto;\n}\n\n.db-content th,\n.db-content td {\n border: 1px solid rgba(127, 127, 127, 0.3);\n padding: 0.5em 0.75em;\n text-align: left;\n}\n\n.db-content th {\n background: rgba(127, 127, 127, 0.08);\n font-weight: 600;\n}\n\n.db-content img {\n max-width: 100%;\n height: auto;\n border-radius: 8px;\n}\n\n.db-content a {\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n.db-content a:hover {\n opacity: 0.8;\n}\n\n.db-content hr {\n border: none;\n border-top: 1px solid rgba(127, 127, 127, 0.3);\n margin: 2em 0;\n}\n";
|
package/dist/cssText.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// Generated from styles.css by scripts/generate-css-text.mjs — do not edit directly.
|
|
2
|
+
export const CSS_TEXT = "/* Slim default typography for Draftbase MDX content.\n Opt-in: import \"@draftbase/renderer/styles.css\".\n Disable per-render: <MDXContent unstyled /> or toHtml(source, { unstyled: true }).\n Theme via custom properties: --db-heading-color, --db-link-color, --db-accent-border,\n --db-target-bg. Override further: target \".db-content\" with higher-specificity or\n later-loaded rules. */\n\n.db-content {\n line-height: 1.6;\n color: inherit;\n overflow-wrap: break-word;\n}\n\n.db-content h1,\n.db-content h2,\n.db-content h3,\n.db-content h4,\n.db-content h5,\n.db-content h6 {\n color: var(--db-heading-color, inherit);\n}\n\n.db-content a {\n color: var(--db-link-color, inherit);\n}\n\n/* remark-gfm emits the footnotes label as .sr-only; define it so it stays hidden\n without the consumer needing Tailwind or their own utility. */\n.db-content .sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n.db-content > :first-child {\n margin-top: 0;\n}\n.db-content > :last-child {\n margin-bottom: 0;\n}\n\n.db-content h1,\n.db-content h2,\n.db-content h3,\n.db-content h4,\n.db-content h5,\n.db-content h6 {\n font-weight: 600;\n line-height: 1.25;\n margin: 1.5em 0 0.5em;\n}\n\n/* Highlights a footnote/citation anchor when the URL hash lands on it, e.g. #fn-1. */\n.db-content :target {\n background: var(--db-target-bg, rgba(127, 127, 127, 0.15));\n}\n\n.db-content h1 {\n font-size: 2.25em;\n font-weight: 700;\n line-height: 1.2;\n letter-spacing: -0.025em;\n margin-top: 1.25em;\n}\n\n.db-content h2 {\n font-size: 1.5em;\n line-height: 1.25;\n letter-spacing: -0.02em;\n margin-top: 1.75em;\n}\n\n.db-content h3 {\n font-size: 1.25em;\n line-height: 1.3;\n}\n\n.db-content h4 {\n font-size: 1.125em;\n line-height: 1.35;\n}\n\n.db-content h5 {\n font-size: 1em;\n line-height: 1.4;\n}\n\n.db-content h6 {\n font-size: 0.875em;\n line-height: 1.4;\n}\n\n@media (max-width: 640px) {\n .db-content h1 {\n font-size: 1.875em;\n }\n .db-content h2 {\n font-size: 1.25em;\n }\n .db-content h3 {\n font-size: 1.125em;\n }\n}\n\n.db-content p,\n.db-content ul,\n.db-content ol,\n.db-content blockquote,\n.db-content pre,\n.db-content table {\n margin: 0 0 1em;\n}\n\n.db-content ul,\n.db-content ol {\n padding-left: 1.5em;\n}\n\n.db-content ul {\n list-style-type: disc;\n}\n\n.db-content ol {\n list-style-type: decimal;\n}\n\n.db-content ul ul {\n list-style-type: circle;\n}\n\n.db-content ol ol {\n list-style-type: lower-alpha;\n}\n\n.db-content ul ul ul,\n.db-content ol ol ol {\n list-style-type: square;\n}\n\n.db-content li {\n margin: 0.25em 0;\n}\n.db-content li > ul,\n.db-content li > ol {\n margin: 0.25em 0 0;\n}\n.db-content li > p {\n margin: 0;\n}\n\n.db-content ul.contains-task-list {\n list-style: none;\n padding-left: 0.25em;\n}\n.db-content li.task-list-item {\n list-style: none;\n margin-left: 0;\n}\n.db-content li.task-list-item input[type=\"checkbox\"] {\n margin-right: 0.5em;\n}\n\n.db-content strong {\n font-weight: 600;\n}\n.db-content em {\n font-style: italic;\n}\n.db-content del {\n text-decoration: line-through;\n opacity: 0.75;\n}\n\n.db-content blockquote {\n margin-left: 0;\n padding-left: 1em;\n border-left: 3px solid var(--db-accent-border, currentColor);\n opacity: 0.85;\n}\n\n.db-content code {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 0.9em;\n padding: 0.15em 0.35em;\n background: rgba(127, 127, 127, 0.15);\n border-radius: 4px;\n}\n\n.db-content pre {\n padding: 1em;\n overflow-x: auto;\n background: rgba(127, 127, 127, 0.1);\n border-radius: 8px;\n}\n.db-content pre code {\n padding: 0;\n background: none;\n}\n\n/* display:block so wide tables scroll instead of overflowing the page on mobile;\n costs full-bleed stretch for narrow tables. */\n.db-content table {\n border-collapse: collapse;\n display: block;\n width: max-content;\n max-width: 100%;\n overflow-x: auto;\n}\n\n.db-content th,\n.db-content td {\n border: 1px solid rgba(127, 127, 127, 0.3);\n padding: 0.5em 0.75em;\n text-align: left;\n}\n\n.db-content th {\n background: rgba(127, 127, 127, 0.08);\n font-weight: 600;\n}\n\n.db-content img {\n max-width: 100%;\n height: auto;\n border-radius: 8px;\n}\n\n.db-content a {\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n.db-content a:hover {\n opacity: 0.8;\n}\n\n.db-content hr {\n border: none;\n border-top: 1px solid rgba(127, 127, 127, 0.3);\n margin: 2em 0;\n}\n";
|
package/dist/styles.css
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/* Slim default typography for Draftbase MDX content.
|
|
2
|
-
Opt-in: import "@draftbase/
|
|
3
|
-
Disable per-render: <MDXContent unstyled
|
|
4
|
-
|
|
2
|
+
Opt-in: import "@draftbase/renderer/styles.css".
|
|
3
|
+
Disable per-render: <MDXContent unstyled /> or toHtml(source, { unstyled: true }).
|
|
4
|
+
Theme via custom properties: --db-heading-color, --db-link-color, --db-accent-border,
|
|
5
|
+
--db-target-bg. Override further: target ".db-content" with higher-specificity or
|
|
6
|
+
later-loaded rules. */
|
|
5
7
|
|
|
6
8
|
.db-content {
|
|
7
9
|
line-height: 1.6;
|
|
@@ -9,6 +11,19 @@
|
|
|
9
11
|
overflow-wrap: break-word;
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
.db-content h1,
|
|
15
|
+
.db-content h2,
|
|
16
|
+
.db-content h3,
|
|
17
|
+
.db-content h4,
|
|
18
|
+
.db-content h5,
|
|
19
|
+
.db-content h6 {
|
|
20
|
+
color: var(--db-heading-color, inherit);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.db-content a {
|
|
24
|
+
color: var(--db-link-color, inherit);
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
/* remark-gfm emits the footnotes label as .sr-only; define it so it stays hidden
|
|
13
28
|
without the consumer needing Tailwind or their own utility. */
|
|
14
29
|
.db-content .sr-only {
|
|
@@ -38,7 +53,6 @@
|
|
|
38
53
|
font-weight: 600;
|
|
39
54
|
line-height: 1.25;
|
|
40
55
|
margin: 1.5em 0 0.5em;
|
|
41
|
-
color: inherit;
|
|
42
56
|
}
|
|
43
57
|
|
|
44
58
|
/* Highlights a footnote/citation anchor when the URL hash lands on it, e.g. #fn-1. */
|
|
@@ -165,7 +179,7 @@
|
|
|
165
179
|
.db-content blockquote {
|
|
166
180
|
margin-left: 0;
|
|
167
181
|
padding-left: 1em;
|
|
168
|
-
border-left: 3px solid currentColor;
|
|
182
|
+
border-left: 3px solid var(--db-accent-border, currentColor);
|
|
169
183
|
opacity: 0.85;
|
|
170
184
|
}
|
|
171
185
|
|
|
@@ -217,7 +231,6 @@
|
|
|
217
231
|
}
|
|
218
232
|
|
|
219
233
|
.db-content a {
|
|
220
|
-
color: inherit;
|
|
221
234
|
text-decoration: underline;
|
|
222
235
|
text-underline-offset: 2px;
|
|
223
236
|
}
|
package/dist/toHtml.d.ts
CHANGED
|
@@ -11,7 +11,19 @@ export interface ToHtmlOptions {
|
|
|
11
11
|
target?: string;
|
|
12
12
|
rel?: string;
|
|
13
13
|
};
|
|
14
|
+
/** Skip wrapping the output in a `db-content`-classed `<div>` and the inlined default
|
|
15
|
+
* styles (see below). Same semantics as `MDXContent`'s `unstyled` prop. */
|
|
16
|
+
unstyled?: boolean;
|
|
17
|
+
/** Extra class name(s) merged onto the wrapper `<div>`. */
|
|
18
|
+
className?: string;
|
|
19
|
+
/** Called (in addition to `console.error`) when a custom component (from `components`) throws
|
|
20
|
+
* while rendering. The tag is left as literal HTML instead of failing the whole render. */
|
|
21
|
+
onError?: (error: unknown, tagName: string) => void;
|
|
14
22
|
}
|
|
15
|
-
/** Renders MDX/markdown to a static HTML string
|
|
16
|
-
*
|
|
23
|
+
/** Renders MDX/markdown to a static, self-contained HTML string: pre-wrapped in
|
|
24
|
+
* `<div class="db-content">` with the default styles inlined as a `<style>` tag ahead of it —
|
|
25
|
+
* no separate `styles.css` import needed. Calling `toHtml` more than once on the same page
|
|
26
|
+
* repeats that `<style>` tag (harmless, just redundant bytes); pass `unstyled: true` and load
|
|
27
|
+
* `@draftbase/renderer/styles.css` yourself once if that matters. No React, no mounted tree —
|
|
28
|
+
* use `compileMDX`/`MDXContent` instead when you need a React tree. */
|
|
17
29
|
export declare function toHtml(source: string, options?: ToHtmlOptions): Promise<string>;
|
package/dist/toHtml.js
CHANGED
|
@@ -6,6 +6,8 @@ import rehypeRaw from "rehype-raw";
|
|
|
6
6
|
import rehypeSlug from "rehype-slug";
|
|
7
7
|
import rehypeStringify from "rehype-stringify";
|
|
8
8
|
import { toHtml as hastToHtml } from "hast-util-to-html";
|
|
9
|
+
import { wrapperClassName } from "./wrapperClassName.js";
|
|
10
|
+
import { CSS_TEXT } from "./cssText.js";
|
|
9
11
|
const SCHEME_HREF = /^[a-z][a-z0-9+.-]*:/i;
|
|
10
12
|
const defaultEntryLink = (props, childrenHtml) => `<a href="/entries/${props.id ?? ""}">${childrenHtml}</a>`;
|
|
11
13
|
function hastPropsToStrings(properties) {
|
|
@@ -28,16 +30,22 @@ function rehypeDraftbase(options) {
|
|
|
28
30
|
transform(child);
|
|
29
31
|
if (node.type !== "element" || !node.tagName)
|
|
30
32
|
return;
|
|
31
|
-
const
|
|
33
|
+
const tagName = node.tagName;
|
|
34
|
+
const render = componentsByTag?.get(tagName);
|
|
32
35
|
if (render) {
|
|
33
|
-
|
|
34
|
-
allowDangerousHtml: true
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
try {
|
|
37
|
+
const childrenHtml = hastToHtml({ type: "root", children: node.children ?? [] }, { allowDangerousHtml: true });
|
|
38
|
+
node.value = render(hastPropsToStrings(node.properties), childrenHtml);
|
|
39
|
+
node.type = "raw";
|
|
40
|
+
node.tagName = undefined;
|
|
41
|
+
node.properties = undefined;
|
|
42
|
+
node.children = undefined;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
// Leave the tag as literal HTML instead of failing the whole render.
|
|
46
|
+
console.error(`Draftbase component "${tagName}" failed to render`, error);
|
|
47
|
+
options.onError?.(error, tagName);
|
|
48
|
+
}
|
|
41
49
|
return;
|
|
42
50
|
}
|
|
43
51
|
if (node.tagName === "a" && options.externalLinks) {
|
|
@@ -55,8 +63,12 @@ function rehypeDraftbase(options) {
|
|
|
55
63
|
transform(tree);
|
|
56
64
|
};
|
|
57
65
|
}
|
|
58
|
-
/** Renders MDX/markdown to a static HTML string
|
|
59
|
-
*
|
|
66
|
+
/** Renders MDX/markdown to a static, self-contained HTML string: pre-wrapped in
|
|
67
|
+
* `<div class="db-content">` with the default styles inlined as a `<style>` tag ahead of it —
|
|
68
|
+
* no separate `styles.css` import needed. Calling `toHtml` more than once on the same page
|
|
69
|
+
* repeats that `<style>` tag (harmless, just redundant bytes); pass `unstyled: true` and load
|
|
70
|
+
* `@draftbase/renderer/styles.css` yourself once if that matters. No React, no mounted tree —
|
|
71
|
+
* use `compileMDX`/`MDXContent` instead when you need a React tree. */
|
|
60
72
|
export async function toHtml(source, options = {}) {
|
|
61
73
|
const needsRawParse = Boolean(options.components);
|
|
62
74
|
const processor = unified()
|
|
@@ -70,5 +82,11 @@ export async function toHtml(source, options = {}) {
|
|
|
70
82
|
.use(rehypeDraftbase, options)
|
|
71
83
|
.use(rehypeStringify, { allowDangerousHtml: true });
|
|
72
84
|
const file = await processor.process(source);
|
|
73
|
-
|
|
85
|
+
const html = String(file);
|
|
86
|
+
const wrapperClass = wrapperClassName(options.unstyled, options.className);
|
|
87
|
+
if (!wrapperClass)
|
|
88
|
+
return html;
|
|
89
|
+
return options.unstyled
|
|
90
|
+
? `<div class="${wrapperClass}">${html}</div>`
|
|
91
|
+
: `<style>${CSS_TEXT}</style><div class="${wrapperClass}">${html}</div>`;
|
|
74
92
|
}
|
package/dist/toHtml.test.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { test } from "node:test";
|
|
3
3
|
import { toHtml } from "./toHtml.js";
|
|
4
|
+
import { CSS_TEXT } from "./cssText.js";
|
|
4
5
|
test("renders markdown to html", async () => {
|
|
5
6
|
const html = await toHtml("# Title\n\nSome **bold** text.");
|
|
6
7
|
assert.match(html, /<h1 id="title">Title<\/h1>/);
|
|
7
8
|
assert.match(html, /<strong>bold<\/strong>/);
|
|
8
9
|
});
|
|
10
|
+
test("wraps output in a db-content div with inlined styles by default", async () => {
|
|
11
|
+
const html = await toHtml("hi");
|
|
12
|
+
assert.equal(html, `<style>${CSS_TEXT}</style><div class="db-content"><p>hi</p></div>`);
|
|
13
|
+
});
|
|
14
|
+
test("unstyled skips the wrapper div and the inlined styles", async () => {
|
|
15
|
+
const html = await toHtml("hi", { unstyled: true });
|
|
16
|
+
assert.equal(html, "<p>hi</p>");
|
|
17
|
+
});
|
|
18
|
+
test("className merges onto the wrapper div, styles still inlined", async () => {
|
|
19
|
+
const html = await toHtml("hi", { className: "prose" });
|
|
20
|
+
assert.equal(html, `<style>${CSS_TEXT}</style><div class="db-content prose"><p>hi</p></div>`);
|
|
21
|
+
});
|
|
9
22
|
test("supports gfm tables", async () => {
|
|
10
23
|
const html = await toHtml("| a | b |\n| - | - |\n| 1 | 2 |");
|
|
11
24
|
assert.match(html, /<table>/);
|
|
@@ -43,6 +56,21 @@ test("components resolves nested custom tags before serializing the parent's chi
|
|
|
43
56
|
});
|
|
44
57
|
assert.match(html, /<div><a href="\/entries\/abc">Post<\/a><\/div>/);
|
|
45
58
|
});
|
|
59
|
+
test("a component that throws leaves the tag as literal HTML and calls onError", async () => {
|
|
60
|
+
const errors = [];
|
|
61
|
+
const html = await toHtml('<Callout type="warning">Careful</Callout>', {
|
|
62
|
+
components: {
|
|
63
|
+
Callout: () => {
|
|
64
|
+
throw new Error("boom");
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
onError: (error, tagName) => errors.push([error, tagName]),
|
|
68
|
+
});
|
|
69
|
+
assert.match(html, /<callout type="warning">Careful<\/callout>/);
|
|
70
|
+
assert.equal(errors.length, 1);
|
|
71
|
+
assert.equal(errors[0][0].message, "boom");
|
|
72
|
+
assert.equal(errors[0][1], "callout");
|
|
73
|
+
});
|
|
46
74
|
test("adds target/rel to external links when externalLinks is true", async () => {
|
|
47
75
|
const html = await toHtml("[ext](https://example.com) and [rel](/local)", {
|
|
48
76
|
externalLinks: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@draftbase/renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"README.md"
|
|
41
41
|
],
|
|
42
42
|
"scripts": {
|
|
43
|
-
"build": "tsc -p tsconfig.json && cp src/styles.css dist/styles.css",
|
|
44
|
-
"lint": "tsc --noEmit",
|
|
43
|
+
"build": "node scripts/generate-css-text.mjs && tsc -p tsconfig.json && cp src/styles.css dist/styles.css",
|
|
44
|
+
"lint": "node scripts/generate-css-text.mjs && tsc --noEmit",
|
|
45
45
|
"test": "node --test dist/wrapperClassName.test.js dist/toHtml.test.js dist/compileMDX.test.js dist/reactNativeComponents.test.js dist/reactNative.test.js dist/MDXContent.test.js"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|