@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 +56 -14
- package/dist/MDXContent.d.ts +4 -2
- package/dist/MDXContent.js +7 -4
- package/dist/MDXContent.test.d.ts +1 -0
- package/dist/MDXContent.test.js +21 -0
- package/dist/compileMDX.test.js +40 -5
- package/dist/core.d.ts +13 -1
- package/dist/core.js +26 -2
- package/dist/reactNative.d.ts +31 -0
- package/dist/reactNative.js +27 -0
- package/dist/reactNative.test.d.ts +1 -0
- package/dist/reactNative.test.js +36 -0
- package/dist/reactNativeComponents.d.ts +209 -0
- package/dist/reactNativeComponents.js +53 -0
- package/dist/reactNativeComponents.test.d.ts +1 -0
- package/dist/reactNativeComponents.test.js +44 -0
- package/dist/reactNativeStyles.d.ts +4 -0
- package/dist/reactNativeStyles.js +46 -0
- package/dist/vue.d.ts +2 -1
- package/dist/vue.js +5 -3
- package/package.json +3 -2
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
|
|
25
|
-
|
|
|
26
|
-
| React, Next.js,
|
|
27
|
-
|
|
|
28
|
-
|
|
|
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,
|
|
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
|
|
109
|
+
<Content /> {/* p, h1-h6, a, table, EntryLink, ... all render already-styled */}
|
|
70
110
|
</View>
|
|
71
111
|
);
|
|
72
112
|
}
|
|
73
113
|
```
|
|
74
114
|
|
|
75
|
-
|
|
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
|
-
|
|
118
|
+
// override specific tags
|
|
119
|
+
createReactNativeRenderer({ Text, View, Image }, { styles: { h1: { fontSize: 32 } } });
|
|
79
120
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
</MDXErrorBoundary>;
|
|
121
|
+
// drop all default styling
|
|
122
|
+
createReactNativeRenderer({ Text, View, Image }, { unstyled: true });
|
|
83
123
|
```
|
|
84
124
|
|
|
85
|
-
|
|
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
|
|
package/dist/MDXContent.d.ts
CHANGED
|
@@ -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
|
|
14
|
-
*
|
|
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 {
|
package/dist/MDXContent.js
CHANGED
|
@@ -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
|
|
9
|
-
*
|
|
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
|
+
});
|
package/dist/compileMDX.test.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
+
"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",
|