@draftbase/renderer 0.3.2 → 0.4.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 CHANGED
@@ -21,13 +21,16 @@ pnpm add @draftbase/renderer
21
21
 
22
22
  Every framework entry point exposes the **same API shape** — `compileMDX(source)` resolving to `{ ok: true, Content }` or `{ ok: false, error }` — so switching frameworks (or supporting several in one monorepo) means changing the import path, not the calling code:
23
23
 
24
- | Framework | Import | `Content` is a... |
25
- | ------------------------------------------------ | ----------------------------------------- | ------------------------------------------- |
26
- | React, Next.js, React Native, Remix, Astro, Vite | `@draftbase/renderer` | React component |
27
- | Vue | `@draftbase/renderer/vue` | Vue component |
28
- | Anything else (Svelte, plain HTML, email, RSS) | `toHtml` (below), from either entry point | (returns an HTML string, not a component) |
24
+ | Framework | Import | `Content` is a... |
25
+ | ---------------------------------------------- | ----------------------------------------- | ------------------------------------------- |
26
+ | React, Next.js, Remix, Astro, Vite | `@draftbase/renderer` | React component |
27
+ | React Native | `@draftbase/renderer/react-native` | React component |
28
+ | Vue, Nuxt | `@draftbase/renderer/vue` | Vue component |
29
+ | Anything else (Svelte, plain HTML, email, RSS) | `toHtml` (below), from either entry point | — (returns an HTML string, not a component) |
29
30
 
