@draftbase/renderer 0.4.1 → 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/README.md +25 -0
- package/dist/MDXContent.d.ts +12 -13
- package/dist/MDXContent.js +13 -12
- package/dist/MDXContent.test.js +20 -7
- package/dist/MDXErrorBoundary.d.ts +1 -0
- package/dist/MDXErrorBoundary.js +3 -4
- package/dist/compileMDX.test.js +2 -4
- package/dist/core.d.ts +4 -11
- package/dist/core.js +4 -11
- package/dist/cssText.d.ts +1 -0
- package/dist/cssText.js +2 -0
- package/dist/reactNative.d.ts +2 -15
- package/dist/reactNative.js +34 -16
- package/dist/reactNative.test.js +2 -3
- package/dist/reactNativeComponents.d.ts +8 -21
- package/dist/reactNativeComponents.js +18 -9
- package/dist/reactNativeComponents.test.js +2 -3
- package/dist/reactNativeStyles.d.ts +2 -3
- package/dist/reactNativeStyles.js +2 -3
- package/dist/styles.css +19 -6
- package/dist/toHtml.d.ts +29 -7
- package/dist/toHtml.js +82 -12
- package/dist/toHtml.test.js +68 -0
- package/dist/vue.d.ts +2 -4
- package/dist/vue.js +2 -4
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -232,6 +232,31 @@ import { toHtml } from "@draftbase/renderer";
|
|
|
232
232
|
const html = await toHtml(entry.fields.body);
|
|
233
233
|
```
|
|
234
234
|
|
|
235
|
+
By default, custom tags in the source (`<Callout>`, `<EntryLink>`, ...) pass through as literal HTML — `toHtml` produces a plain string, not a React tree, so there's no component tree to substitute into. Pass `components` to render one to an HTML string instead, keyed by tag name (case-insensitive):
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
const html = await toHtml(entry.fields.body, {
|
|
239
|
+
components: {
|
|
240
|
+
Callout: (props, childrenHtml) => `<div class="callout-${props.type}">${childrenHtml}</div>`,
|
|
241
|
+
},
|
|
242
|
+
externalLinks: true, // adds target="_blank" rel="noopener noreferrer" to off-site <a> tags
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Unlike a React `components` map, each prop always arrives as a string — real HTML parsing, not JSX evaluation, so `count="3"` is `"3"`, never `3`. `childrenHtml` is the tag's contents already rendered to HTML, with any nested custom tags resolved first.
|
|
247
|
+
|
|
248
|
+
`EntryLink` gets a default (`<a href="/entries/{id}">`) the moment `components` is passed at all, same as the React renderer's default — override it the same way as any other tag:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
const html = await toHtml(entry.fields.body, {
|
|
252
|
+
components: {
|
|
253
|
+
EntryLink: (props, childrenHtml) => `<a href="/blog/${props.id}">${childrenHtml}</a>`,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Omit `components` entirely and every custom tag, `EntryLink` included, stays literal HTML (previous behavior); omit `externalLinks` and links are left as-is.
|
|
259
|
+
|
|
235
260
|
## Custom components
|
|
236
261
|
|
|
237
262
|
Content can invoke JSX components by name inside the MDX source (e.g. `<Callout type="warning">...</Callout>`). Pass the implementations:
|
package/dist/MDXContent.d.ts
CHANGED
|
@@ -9,21 +9,17 @@ export interface CompiledMDX {
|
|
|
9
9
|
}
|
|
10
10
|
export type { FailedMDX };
|
|
11
11
|
/**
|
|
12
|
-
* Compiles raw MDX/markdown into a renderable React component — no DOM assumptions,
|
|
13
|
-
*
|
|
14
|
-
* `MDXContent` component below does the latter). Standard markdown elements (`p`,
|
|
15
|
-
* `h1`, `table`, ...) render via real DOM tags with zero setup; only a JSX component
|
|
16
|
-
* with no built-in default (e.g. `<Callout>`) needs `components`.
|
|
12
|
+
* Compiles raw MDX/markdown into a renderable React component — no DOM assumptions, safe for client effects or RSC.
|
|
13
|
+
* Standard markdown elements render via real DOM tags with zero setup; only a JSX component with no built-in default needs `components`.
|
|
17
14
|
*/
|
|
18
15
|
export declare function compileMDX(source: string): Promise<CompiledMDX | FailedMDX>;
|
|
19
16
|
export interface MDXContentProps {
|
|
20
17
|
/** Raw MDX/markdown string, e.g. an entry's rich text field. */
|
|
21
18
|
source: string;
|
|
22
|
-
/** Custom components available by name inside the MDX source
|
|
23
|
-
*
|
|
24
|
-
* non-DOM renderers such as React Native, which have no intrinsic `div`/`p`/`a` tags. */
|
|
19
|
+
/** Custom components available by name inside the MDX source, and/or overrides for standard markdown elements —
|
|
20
|
+
* required on non-DOM renderers such as React Native, which have no intrinsic `div`/`p`/`a` tags. */
|
|
25
21
|
components?: MDXComponents;
|
|
26
|
-
/** Skip the default `db-content` styling class. */
|
|
22
|
+
/** Skip the default `db-content` styling class and its inlined styles. */
|
|
27
23
|
unstyled?: boolean;
|
|
28
24
|
/** Extra class name(s) merged onto the wrapper element. */
|
|
29
25
|
className?: string;
|
|
@@ -33,10 +29,13 @@ export interface MDXContentProps {
|
|
|
33
29
|
/** Element/component used to render the fallback plain-text output when `source` fails to
|
|
34
30
|
* compile as MDX. Defaults to `"p"`; pass React Native's `Text`. */
|
|
35
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;
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
|
-
* Next.js Server Component wrapper around {@link compileMDX}. For non-RSC React
|
|
39
|
-
*
|
|
40
|
-
* and render `Content` yourself — `await` inside a component body only works as an RSC.
|
|
38
|
+
* Next.js Server Component wrapper around {@link compileMDX}. For non-RSC React, call `compileMDX` directly instead —
|
|
39
|
+
* `await` inside a component body only works as an RSC.
|
|
41
40
|
*/
|
|
42
|
-
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,35 +1,36 @@
|
|
|
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
|
-
* Compiles raw MDX/markdown into a renderable React component — no DOM assumptions,
|
|
9
|
-
*
|
|
10
|
-
* `MDXContent` component below does the latter). Standard markdown elements (`p`,
|
|
11
|
-
* `h1`, `table`, ...) render via real DOM tags with zero setup; only a JSX component
|
|
12
|
-
* with no built-in default (e.g. `<Callout>`) needs `components`.
|
|
9
|
+
* Compiles raw MDX/markdown into a renderable React component — no DOM assumptions, safe for client effects or RSC.
|
|
10
|
+
* Standard markdown elements render via real DOM tags with zero setup; only a JSX component with no built-in default needs `components`.
|
|
13
11
|
*/
|
|
14
12
|
export async function compileMDX(source) {
|
|
15
13
|
return compileMDXCore(source, runtime, defaultComponents);
|
|
16
14
|
}
|
|
17
15
|
/**
|
|
18
|
-
* Next.js Server Component wrapper around {@link compileMDX}. For non-RSC React
|
|
19
|
-
*
|
|
20
|
-
* and render `Content` yourself — `await` inside a component body only works as an RSC.
|
|
16
|
+
* Next.js Server Component wrapper around {@link compileMDX}. For non-RSC React, call `compileMDX` directly instead —
|
|
17
|
+
* `await` inside a component body only works as an RSC.
|
|
21
18
|
*/
|
|
22
|
-
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, }) {
|
|
23
20
|
const Wrapper = wrapperTag;
|
|
24
21
|
const ErrorTag = errorTag;
|
|
25
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 }));
|
|
26
26
|
const compiled = await compileMDX(source);
|
|
27
27
|
if (!compiled.ok) {
|
|
28
28
|
// Source isn't valid MDX/JSX (e.g. stray `<`/`{` in prose) — fail soft instead
|
|
29
29
|
// of crashing the page; render it as plain text so the copy still shows.
|
|
30
30
|
console.error("MDX compile failed, rendering as plain text", compiled.error);
|
|
31
|
-
|
|
31
|
+
onError?.(compiled.error);
|
|
32
|
+
return (_jsxs(_Fragment, { children: [styles, _jsx(Wrapper, { className: wrapperClass, children: _jsx(ErrorTag, { children: source }) })] }));
|
|
32
33
|
}
|
|
33
34
|
const { Content } = compiled;
|
|
34
|
-
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 }) }) })] }));
|
|
35
36
|
}
|
package/dist/MDXContent.test.js
CHANGED
|
@@ -1,20 +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
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
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
|
+
}
|
|
7
10
|
test("MDXContent wraps compiled MDX in the styled wrapper element", async () => {
|
|
8
|
-
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);
|
|
9
15
|
assert.equal(element.type, "div");
|
|
10
16
|
assert.equal(element.props.className, "db-content");
|
|
11
17
|
});
|
|
12
|
-
test("unstyled drops the db-content class", async () => {
|
|
13
|
-
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);
|
|
14
26
|
assert.equal(element.props.className, undefined);
|
|
15
27
|
});
|
|
16
28
|
test("falls back to plain text when the source fails to compile as MDX", async () => {
|
|
17
|
-
const
|
|
29
|
+
const fragment = (await MDXContent({ source: "<broken" }));
|
|
30
|
+
const element = wrapperOf(fragment);
|
|
18
31
|
const fallback = element.props.children;
|
|
19
32
|
assert.equal(fallback.type, "p");
|
|
20
33
|
assert.equal(fallback.props.children, "<broken");
|
package/dist/MDXErrorBoundary.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Component } from "react";
|
|
3
|
-
// Catches render-time errors from compiled MDX content — most commonly a JSX component
|
|
4
|
-
//
|
|
5
|
-
// `components`. Without this, that throws "Element type is invalid" and crashes the whole
|
|
6
|
-
// page instead of just the content block.
|
|
3
|
+
// Catches render-time errors from compiled MDX content — most commonly a JSX component referenced in the
|
|
4
|
+
// source but missing from `components`, which otherwise throws and crashes the whole page.
|
|
7
5
|
export class MDXErrorBoundary extends Component {
|
|
8
6
|
state = { hasError: false };
|
|
9
7
|
static getDerivedStateFromError() {
|
|
@@ -11,6 +9,7 @@ export class MDXErrorBoundary extends Component {
|
|
|
11
9
|
}
|
|
12
10
|
componentDidCatch(error) {
|
|
13
11
|
console.error("MDX content failed to render", error);
|
|
12
|
+
this.props.onError?.(error);
|
|
14
13
|
}
|
|
15
14
|
render() {
|
|
16
15
|
return this.state.hasError ? this.props.fallback : this.props.children;
|
package/dist/compileMDX.test.js
CHANGED
|
@@ -3,10 +3,8 @@ import test from "node:test";
|
|
|
3
3
|
import { compileMDX as compileReactMDX } from "./MDXContent.js";
|
|
4
4
|
import { compileMDX as compileVueMDX } from "./vue.js";
|
|
5
5
|
const SOURCE = "# Hello\n\nWorld";
|
|
6
|
-
/**
|
|
7
|
-
*
|
|
8
|
-
* same "call it like a plain function" style as the rest of these tests (no renderer
|
|
9
|
-
* mounted). */
|
|
6
|
+
/** Content is wrapped to merge in default components (see core.ts) — unwrap by invoking each
|
|
7
|
+
* function-typed element until reaching real output (no renderer mounted). */
|
|
10
8
|
function resolve(element) {
|
|
11
9
|
return typeof element.type === "function"
|
|
12
10
|
? resolve(element.type(element.props))
|
package/dist/core.d.ts
CHANGED
|
@@ -9,19 +9,12 @@ export interface FailedMDX {
|
|
|
9
9
|
error: unknown;
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* Shared MDX-to-component evaluation
|
|
13
|
-
*
|
|
14
|
-
* supplies its own without duplicating the remark/rehype pipeline.
|
|
15
|
-
*
|
|
16
|
-
* `defaultComponents`, when given, are merged underneath whatever the caller passes
|
|
17
|
-
* as `Content`'s own `components` prop (caller always wins per-tag) — this is what
|
|
18
|
-
* lets a framework entry point ship working defaults (e.g. a default `EntryLink`,
|
|
19
|
-
* or React Native's Text/View mapping) without requiring a mapping up front.
|
|
12
|
+
* Shared MDX-to-component evaluation, parameterized by JSX runtime so each framework entry point avoids duplicating
|
|
13
|
+
* the remark/rehype pipeline. `defaultComponents`, when given, are merged underneath the caller's own `components` (caller always wins per-tag).
|
|
20
14
|
*/
|
|
21
15
|
export declare function compileMDXCore<TComponent>(source: string, jsxRuntime: JsxRuntime, defaultComponents?: Record<string, unknown>): Promise<CompiledMDX<TComponent> | FailedMDX>;
|
|
22
|
-
/** Default `EntryLink` for a given JSX runtime — renders `<a href="/entries/{id}"
|
|
23
|
-
*
|
|
24
|
-
* route entries differently still override it by passing `components.EntryLink`. */
|
|
16
|
+
/** Default `EntryLink` for a given JSX runtime — renders `<a href="/entries/{id}">`; apps that
|
|
17
|
+
* route entries differently override it by passing `components.EntryLink`. */
|
|
25
18
|
export declare function makeDefaultEntryLink(jsxRuntime: JsxRuntime): ({ id, children }: {
|
|
26
19
|
id?: string;
|
|
27
20
|
children?: unknown;
|
package/dist/core.js
CHANGED
|
@@ -2,14 +2,8 @@ import { evaluate } from "@mdx-js/mdx";
|
|
|
2
2
|
import remarkGfm from "remark-gfm";
|
|
3
3
|
import rehypeSlug from "rehype-slug";
|
|
4
4
|
/**
|
|
5
|
-
* Shared MDX-to-component evaluation
|
|
6
|
-
*
|
|
7
|
-
* supplies its own without duplicating the remark/rehype pipeline.
|
|
8
|
-
*
|
|
9
|
-
* `defaultComponents`, when given, are merged underneath whatever the caller passes
|
|
10
|
-
* as `Content`'s own `components` prop (caller always wins per-tag) — this is what
|
|
11
|
-
* lets a framework entry point ship working defaults (e.g. a default `EntryLink`,
|
|
12
|
-
* or React Native's Text/View mapping) without requiring a mapping up front.
|
|
5
|
+
* Shared MDX-to-component evaluation, parameterized by JSX runtime so each framework entry point avoids duplicating
|
|
6
|
+
* the remark/rehype pipeline. `defaultComponents`, when given, are merged underneath the caller's own `components` (caller always wins per-tag).
|
|
13
7
|
*/
|
|
14
8
|
export async function compileMDXCore(source, jsxRuntime, defaultComponents) {
|
|
15
9
|
try {
|
|
@@ -37,9 +31,8 @@ function withDefaultComponents(Content, defaults, jsxRuntime) {
|
|
|
37
31
|
});
|
|
38
32
|
};
|
|
39
33
|
}
|
|
40
|
-
/** Default `EntryLink` for a given JSX runtime — renders `<a href="/entries/{id}"
|
|
41
|
-
*
|
|
42
|
-
* route entries differently still override it by passing `components.EntryLink`. */
|
|
34
|
+
/** Default `EntryLink` for a given JSX runtime — renders `<a href="/entries/{id}">`; apps that
|
|
35
|
+
* route entries differently override it by passing `components.EntryLink`. */
|
|
43
36
|
export function makeDefaultEntryLink(jsxRuntime) {
|
|
44
37
|
return function EntryLink({ id, children }) {
|
|
45
38
|
return jsxRuntime.jsx("a", { href: id ? `/entries/${id}` : undefined, 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/reactNative.d.ts
CHANGED
|
@@ -10,21 +10,8 @@ export interface CompiledMDX {
|
|
|
10
10
|
}
|
|
11
11
|
export type { FailedMDX };
|
|
12
12
|
/**
|
|
13
|
-
* Wires up a React Native `compileMDX` with default Text/View/Image mappings for
|
|
14
|
-
*
|
|
15
|
-
* primitives once instead of mapping every tag on every call:
|
|
16
|
-
*
|
|
17
|
-
* ```ts
|
|
18
|
-
* import { Text, View, Image } from "react-native";
|
|
19
|
-
* import { createReactNativeRenderer } from "@draftbase/renderer/react-native";
|
|
20
|
-
*
|
|
21
|
-
* const { compileMDX } = createReactNativeRenderer({ Text, View, Image });
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
24
|
-
* Ships default styling that mirrors the web look (heading scale, link color, table
|
|
25
|
-
* layout, ...) — pass `styleOptions.unstyled` to drop it, or `styleOptions.styles` to
|
|
26
|
-
* override specific tags (`{ h1: { fontSize: 32 } }`). Pass `components` on the
|
|
27
|
-
* returned `Content` to override individual tags' component, same as the web entry point.
|
|
13
|
+
* Wires up a React Native `compileMDX` with default Text/View/Image mappings for every standard markdown element
|
|
14
|
+
* (and `EntryLink`), so a project sets up its RN primitives once instead of mapping every tag on every call.
|
|
28
15
|
*/
|
|
29
16
|
export declare function createReactNativeRenderer(primitives: ReactNativePrimitives, styleOptions?: ReactNativeStyleOptions): {
|
|
30
17
|
compileMDX: (source: string) => Promise<CompiledMDX | FailedMDX>;
|
package/dist/reactNative.js
CHANGED
|
@@ -2,26 +2,44 @@ import * as runtime from "react/jsx-runtime";
|
|
|
2
2
|
import { compileMDXCore } from "./core.js";
|
|
3
3
|
import { buildReactNativeComponents, } from "./reactNativeComponents.js";
|
|
4
4
|
/**
|
|
5
|
-
* Wires up a React Native `compileMDX` with default Text/View/Image mappings for
|
|
6
|
-
*
|
|
7
|
-
* primitives once instead of mapping every tag on every call:
|
|
8
|
-
*
|
|
9
|
-
* ```ts
|
|
10
|
-
* import { Text, View, Image } from "react-native";
|
|
11
|
-
* import { createReactNativeRenderer } from "@draftbase/renderer/react-native";
|
|
12
|
-
*
|
|
13
|
-
* const { compileMDX } = createReactNativeRenderer({ Text, View, Image });
|
|
14
|
-
* ```
|
|
15
|
-
*
|
|
16
|
-
* Ships default styling that mirrors the web look (heading scale, link color, table
|
|
17
|
-
* layout, ...) — pass `styleOptions.unstyled` to drop it, or `styleOptions.styles` to
|
|
18
|
-
* override specific tags (`{ h1: { fontSize: 32 } }`). Pass `components` on the
|
|
19
|
-
* returned `Content` to override individual tags' component, same as the web entry point.
|
|
5
|
+
* Wires up a React Native `compileMDX` with default Text/View/Image mappings for every standard markdown element
|
|
6
|
+
* (and `EntryLink`), so a project sets up its RN primitives once instead of mapping every tag on every call.
|
|
20
7
|
*/
|
|
21
8
|
export function createReactNativeRenderer(primitives, styleOptions) {
|
|
22
9
|
const defaultComponents = buildReactNativeComponents(primitives, styleOptions);
|
|
23
10
|
async function compileMDX(source) {
|
|
24
|
-
|
|
11
|
+
const result = await compileMDXCore(source, runtime, defaultComponents);
|
|
12
|
+
if (!result.ok)
|
|
13
|
+
return result;
|
|
14
|
+
// MDX bakes the source's literal inter-block whitespace into the compiled output as bare
|
|
15
|
+
// "\n" string children of the root Fragment (e.g. between a heading and the paragraph after
|
|
16
|
+
// it). React Native throws on a bare string child that isn't inside a <Text>, so those need
|
|
17
|
+
// stripping here — element-mapped tags already handle it themselves (see reactNativeComponents.ts).
|
|
18
|
+
const Content = result.Content;
|
|
19
|
+
// withDefaultComponents (core.ts) wraps the compiled MDX component in one props-forwarding
|
|
20
|
+
// layer, so unwrap exactly that to reach the actual root element (a Fragment for a multi-block
|
|
21
|
+
// document, or a single mapped element like Text/View otherwise) before stripping whitespace.
|
|
22
|
+
// Calling any further would start invoking the RN primitives (Text/View/Image) themselves.
|
|
23
|
+
const RootWithoutWhitespace = (props) => {
|
|
24
|
+
const wrapped = Content(props);
|
|
25
|
+
const rootElement = typeof wrapped.type === "function"
|
|
26
|
+
? wrapped.type(wrapped.props)
|
|
27
|
+
: wrapped;
|
|
28
|
+
return stripWhitespaceRootChildren(rootElement);
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
ok: true,
|
|
32
|
+
Content: RootWithoutWhitespace,
|
|
33
|
+
};
|
|
25
34
|
}
|
|
26
35
|
return { compileMDX };
|
|
27
36
|
}
|
|
37
|
+
function stripWhitespaceRootChildren(element) {
|
|
38
|
+
const children = element.props?.children;
|
|
39
|
+
if (!Array.isArray(children))
|
|
40
|
+
return element;
|
|
41
|
+
const filtered = children.filter((child) => typeof child !== "string" || child.trim() !== "");
|
|
42
|
+
if (filtered.length === children.length)
|
|
43
|
+
return element;
|
|
44
|
+
return runtime.jsxs(element.type, { ...element.props, children: filtered }, element.key ?? undefined);
|
|
45
|
+
}
|
package/dist/reactNative.test.js
CHANGED
|
@@ -6,9 +6,8 @@ const Text = () => null;
|
|
|
6
6
|
const View = () => null;
|
|
7
7
|
const Image = () => null;
|
|
8
8
|
const LEAVES = [Text, View, Image];
|
|
9
|
-
/** Unwraps nested function-component elements
|
|
10
|
-
*
|
|
11
|
-
* into the primitive itself (Text/View/Image are opaque leaves here, not JSX). */
|
|
9
|
+
/** Unwraps nested function-component elements down to the RN primitive actually used,
|
|
10
|
+
* without calling into it (Text/View/Image are opaque leaves here, not JSX). */
|
|
12
11
|
function resolve(element) {
|
|
13
12
|
return typeof element.type === "function" && !LEAVES.includes(element.type)
|
|
14
13
|
? resolve(element.type(element.props))
|
|
@@ -25,12 +25,8 @@ export interface ReactNativeStyleOptions {
|
|
|
25
25
|
styles?: Partial<Record<string, Record<string, unknown>>>;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
* Default MDX `components` map for React Native, built from its Text/View/Image
|
|
29
|
-
*
|
|
30
|
-
* does, so every standard markdown element needs an explicit mapping — this covers
|
|
31
|
-
* the common ones, styled to match the web default look (heading scale, link color,
|
|
32
|
-
* table layout, ...), so a project only wires up the three RN primitives once (via
|
|
33
|
-
* `createReactNativeRenderer`) instead of mapping and styling every tag itself.
|
|
28
|
+
* Default MDX `components` map for React Native, built from its Text/View/Image primitives. RN has no intrinsic
|
|
29
|
+
* host tags for `p`/`h1`/`a`/etc the way web React does, so every standard markdown element needs an explicit mapping.
|
|
34
30
|
*/
|
|
35
31
|
export declare function buildReactNativeComponents({ Text, View, Image }: ReactNativePrimitives, { unstyled, styles }?: ReactNativeStyleOptions): {
|
|
36
32
|
p: ComponentType<{
|
|
@@ -138,27 +134,18 @@ export declare function buildReactNativeComponents({ Text, View, Image }: ReactN
|
|
|
138
134
|
children?: ReactNode;
|
|
139
135
|
style?: unknown;
|
|
140
136
|
}) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
|
|
141
|
-
ul:
|
|
137
|
+
ul: (props: {
|
|
142
138
|
children?: ReactNode;
|
|
143
139
|
style?: unknown;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
style?: unknown;
|
|
147
|
-
}) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
|
|
148
|
-
ol: ComponentType<{
|
|
149
|
-
children?: ReactNode;
|
|
150
|
-
style?: unknown;
|
|
151
|
-
}> | ((props: {
|
|
152
|
-
children?: ReactNode;
|
|
153
|
-
style?: unknown;
|
|
154
|
-
}) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
|
|
155
|
-
blockquote: ComponentType<{
|
|
140
|
+
}) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
141
|
+
ol: (props: {
|
|
156
142
|
children?: ReactNode;
|
|
157
143
|
style?: unknown;
|
|
158
|
-
}
|
|
144
|
+
}) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
145
|
+
blockquote: (props: {
|
|
159
146
|
children?: ReactNode;
|
|
160
147
|
style?: unknown;
|
|
161
|
-
}) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any
|
|
148
|
+
}) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
162
149
|
hr: ComponentType<{
|
|
163
150
|
children?: ReactNode;
|
|
164
151
|
style?: unknown;
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import * as runtime from "react/jsx-runtime";
|
|
2
2
|
import { defaultReactNativeStyles } from "./reactNativeStyles.js";
|
|
3
3
|
/**
|
|
4
|
-
* Default MDX `components` map for React Native, built from its Text/View/Image
|
|
5
|
-
*
|
|
6
|
-
* does, so every standard markdown element needs an explicit mapping — this covers
|
|
7
|
-
* the common ones, styled to match the web default look (heading scale, link color,
|
|
8
|
-
* table layout, ...), so a project only wires up the three RN primitives once (via
|
|
9
|
-
* `createReactNativeRenderer`) instead of mapping and styling every tag itself.
|
|
4
|
+
* Default MDX `components` map for React Native, built from its Text/View/Image primitives. RN has no intrinsic
|
|
5
|
+
* host tags for `p`/`h1`/`a`/etc the way web React does, so every standard markdown element needs an explicit mapping.
|
|
10
6
|
*/
|
|
11
7
|
export function buildReactNativeComponents({ Text, View, Image }, { unstyled, styles } = {}) {
|
|
12
8
|
const styleFor = (tag) => unstyled ? styles?.[tag] : { ...defaultReactNativeStyles[tag], ...styles?.[tag] };
|
|
@@ -16,6 +12,19 @@ export function buildReactNativeComponents({ Text, View, Image }, { unstyled, st
|
|
|
16
12
|
return Base;
|
|
17
13
|
return (props) => runtime.jsx(Base, { ...props, style: props.style ? [style, props.style] : style });
|
|
18
14
|
}
|
|
15
|
+
// MDX bakes the source's literal formatting whitespace ("\n" between block elements) into the
|
|
16
|
+
// compiled JSX as string children, even though structurally these tags never hold real text.
|
|
17
|
+
// React Native's Text/View don't tolerate a bare string child of View, so container tags need it
|
|
18
|
+
// stripped — Text-mapped tags (p, li, td, ...) keep it, since inline whitespace there is real content.
|
|
19
|
+
function stripWhitespaceChildren(children) {
|
|
20
|
+
if (Array.isArray(children))
|
|
21
|
+
return children.filter((child) => typeof child !== "string" || child.trim() !== "");
|
|
22
|
+
return typeof children === "string" && children.trim() === "" ? undefined : children;
|
|
23
|
+
}
|
|
24
|
+
function container(Base, tag) {
|
|
25
|
+
const Styled = styled(Base, tag);
|
|
26
|
+
return (props) => runtime.jsx(Styled, { ...props, children: stripWhitespaceChildren(props.children) });
|
|
27
|
+
}
|
|
19
28
|
const Img = ({ src, alt }) => runtime.jsx(Image, {
|
|
20
29
|
source: { uri: src },
|
|
21
30
|
accessibilityLabel: alt,
|
|
@@ -38,9 +47,9 @@ export function buildReactNativeComponents({ Text, View, Image }, { unstyled, st
|
|
|
38
47
|
li: styled(Text, "li"),
|
|
39
48
|
th: styled(Text, "th"),
|
|
40
49
|
td: styled(Text, "td"),
|
|
41
|
-
ul:
|
|
42
|
-
ol:
|
|
43
|
-
blockquote:
|
|
50
|
+
ul: container(View, "ul"),
|
|
51
|
+
ol: container(View, "ol"),
|
|
52
|
+
blockquote: container(View, "blockquote"),
|
|
44
53
|
hr: styled(View, "hr"),
|
|
45
54
|
table: styled(View, "table"),
|
|
46
55
|
thead: View,
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { buildReactNativeComponents } from "./reactNativeComponents.js";
|
|
4
|
-
// Stand-ins for react-native's Text/View/Image — this package doesn't depend on
|
|
5
|
-
//
|
|
6
|
-
// logic is tested against fakes with the same shape.
|
|
4
|
+
// Stand-ins for react-native's Text/View/Image — this package doesn't depend on react-native itself
|
|
5
|
+
// (it only resolves inside Metro, not plain Node), so the mapping is tested against fakes with the same shape.
|
|
7
6
|
const Text = () => null;
|
|
8
7
|
const View = () => null;
|
|
9
8
|
const Image = () => null;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Default per-tag style objects for the React Native preset — mirrors `styles.css`'s
|
|
2
|
-
* web
|
|
3
|
-
* spacing) so RN output looks like the web default instead of unstyled plain text. */
|
|
1
|
+
/** Default per-tag style objects for the React Native preset — mirrors `styles.css`'s web defaults
|
|
2
|
+
* so RN output looks like the web default instead of unstyled plain text. */
|
|
4
3
|
export declare const defaultReactNativeStyles: Record<string, Record<string, unknown>>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
/** Default per-tag style objects for the React Native preset — mirrors `styles.css`'s
|
|
2
|
-
* web
|
|
3
|
-
* spacing) so RN output looks like the web default instead of unstyled plain text. */
|
|
1
|
+
/** Default per-tag style objects for the React Native preset — mirrors `styles.css`'s web defaults
|
|
2
|
+
* so RN output looks like the web default instead of unstyled plain text. */
|
|
4
3
|
export const defaultReactNativeStyles = {
|
|
5
4
|
p: { marginBottom: 12, lineHeight: 22 },
|
|
6
5
|
h1: { fontSize: 28, fontWeight: "700", marginTop: 20, marginBottom: 10 },
|
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
|
@@ -1,7 +1,29 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*/
|
|
7
|
-
|
|
1
|
+
/** Renders a custom JSX tag to an HTML string. `props` values are always strings (HTML parsing,
|
|
2
|
+
* not JSX). `childrenHtml` is the tag's contents, already rendered, nested tags resolved first. */
|
|
3
|
+
export type ToHtmlComponent = (props: Record<string, string>, childrenHtml: string) => string;
|
|
4
|
+
export interface ToHtmlOptions {
|
|
5
|
+
/** Renders custom tags (e.g. `<Callout>`, `<EntryLink id>`) by tag name, case-insensitive.
|
|
6
|
+
* `EntryLink` defaults to `<a href="/entries/{id}">` unless overridden. */
|
|
7
|
+
components?: Record<string, ToHtmlComponent>;
|
|
8
|
+
/** Adds `target`/`rel` to links whose `href` has a URL scheme (`https:`, `mailto:`, ...).
|
|
9
|
+
* `true` uses `target="_blank" rel="noopener noreferrer"`; pass an object to override. */
|
|
10
|
+
externalLinks?: boolean | {
|
|
11
|
+
target?: string;
|
|
12
|
+
rel?: string;
|
|
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;
|
|
22
|
+
}
|
|
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. */
|
|
29
|
+
export declare function toHtml(source: string, options?: ToHtmlOptions): Promise<string>;
|
package/dist/toHtml.js
CHANGED
|
@@ -2,21 +2,91 @@ import { unified } from "unified";
|
|
|
2
2
|
import remarkParse from "remark-parse";
|
|
3
3
|
import remarkGfm from "remark-gfm";
|
|
4
4
|
import remarkRehype from "remark-rehype";
|
|
5
|
+
import rehypeRaw from "rehype-raw";
|
|
5
6
|
import rehypeSlug from "rehype-slug";
|
|
6
7
|
import rehypeStringify from "rehype-stringify";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
8
|
+
import { toHtml as hastToHtml } from "hast-util-to-html";
|
|
9
|
+
import { wrapperClassName } from "./wrapperClassName.js";
|
|
10
|
+
import { CSS_TEXT } from "./cssText.js";
|
|
11
|
+
const SCHEME_HREF = /^[a-z][a-z0-9+.-]*:/i;
|
|
12
|
+
const defaultEntryLink = (props, childrenHtml) => `<a href="/entries/${props.id ?? ""}">${childrenHtml}</a>`;
|
|
13
|
+
function hastPropsToStrings(properties) {
|
|
14
|
+
const props = {};
|
|
15
|
+
for (const [key, value] of Object.entries(properties ?? {})) {
|
|
16
|
+
if (typeof value === "string")
|
|
17
|
+
props[key] = value;
|
|
18
|
+
else if (Array.isArray(value))
|
|
19
|
+
props[key] = value.join(" ");
|
|
20
|
+
}
|
|
21
|
+
return props;
|
|
22
|
+
}
|
|
23
|
+
function rehypeDraftbase(options) {
|
|
24
|
+
const componentsByTag = options.components
|
|
25
|
+
? new Map(Object.entries({ EntryLink: defaultEntryLink, ...options.components }).map(([name, render]) => [name.toLowerCase(), render]))
|
|
26
|
+
: undefined;
|
|
27
|
+
return (tree) => {
|
|
28
|
+
function transform(node) {
|
|
29
|
+
for (const child of node.children ?? [])
|
|
30
|
+
transform(child);
|
|
31
|
+
if (node.type !== "element" || !node.tagName)
|
|
32
|
+
return;
|
|
33
|
+
const tagName = node.tagName;
|
|
34
|
+
const render = componentsByTag?.get(tagName);
|
|
35
|
+
if (render) {
|
|
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
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (node.tagName === "a" && options.externalLinks) {
|
|
52
|
+
const href = node.properties?.href;
|
|
53
|
+
if (typeof href !== "string" || !SCHEME_HREF.test(href))
|
|
54
|
+
return;
|
|
55
|
+
const overrides = typeof options.externalLinks === "object" ? options.externalLinks : {};
|
|
56
|
+
node.properties = {
|
|
57
|
+
...node.properties,
|
|
58
|
+
target: overrides.target ?? "_blank",
|
|
59
|
+
rel: overrides.rel ?? "noopener noreferrer",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
transform(tree);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
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. */
|
|
72
|
+
export async function toHtml(source, options = {}) {
|
|
73
|
+
const needsRawParse = Boolean(options.components);
|
|
74
|
+
const processor = unified()
|
|
15
75
|
.use(remarkParse)
|
|
16
76
|
.use(remarkGfm)
|
|
17
|
-
.use(remarkRehype, { allowDangerousHtml: true })
|
|
77
|
+
.use(remarkRehype, { allowDangerousHtml: true });
|
|
78
|
+
if (needsRawParse)
|
|
79
|
+
processor.use(rehypeRaw);
|
|
80
|
+
processor
|
|
18
81
|
.use(rehypeSlug)
|
|
19
|
-
.use(
|
|
20
|
-
.
|
|
21
|
-
|
|
82
|
+
.use(rehypeDraftbase, options)
|
|
83
|
+
.use(rehypeStringify, { allowDangerousHtml: true });
|
|
84
|
+
const file = await processor.process(source);
|
|
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>`;
|
|
22
92
|
}
|
package/dist/toHtml.test.js
CHANGED
|
@@ -1,12 +1,80 @@
|
|
|
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>/);
|
|
12
25
|
});
|
|
26
|
+
test("leaves custom tags as literal HTML when components isn't passed", async () => {
|
|
27
|
+
const html = await toHtml('<EntryLink id="abc">Post</EntryLink><Callout>hi</Callout>');
|
|
28
|
+
assert.match(html, /<EntryLink id="abc">Post<\/EntryLink>/);
|
|
29
|
+
assert.match(html, /<Callout>hi<\/Callout>/);
|
|
30
|
+
});
|
|
31
|
+
test("EntryLink defaults to /entries/{id} once components is passed", async () => {
|
|
32
|
+
const html = await toHtml('<EntryLink id="abc">Post</EntryLink>', { components: {} });
|
|
33
|
+
assert.match(html, /<a href="\/entries\/abc">Post<\/a>/);
|
|
34
|
+
});
|
|
35
|
+
test("components renders a custom tag, keyed case-insensitively, with string props", async () => {
|
|
36
|
+
const html = await toHtml('<Callout type="warning">Careful</Callout>', {
|
|
37
|
+
components: {
|
|
38
|
+
Callout: (props, childrenHtml) => `<div class="callout-${props.type}">${childrenHtml}</div>`,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
assert.match(html, /<div class="callout-warning">Careful<\/div>/);
|
|
42
|
+
});
|
|
43
|
+
test("components overrides the default EntryLink", async () => {
|
|
44
|
+
const html = await toHtml('<EntryLink id="abc">Post</EntryLink>', {
|
|
45
|
+
components: {
|
|
46
|
+
EntryLink: (props, childrenHtml) => `<a href="/blog/${props.id}">${childrenHtml}</a>`,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
assert.match(html, /<a href="\/blog\/abc">Post<\/a>/);
|
|
50
|
+
});
|
|
51
|
+
test("components resolves nested custom tags before serializing the parent's children", async () => {
|
|
52
|
+
const html = await toHtml('<Callout><EntryLink id="abc">Post</EntryLink></Callout>', {
|
|
53
|
+
components: {
|
|
54
|
+
Callout: (_props, childrenHtml) => `<div>${childrenHtml}</div>`,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
assert.match(html, /<div><a href="\/entries\/abc">Post<\/a><\/div>/);
|
|
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
|
+
});
|
|
74
|
+
test("adds target/rel to external links when externalLinks is true", async () => {
|
|
75
|
+
const html = await toHtml("[ext](https://example.com) and [rel](/local)", {
|
|
76
|
+
externalLinks: true,
|
|
77
|
+
});
|
|
78
|
+
assert.match(html, /<a href="https:\/\/example\.com" target="_blank" rel="noopener noreferrer">ext<\/a>/);
|
|
79
|
+
assert.match(html, /<a href="\/local">rel<\/a>/);
|
|
80
|
+
});
|
package/dist/vue.d.ts
CHANGED
|
@@ -6,9 +6,7 @@ export interface CompiledMDX {
|
|
|
6
6
|
}
|
|
7
7
|
export type { FailedMDX };
|
|
8
8
|
/**
|
|
9
|
-
* Compiles raw MDX/markdown into a renderable Vue component. Vue has no RSC-style
|
|
10
|
-
*
|
|
11
|
-
* yourself: `h(Content, props)` or `<component :is="Content" />`. Standard markdown
|
|
12
|
-
* elements render via real DOM tags with zero setup.
|
|
9
|
+
* Compiles raw MDX/markdown into a renderable Vue component. Vue has no RSC-style async component, so call this
|
|
10
|
+
* from `setup()`/a composable and render the result yourself: `h(Content, props)`.
|
|
13
11
|
*/
|
|
14
12
|
export declare function compileMDX(source: string): Promise<CompiledMDX | FailedMDX>;
|
package/dist/vue.js
CHANGED
|
@@ -2,10 +2,8 @@ import { compileMDXCore, makeDefaultEntryLink } from "./core.js";
|
|
|
2
2
|
import { vueJsxRuntime } from "./vueRuntime.js";
|
|
3
3
|
const defaultComponents = { EntryLink: makeDefaultEntryLink(vueJsxRuntime) };
|
|
4
4
|
/**
|
|
5
|
-
* Compiles raw MDX/markdown into a renderable Vue component. Vue has no RSC-style
|
|
6
|
-
*
|
|
7
|
-
* yourself: `h(Content, props)` or `<component :is="Content" />`. Standard markdown
|
|
8
|
-
* elements render via real DOM tags with zero setup.
|
|
5
|
+
* Compiles raw MDX/markdown into a renderable Vue component. Vue has no RSC-style async component, so call this
|
|
6
|
+
* from `setup()`/a composable and render the result yourself: `h(Content, props)`.
|
|
9
7
|
*/
|
|
10
8
|
export async function compileMDX(source) {
|
|
11
9
|
return compileMDXCore(source, vueJsxRuntime, defaultComponents);
|
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": {
|
|
@@ -61,7 +61,9 @@
|
|
|
61
61
|
"remark-gfm": "^4.0.0",
|
|
62
62
|
"remark-parse": "^11.0.0",
|
|
63
63
|
"remark-rehype": "^11.1.2",
|
|
64
|
+
"rehype-raw": "^7.0.0",
|
|
64
65
|
"rehype-slug": "^6.0.0",
|
|
66
|
+
"hast-util-to-html": "^9.0.0",
|
|
65
67
|
"rehype-stringify": "^10.0.1",
|
|
66
68
|
"unified": "^11.0.5"
|
|
67
69
|
},
|