30
- Only import the entry point for the framework you use — each pulls in just that framework's peer dependency (React or Vue), never both, so an app using one never bundles code for the other.
31
+ Only import the entry point for the framework you use — each pulls in just that framework's peer dependency (React or Vue), never both, so an app using one never bundles code for the other. The `/react-native` entry point never imports the `react-native` package itself (see its section below), so a Vue or web-only React project never pulls it in either.
32
+
33
+ Every entry point renders standard markdown (`p`, `h1`-`h6`, tables, lists, links, ...) **with zero component mapping** — web React/Vue use real DOM elements automatically, and the React Native entry ships default Text/View/Image mappings (see below). You only ever need to pass `components` for content-specific custom JSX tags (e.g. `<Callout>`) that have no sensible default — and even `EntryLink` (the CMS's entry-link tag) has a default (`<a href="/entries/{id}">`) unless you override it.
31
34
 
32
35
  ## ⚛️ Next.js App Router
33
36
 
@@ -44,14 +47,51 @@ export default async function Page() {
44
47
 
45
48
  `MDXContent` is an `async` Server Component — it only works where React can await inside a component body (Next.js RSC).
46
49
 
47
- ## Other React (client-side web, React Native, Remix, ...)
50
+ ## Other React (client-side web, Remix, ...)
48
51
 
49
52
  Call `compileMDX` yourself from a loader/effect and render the result — no RSC required:
50
53
 
51
54
  ```tsx
52
55
  import { useEffect, useState } from "react";
53
56
  import { compileMDX } from "@draftbase/renderer";
57
+
58
+ function Entry({ source }: { source: string }) {
59
+ const [compiled, setCompiled] = useState<Awaited<ReturnType<typeof compileMDX>>>();
60
+
61
+ useEffect(() => {
62
+ compileMDX(source).then(setCompiled);
63
+ }, [source]);
64
+
65
+ if (!compiled) return null;
66
+ if (!compiled.ok) return <p>{source}</p>;
67
+
68
+ const { Content } = compiled;
69
+ return <Content />; // standard markdown renders as real DOM elements, no components map needed
70
+ }
71
+ ```
72
+
73
+ `compiled.ok` only catches MDX _syntax_ errors. If the source references a custom JSX component you didn't pass in `components` (e.g. `<Callout>` without a `Callout` implementation), React throws while rendering `<Content>` — wrap it in the exported `MDXErrorBoundary` to log it to the console and fail soft instead of crashing the page. `MDXContent` (the Next.js RSC helper above) already does this for you automatically.
74
+
75
+ ```tsx
76
+ import { MDXErrorBoundary } from "@draftbase/renderer";
77
+
78
+ <MDXErrorBoundary fallback={<p>{source}</p>}>
79
+ <Content />
80
+ </MDXErrorBoundary>;
81
+ ```
82
+
83
+ Extended markdown (tables, strikethrough, task lists, autolinks) is supported out of the box via `remark-gfm`.
84
+
85
+ ## React Native
86
+
87
+ RN has no intrinsic host tags for `p`/`h1`/`a`/etc the way web React has real DOM elements, so it can't render standard markdown for free the way the web entry point does. Instead of mapping every tag yourself, wire up RN's three core primitives **once** via `@draftbase/renderer/react-native` and every markdown element — including a styled default `EntryLink` — works out of the box from there:
88
+
89
+ ```tsx
90
+ import { useEffect, useState } from "react";
54
91
  import { View, Text } from "react-native";
92
+ import { createReactNativeRenderer } from "@draftbase/renderer/react-native";
93
+
94
+ const { compileMDX } = createReactNativeRenderer({ Text, View, Image });
55
95
 
56
96
  function Entry({ source }: { source: string }) {
57
97
  const [compiled, setCompiled] = useState<Awaited<ReturnType<typeof compileMDX>>>();
@@ -66,23 +106,25 @@ function Entry({ source }: { source: string }) {
66
106
  const { Content } = compiled;
67
107
  return (
68
108
  <View>
69
- <Content components={{ p: Text, h1: Text /* ... */ }} />
109
+ <Content /> {/* p, h1-h6, a, table, EntryLink, ... all render already-styled */}
70
110
  </View>
71
111
  );
72
112
  }
73
113
  ```
74
114
 
75
- `compiled.ok` only catches MDX _syntax_ errors. If the source references a JSX component you didn't pass in `components` (e.g. `<Callout>` without a `Callout` implementation), React throws while rendering `<Content>` wrap it in the exported `MDXErrorBoundary` (React only, not React Native's non-DOM tree unless you supply an `errorTag`-equivalent fallback) to log it to the console and fail soft instead of crashing the page. `MDXContent` (the Next.js RSC helper above) already does this for you automatically.
115
+ Default styling mirrors the web look (heading scale, blue underlined links, monospace code, table borders and row layout, ...). Override or disable it via a second argument:
76
116
 
77
117
  ```tsx
78
- import { MDXErrorBoundary } from "@draftbase/renderer";
118
+ // override specific tags
119
+ createReactNativeRenderer({ Text, View, Image }, { styles: { h1: { fontSize: 32 } } });
79
120
 
80
- <MDXErrorBoundary fallback={<Text>{source}</Text>}>
81
- <Content components={{ p: Text, h1: Text /* ... */ }} />
82
- </MDXErrorBoundary>;
121
+ // drop all default styling
122
+ createReactNativeRenderer({ Text, View, Image }, { unstyled: true });
83
123
  ```
84
124
 
85
- Extended markdown (tables, strikethrough, task lists, autolinks) is supported out of the box via `remark-gfm`.
125
+ `components` on the returned `Content` still works the same way, per-tag, for anything you want to swap out entirely (e.g. a tappable `EntryLink` that actually navigates — the default renders a plain styled `Text`, since navigation is app-specific).
126
+
127
+ This package never imports the `react-native` package itself — you pass its `Text`/`View`/`Image` in, so nothing RN-specific ends up in a web or Vue bundle.
86
128
 
87
129
  ## Astro
88
130
 
@@ -10,8 +10,10 @@ export interface CompiledMDX {
10
10
  export type { FailedMDX };
11
11
  /**
12
12
  * Compiles raw MDX/markdown into a renderable React component — no DOM assumptions,
13
- * safe to call from a React Native loader, a client-side effect, or a Next.js Server
14
- * Component (the `MDXContent` component below does the latter).
13
+ * safe to call from a client-side effect or a Next.js Server Component (the
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`.
15
17
  */
16
18
  export declare function compileMDX(source: string): Promise<CompiledMDX | FailedMDX>;
17
19
  export interface MDXContentProps {
@@ -1,15 +1,18 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import * as runtime from "react/jsx-runtime";
3
- import { compileMDXCore } from "./core.js";
3
+ import { compileMDXCore, makeDefaultEntryLink } from "./core.js";
4
4
  import { wrapperClassName } from "./wrapperClassName.js";
5
5
  import { MDXErrorBoundary } from "./MDXErrorBoundary.js";
6
+ const defaultComponents = { EntryLink: makeDefaultEntryLink(runtime) };
6
7
  /**
7
8
  * Compiles raw MDX/markdown into a renderable React component — no DOM assumptions,
8
- * safe to call from a React Native loader, a client-side effect, or a Next.js Server
9
- * Component (the `MDXContent` component below does the latter).
9
+ * safe to call from a client-side effect or a Next.js Server Component (the
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`.
10
13
  */
11
14
  export async function compileMDX(source) {
12
- return compileMDXCore(source, runtime);
15
+ return compileMDXCore(source, runtime, defaultComponents);
13
16
  }
14
17
  /**
15
18
  * Next.js Server Component wrapper around {@link compileMDX}. For non-RSC React (client-side
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { MDXContent } from "./MDXContent.js";
4
+ // Smoke test for the Next.js App Router / RSC entry point — MDXContent is an async
5
+ // Server Component; call it directly (as React's RSC runtime would) and inspect the
6
+ // element tree it returns, same style as compileMDX.test.ts.
7
+ test("MDXContent wraps compiled MDX in the styled wrapper element", async () => {
8
+ const element = (await MDXContent({ source: "# Hello" }));
9
+ assert.equal(element.type, "div");
10
+ assert.equal(element.props.className, "db-content");
11
+ });
12
+ test("unstyled drops the db-content class", async () => {
13
+ const element = (await MDXContent({ source: "# Hello", unstyled: true }));
14
+ assert.equal(element.props.className, undefined);
15
+ });
16
+ test("falls back to plain text when the source fails to compile as MDX", async () => {
17
+ const element = (await MDXContent({ source: "<broken" }));
18
+ const fallback = element.props.children;
19
+ assert.equal(fallback.type, "p");
20
+ assert.equal(fallback.props.children, "<broken");
21
+ });
@@ -3,14 +3,21 @@ 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
+ /** compileMDX's Content is wrapped to merge in default components (see core.ts) —
7
+ * unwrap by directly invoking each function-typed element until reaching real output,
8
+ * same "call it like a plain function" style as the rest of these tests (no renderer
9
+ * mounted). */
10
+ function resolve(element) {
11
+ return typeof element.type === "function"
12
+ ? resolve(element.type(element.props))
13
+ : element;
14
+ }
6
15
  test("react compileMDX evaluates MDX into a React element tree", async () => {
7
16
  const result = await compileReactMDX(SOURCE);
8
17
  assert.equal(result.ok, true);
9
18
  if (!result.ok)
10
19
  return;
11
- assert.equal(typeof result.Content, "function");
12
- const render = result.Content;
13
- const element = render({});
20
+ const element = resolve(result.Content({}));
14
21
  assert.equal(element.props.children[0].type, "h1");
15
22
  });
16
23
  test("vue compileMDX evaluates the same MDX source into a Vue vnode tree", async () => {
@@ -18,7 +25,35 @@ test("vue compileMDX evaluates the same MDX source into a Vue vnode tree", async
18
25
  assert.equal(result.ok, true);
19
26
  if (!result.ok)
20
27
  return;
21
- assert.equal(typeof result.Content, "function");
22
- const vnode = result.Content({});
28
+ const vnode = resolve(result.Content({}));
23
29
  assert.equal(vnode.children[0].type, "h1");
24
30
  });
31
+ test("react EntryLink renders as a link to /entries/{id} with no components map supplied", async () => {
32
+ const result = await compileReactMDX('<EntryLink id="abc123">Read more</EntryLink>');
33
+ assert.equal(result.ok, true);
34
+ if (!result.ok)
35
+ return;
36
+ const link = resolve(result.Content({}));
37
+ assert.equal(link.type, "a");
38
+ assert.equal(link.props.href, "/entries/abc123");
39
+ });
40
+ test("vue EntryLink renders as a link to /entries/{id} with no components map supplied", async () => {
41
+ const result = await compileVueMDX('<EntryLink id="abc123">Read more</EntryLink>');
42
+ assert.equal(result.ok, true);
43
+ if (!result.ok)
44
+ return;
45
+ const link = resolve(result.Content({}));
46
+ assert.equal(link.type, "a");
47
+ assert.equal(link.props.href, "/entries/abc123");
48
+ });
49
+ test("a supplied EntryLink override still wins over the default", async () => {
50
+ const result = await compileReactMDX('<EntryLink id="abc123">Read more</EntryLink>');
51
+ assert.equal(result.ok, true);
52
+ if (!result.ok)
53
+ return;
54
+ const CustomEntryLink = () => ({ type: "custom-entry-link", props: {} });
55
+ const link = resolve(result.Content({
56
+ components: { EntryLink: CustomEntryLink },
57
+ }));
58
+ assert.equal(link.type, "custom-entry-link");
59
+ });
package/dist/core.d.ts CHANGED
@@ -12,5 +12,17 @@ export interface FailedMDX {
12
12
  * Shared MDX-to-component evaluation. Parameterized by JSX runtime (React's
13
13
  * `react/jsx-runtime`, a Vue `h()`-based shim, ...) so each framework entry point
14
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.
15
20
  */
16
- export declare function compileMDXCore<TComponent>(source: string, jsxRuntime: JsxRuntime): Promise<CompiledMDX<TComponent> | FailedMDX>;
21
+ 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
+ * Covers the common case (a plain link to the entry's default route); apps that
24
+ * route entries differently still override it by passing `components.EntryLink`. */
25
+ export declare function makeDefaultEntryLink(jsxRuntime: JsxRuntime): ({ id, children }: {
26
+ id?: string;
27
+ children?: unknown;
28
+ }) => JSX.Element;
package/dist/core.js CHANGED
@@ -5,19 +5,43 @@ import rehypeSlug from "rehype-slug";
5
5
  * Shared MDX-to-component evaluation. Parameterized by JSX runtime (React's
6
6
  * `react/jsx-runtime`, a Vue `h()`-based shim, ...) so each framework entry point
7
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.
8
13
  */
9
- export async function compileMDXCore(source, jsxRuntime) {
14
+ export async function compileMDXCore(source, jsxRuntime, defaultComponents) {
10
15
  try {
11
16
  const { default: Content } = await evaluate(source, {
12
17
  ...jsxRuntime,
13
18
  remarkPlugins: [remarkGfm],
14
19
  rehypePlugins: [rehypeSlug],
15
20
  });
21
+ const OutputContent = defaultComponents
22
+ ? withDefaultComponents(Content, defaultComponents, jsxRuntime)
23
+ : Content;
16
24
  // evaluate()'s return type assumes React's JSX types regardless of the runtime
17
25
  // passed in; TComponent reflects the actual shape for the calling framework.
18
- return { ok: true, Content: Content };
26
+ return { ok: true, Content: OutputContent };
19
27
  }
20
28
  catch (error) {
21
29
  return { ok: false, error };
22
30
  }
23
31
  }
32
+ function withDefaultComponents(Content, defaults, jsxRuntime) {
33
+ return function ContentWithDefaults(props) {
34
+ return jsxRuntime.jsx(Content, {
35
+ ...props,
36
+ components: { ...defaults, ...props?.components },
37
+ });
38
+ };
39
+ }
40
+ /** Default `EntryLink` for a given JSX runtime — renders `<a href="/entries/{id}">`.
41
+ * Covers the common case (a plain link to the entry's default route); apps that
42
+ * route entries differently still override it by passing `components.EntryLink`. */
43
+ export function makeDefaultEntryLink(jsxRuntime) {
44
+ return function EntryLink({ id, children }) {
45
+ return jsxRuntime.jsx("a", { href: id ? `/entries/${id}` : undefined, children });
46
+ };
47
+ }
@@ -0,0 +1,31 @@
1
+ import type { MDXComponents } from "mdx/types";
2
+ import type { ComponentType } from "react";
3
+ import { type FailedMDX } from "./core.js";
4
+ import { type ReactNativePrimitives, type ReactNativeStyleOptions } from "./reactNativeComponents.js";
5
+ export interface CompiledMDX {
6
+ ok: true;
7
+ Content: ComponentType<{
8
+ components?: MDXComponents;
9
+ }>;
10
+ }
11
+ export type { FailedMDX };
12
+ /**
13
+ * Wires up a React Native `compileMDX` with default Text/View/Image mappings for
14
+ * every standard markdown element (and `EntryLink`), so a project sets up its RN
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.
28
+ */
29
+ export declare function createReactNativeRenderer(primitives: ReactNativePrimitives, styleOptions?: ReactNativeStyleOptions): {
30
+ compileMDX: (source: string) => Promise<CompiledMDX | FailedMDX>;
31
+ };
@@ -0,0 +1,27 @@
1
+ import * as runtime from "react/jsx-runtime";
2
+ import { compileMDXCore } from "./core.js";
3
+ import { buildReactNativeComponents, } from "./reactNativeComponents.js";
4
+ /**
5
+ * Wires up a React Native `compileMDX` with default Text/View/Image mappings for
6
+ * every standard markdown element (and `EntryLink`), so a project sets up its RN
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.
20
+ */
21
+ export function createReactNativeRenderer(primitives, styleOptions) {
22
+ const defaultComponents = buildReactNativeComponents(primitives, styleOptions);
23
+ async function compileMDX(source) {
24
+ return compileMDXCore(source, runtime, defaultComponents);
25
+ }
26
+ return { compileMDX };
27
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createReactNativeRenderer } from "./reactNative.js";
4
+ // Fakes standing in for react-native's Text/View/Image — see reactNativeComponents.test.ts.
5
+ const Text = () => null;
6
+ const View = () => null;
7
+ const Image = () => null;
8
+ const LEAVES = [Text, View, Image];
9
+ /** Unwraps nested function-component elements (the default-components wrapper, the
10
+ * per-tag style wrapper, ...) down to the RN primitive actually used, without calling
11
+ * into the primitive itself (Text/View/Image are opaque leaves here, not JSX). */
12
+ function resolve(element) {
13
+ return typeof element.type === "function" && !LEAVES.includes(element.type)
14
+ ? resolve(element.type(element.props))
15
+ : element;
16
+ }
17
+ test("React Native smoke test: renders standard markdown through Text/View with zero setup beyond the three primitives", async () => {
18
+ const { compileMDX } = createReactNativeRenderer({ Text, View, Image });
19
+ const result = await compileMDX("# Hello\n\nWorld");
20
+ assert.equal(result.ok, true);
21
+ if (!result.ok)
22
+ return;
23
+ const element = resolve(result.Content({}));
24
+ const heading = resolve(element.props.children[0]);
25
+ assert.equal(heading.type, Text);
26
+ assert.equal(heading.props.style.fontSize, 28);
27
+ });
28
+ test("React Native EntryLink renders out of the box (no components map)", async () => {
29
+ const { compileMDX } = createReactNativeRenderer({ Text, View, Image });
30
+ const result = await compileMDX('<EntryLink id="abc123">Read more</EntryLink>');
31
+ assert.equal(result.ok, true);
32
+ if (!result.ok)
33
+ return;
34
+ const element = resolve(result.Content({}));
35
+ assert.equal(element.type, Text);
36
+ });
@@ -0,0 +1,209 @@
1
+ import type { ComponentType, ReactNode } from "react";
2
+ export interface ReactNativePrimitives {
3
+ Text: ComponentType<{
4
+ children?: ReactNode;
5
+ style?: unknown;
6
+ }>;
7
+ View: ComponentType<{
8
+ children?: ReactNode;
9
+ style?: unknown;
10
+ }>;
11
+ Image: ComponentType<{
12
+ source: {
13
+ uri?: string;
14
+ };
15
+ accessibilityLabel?: string;
16
+ style?: unknown;
17
+ }>;
18
+ }
19
+ export interface ReactNativeStyleOptions {
20
+ /** Skip the built-in default styles (heading scale, link color, table borders, ...)
21
+ * entirely — components render with no `style` unless `styles` supplies one. */
22
+ unstyled?: boolean;
23
+ /** Per-tag style overrides, merged on top of the defaults (or used as-is under
24
+ * `unstyled`). Same tag keys as the components map, e.g. `{ h1: { fontSize: 32 } }`. */
25
+ styles?: Partial<Record<string, Record<string, unknown>>>;
26
+ }
27
+ /**
28
+ * Default MDX `components` map for React Native, built from its Text/View/Image
29
+ * primitives. RN has no intrinsic host tags for `p`/`h1`/`a`/etc the way web React
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.
34
+ */
35
+ export declare function buildReactNativeComponents({ Text, View, Image }: ReactNativePrimitives, { unstyled, styles }?: ReactNativeStyleOptions): {
36
+ p: ComponentType<{
37
+ children?: ReactNode;
38
+ style?: unknown;
39
+ }> | ((props: {
40
+ children?: ReactNode;
41
+ style?: unknown;
42
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
43
+ h1: ComponentType<{
44
+ children?: ReactNode;
45
+ style?: unknown;
46
+ }> | ((props: {
47
+ children?: ReactNode;
48
+ style?: unknown;
49
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
50
+ h2: ComponentType<{
51
+ children?: ReactNode;
52
+ style?: unknown;
53
+ }> | ((props: {
54
+ children?: ReactNode;
55
+ style?: unknown;
56
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
57
+ h3: ComponentType<{
58
+ children?: ReactNode;
59
+ style?: unknown;
60
+ }> | ((props: {
61
+ children?: ReactNode;
62
+ style?: unknown;
63
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
64
+ h4: ComponentType<{
65
+ children?: ReactNode;
66
+ style?: unknown;
67
+ }> | ((props: {
68
+ children?: ReactNode;
69
+ style?: unknown;
70
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
71
+ h5: ComponentType<{
72
+ children?: ReactNode;
73
+ style?: unknown;
74
+ }> | ((props: {
75
+ children?: ReactNode;
76
+ style?: unknown;
77
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
78
+ h6: ComponentType<{
79
+ children?: ReactNode;
80
+ style?: unknown;
81
+ }> | ((props: {
82
+ children?: ReactNode;
83
+ style?: unknown;
84
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
85
+ strong: ComponentType<{
86
+ children?: ReactNode;
87
+ style?: unknown;
88
+ }> | ((props: {
89
+ children?: ReactNode;
90
+ style?: unknown;
91
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
92
+ em: ComponentType<{
93
+ children?: ReactNode;
94
+ style?: unknown;
95
+ }> | ((props: {
96
+ children?: ReactNode;
97
+ style?: unknown;
98
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
99
+ del: ComponentType<{
100
+ children?: ReactNode;
101
+ style?: unknown;
102
+ }> | ((props: {
103
+ children?: ReactNode;
104
+ style?: unknown;
105
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
106
+ code: ComponentType<{
107
+ children?: ReactNode;
108
+ style?: unknown;
109
+ }> | ((props: {
110
+ children?: ReactNode;
111
+ style?: unknown;
112
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
113
+ pre: ComponentType<{
114
+ children?: ReactNode;
115
+ style?: unknown;
116
+ }> | ((props: {
117
+ children?: ReactNode;
118
+ style?: unknown;
119
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
120
+ li: ComponentType<{
121
+ children?: ReactNode;
122
+ style?: unknown;
123
+ }> | ((props: {
124
+ children?: ReactNode;
125
+ style?: unknown;
126
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
127
+ th: ComponentType<{
128
+ children?: ReactNode;
129
+ style?: unknown;
130
+ }> | ((props: {
131
+ children?: ReactNode;
132
+ style?: unknown;
133
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
134
+ td: ComponentType<{
135
+ children?: ReactNode;
136
+ style?: unknown;
137
+ }> | ((props: {
138
+ children?: ReactNode;
139
+ style?: unknown;
140
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
141
+ ul: ComponentType<{
142
+ children?: ReactNode;
143
+ style?: unknown;
144
+ }> | ((props: {
145
+ children?: ReactNode;
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<{
156
+ children?: ReactNode;
157
+ style?: unknown;
158
+ }> | ((props: {
159
+ children?: ReactNode;
160
+ style?: unknown;
161
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
162
+ hr: ComponentType<{
163
+ children?: ReactNode;
164
+ style?: unknown;
165
+ }> | ((props: {
166
+ children?: ReactNode;
167
+ style?: unknown;
168
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
169
+ table: ComponentType<{
170
+ children?: ReactNode;
171
+ style?: unknown;
172
+ }> | ((props: {
173
+ children?: ReactNode;
174
+ style?: unknown;
175
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
176
+ thead: ComponentType<{
177
+ children?: ReactNode;
178
+ style?: unknown;
179
+ }>;
180
+ tbody: ComponentType<{
181
+ children?: ReactNode;
182
+ style?: unknown;
183
+ }>;
184
+ tr: ComponentType<{
185
+ children?: ReactNode;
186
+ style?: unknown;
187
+ }> | ((props: {
188
+ children?: ReactNode;
189
+ style?: unknown;
190
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
191
+ a: ComponentType<{
192
+ children?: ReactNode;
193
+ style?: unknown;
194
+ }> | ((props: {
195
+ children?: ReactNode;
196
+ style?: unknown;
197
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
198
+ EntryLink: ComponentType<{
199
+ children?: ReactNode;
200
+ style?: unknown;
201
+ }> | ((props: {
202
+ children?: ReactNode;
203
+ style?: unknown;
204
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>);
205
+ img: ({ src, alt }: {
206
+ src?: string;
207
+ alt?: string;
208
+ }) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
209
+ };
@@ -0,0 +1,53 @@
1
+ import * as runtime from "react/jsx-runtime";
2
+ import { defaultReactNativeStyles } from "./reactNativeStyles.js";
3
+ /**
4
+ * Default MDX `components` map for React Native, built from its Text/View/Image
5
+ * primitives. RN has no intrinsic host tags for `p`/`h1`/`a`/etc the way web React
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.
10
+ */
11
+ export function buildReactNativeComponents({ Text, View, Image }, { unstyled, styles } = {}) {
12
+ const styleFor = (tag) => unstyled ? styles?.[tag] : { ...defaultReactNativeStyles[tag], ...styles?.[tag] };
13
+ function styled(Base, tag) {
14
+ const style = styleFor(tag);
15
+ if (!style || Object.keys(style).length === 0)
16
+ return Base;
17
+ return (props) => runtime.jsx(Base, { ...props, style: props.style ? [style, props.style] : style });
18
+ }
19
+ const Img = ({ src, alt }) => runtime.jsx(Image, {
20
+ source: { uri: src },
21
+ accessibilityLabel: alt,
22
+ style: styleFor("img"),
23
+ });
24
+ const Link = styled(({ children, style }) => runtime.jsx(Text, { children, style }), "a");
25
+ return {
26
+ p: styled(Text, "p"),
27
+ h1: styled(Text, "h1"),
28
+ h2: styled(Text, "h2"),
29
+ h3: styled(Text, "h3"),
30
+ h4: styled(Text, "h4"),
31
+ h5: styled(Text, "h5"),
32
+ h6: styled(Text, "h6"),
33
+ strong: styled(Text, "strong"),
34
+ em: styled(Text, "em"),
35
+ del: styled(Text, "del"),
36
+ code: styled(Text, "code"),
37
+ pre: styled(Text, "pre"),
38
+ li: styled(Text, "li"),
39
+ th: styled(Text, "th"),
40
+ td: styled(Text, "td"),
41
+ ul: styled(View, "ul"),
42
+ ol: styled(View, "ol"),
43
+ blockquote: styled(View, "blockquote"),
44
+ hr: styled(View, "hr"),
45
+ table: styled(View, "table"),
46
+ thead: View,
47
+ tbody: View,
48
+ tr: styled(View, "tr"),
49
+ a: Link,
50
+ EntryLink: Link,
51
+ img: Img,
52
+ };
53
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { buildReactNativeComponents } from "./reactNativeComponents.js";
4
+ // Stand-ins for react-native's Text/View/Image — this package doesn't depend on
5
+ // react-native itself (it only resolves inside Metro, not plain Node), so the mapping
6
+ // logic is tested against fakes with the same shape.
7
+ const Text = () => null;
8
+ const View = () => null;
9
+ const Image = () => null;
10
+ test("maps standard markdown elements onto Text/View, with default styling applied", () => {
11
+ const components = buildReactNativeComponents({ Text, View, Image });
12
+ const h1 = components.h1({});
13
+ assert.equal(h1.type, Text);
14
+ assert.equal(h1.props.style.fontSize, 28);
15
+ const table = components.table({});
16
+ assert.equal(table.type, View);
17
+ const tr = components.tr({});
18
+ assert.equal(tr.props.style.flexDirection, "row");
19
+ });
20
+ test("unstyled drops the defaults", () => {
21
+ const components = buildReactNativeComponents({ Text, View, Image }, { unstyled: true });
22
+ assert.equal(components.h1, Text); // no style to apply -> returns the primitive directly
23
+ });
24
+ test("styles option overrides a specific tag without needing unstyled", () => {
25
+ const components = buildReactNativeComponents({ Text, View, Image }, { styles: { h1: { fontSize: 40 } } });
26
+ const h1 = components.h1({});
27
+ assert.equal(h1.props.style.fontSize, 40);
28
+ assert.equal(h1.props.style.fontWeight, "700"); // default still applied
29
+ });
30
+ test("img wrapper passes src through as Image's source.uri", () => {
31
+ const components = buildReactNativeComponents({ Text, View, Image });
32
+ const element = components.img({ src: "https://example.com/a.png", alt: "a" });
33
+ const props = element.props;
34
+ assert.equal(props.source.uri, "https://example.com/a.png");
35
+ assert.equal(props.accessibilityLabel, "a");
36
+ });
37
+ test("a and EntryLink both render as styled Text", () => {
38
+ const components = buildReactNativeComponents({ Text, View, Image });
39
+ assert.equal(components.EntryLink, components.a);
40
+ const element = components.a({ children: "hi" });
41
+ const inner = element.type(element.props);
42
+ assert.equal(inner.type, Text);
43
+ assert.equal(inner.props.style.color, "#2563eb");
44
+ });
@@ -0,0 +1,4 @@
1
+ /** Default per-tag style objects for the React Native preset — mirrors `styles.css`'s
2
+ * web defaults (heading scale, link color/underline, code/blockquote treatment, list
3
+ * spacing) so RN output looks like the web default instead of unstyled plain text. */
4
+ export declare const defaultReactNativeStyles: Record<string, Record<string, unknown>>;
@@ -0,0 +1,46 @@
1
+ /** Default per-tag style objects for the React Native preset — mirrors `styles.css`'s
2
+ * web defaults (heading scale, link color/underline, code/blockquote treatment, list
3
+ * spacing) so RN output looks like the web default instead of unstyled plain text. */
4
+ export const defaultReactNativeStyles = {
5
+ p: { marginBottom: 12, lineHeight: 22 },
6
+ h1: { fontSize: 28, fontWeight: "700", marginTop: 20, marginBottom: 10 },
7
+ h2: { fontSize: 24, fontWeight: "700", marginTop: 18, marginBottom: 8 },
8
+ h3: { fontSize: 20, fontWeight: "700", marginTop: 16, marginBottom: 8 },
9
+ h4: { fontSize: 18, fontWeight: "600", marginTop: 14, marginBottom: 6 },
10
+ h5: { fontSize: 16, fontWeight: "600", marginTop: 12, marginBottom: 6 },
11
+ h6: { fontSize: 14, fontWeight: "600", marginTop: 12, marginBottom: 6 },
12
+ strong: { fontWeight: "700" },
13
+ em: { fontStyle: "italic" },
14
+ del: { textDecorationLine: "line-through" },
15
+ code: {
16
+ fontFamily: "Courier",
17
+ backgroundColor: "#f2f2f2",
18
+ paddingHorizontal: 4,
19
+ borderRadius: 3,
20
+ },
21
+ pre: {
22
+ fontFamily: "Courier",
23
+ backgroundColor: "#f2f2f2",
24
+ padding: 12,
25
+ borderRadius: 6,
26
+ marginBottom: 12,
27
+ },
28
+ blockquote: {
29
+ borderLeftWidth: 3,
30
+ borderLeftColor: "#d0d0d0",
31
+ paddingLeft: 12,
32
+ marginBottom: 12,
33
+ opacity: 0.85,
34
+ },
35
+ li: { marginBottom: 4 },
36
+ ul: { marginBottom: 12 },
37
+ ol: { marginBottom: 12 },
38
+ hr: { borderBottomWidth: 1, borderBottomColor: "#d0d0d0", marginVertical: 16 },
39
+ table: { borderWidth: 1, borderColor: "#d0d0d0", marginBottom: 12 },
40
+ // RN has no native table layout — `tr` lays cells out as a row (flex), `th`/`td` split
41
+ // the row evenly; only a bottom rule separates rows (no per-cell borders, unlike web).
42
+ tr: { flexDirection: "row", borderBottomWidth: 1, borderColor: "#d0d0d0" },
43
+ th: { flex: 1, fontWeight: "700", padding: 6 },
44
+ td: { flex: 1, padding: 6 },
45
+ a: { color: "#2563eb", textDecorationLine: "underline" },
46
+ };
package/dist/vue.d.ts CHANGED
@@ -8,6 +8,7 @@ export type { FailedMDX };
8
8
  /**
9
9
  * Compiles raw MDX/markdown into a renderable Vue component. Vue has no RSC-style
10
10
  * async component, so call this from `setup()`/a composable and render the result
11
- * yourself: `h(Content, props)` or `<component :is="Content" />`.
11
+ * yourself: `h(Content, props)` or `<component :is="Content" />`. Standard markdown
12
+ * elements render via real DOM tags with zero setup.
12
13
  */
13
14
  export declare function compileMDX(source: string): Promise<CompiledMDX | FailedMDX>;
package/dist/vue.js CHANGED
@@ -1,10 +1,12 @@
1
- import { compileMDXCore } from "./core.js";
1
+ import { compileMDXCore, makeDefaultEntryLink } from "./core.js";
2
2
  import { vueJsxRuntime } from "./vueRuntime.js";
3
+ const defaultComponents = { EntryLink: makeDefaultEntryLink(vueJsxRuntime) };
3
4
  /**
4
5
  * Compiles raw MDX/markdown into a renderable Vue component. Vue has no RSC-style
5
6
  * async component, so call this from `setup()`/a composable and render the result
6
- * yourself: `h(Content, props)` or `<component :is="Content" />`.
7
+ * yourself: `h(Content, props)` or `<component :is="Content" />`. Standard markdown
8
+ * elements render via real DOM tags with zero setup.
7
9
  */
8
10
  export async function compileMDX(source) {
9
- return compileMDXCore(source, vueJsxRuntime);
11
+ return compileMDXCore(source, vueJsxRuntime, defaultComponents);
10
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@draftbase/renderer",
3
- "version": "0.3.2",
3
+ "version": "0.4.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",
@@ -31,6 +31,7 @@
31
31
  "exports": {
32
32
  ".": "./dist/index.js",
33
33
  "./vue": "./dist/vue.js",
34
+ "./react-native": "./dist/reactNative.js",
34
35
  "./styles.css": "./dist/styles.css"
35
36
  },
36
37
  "files": [
@@ -41,7 +42,7 @@
41
42
  "scripts": {
42
43
  "build": "tsc -p tsconfig.json && cp src/styles.css dist/styles.css",
43
44
  "lint": "tsc --noEmit",
44
- "test": "node --test dist/wrapperClassName.test.js dist/toHtml.test.js dist/compileMDX.test.js"
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"
45
46
  },
46
47
  "peerDependencies": {
47
48
  "react": ">=18",