@mzebley/mark-down-react 1.0.0

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 ADDED
@@ -0,0 +1,127 @@
1
+ # mark↓ React Adapter
2
+ *(published as `@mzebley/mark-down-react`)*
3
+
4
+ React bindings for the [mark↓ core runtime](../core/README.md). This package exposes context providers, hooks, and ready-to-use components that make it simple to render Markdown snippets safely. For a broader overview of the project, start with the [root README](../../README.md).
5
+
6
+ ## Table of contents
7
+
8
+ 1. [Installation](#installation)
9
+ 2. [Provider setup](#provider-setup)
10
+ 3. [Hook](#hook)
11
+ 4. [`<SnippetView />` component](#snippetview--component)
12
+ 5. [Server-side rendering](#server-side-rendering)
13
+ 6. [TypeScript helpers](#typescript-helpers)
14
+ 7. [Roadmap](#roadmap)
15
+ 8. [Related packages](#related-packages)
16
+
17
+ ## Installation
18
+
19
+ Install the adapter along with the core runtime and DOMPurify (used for sanitising HTML):
20
+
21
+ ```bash
22
+ npm install @mzebley/mark-down-react @mzebley/mark-down dompurify
23
+ ```
24
+
25
+ Generate a manifest with the [CLI](../cli/README.md) before rendering snippets.
26
+
27
+ ## Provider setup
28
+
29
+ Wrap your app with the `SnippetProvider` so that hooks and components can access a shared client instance:
30
+
31
+ ```tsx
32
+ import { SnippetProvider } from '@mzebley/mark-down-react';
33
+
34
+ export function App({ children }: { children: React.ReactNode }) {
35
+ return (
36
+ <SnippetProvider options={{ manifest: '/snippets-index.json' }}>
37
+ {children}
38
+ </SnippetProvider>
39
+ );
40
+ }
41
+ ```
42
+
43
+ `options` maps directly to the [`SnippetClient` configuration](../core/README.md#client-options), so you can provide custom fetchers, renderers, or manifest loaders as needed.
44
+
45
+ ## Hook
46
+
47
+ ### `useSnippet(slug)`
48
+
49
+ Fetch a single snippet and track loading / error state:
50
+
51
+ ```tsx
52
+ import { useSnippet } from '@mzebley/mark-down-react';
53
+
54
+ export function Hero() {
55
+ const { snippet, loading, error } = useSnippet('getting-started-welcome');
56
+
57
+ if (loading) return <p>Loading…</p>;
58
+ if (error) return <p role="alert">Failed to load snippet.</p>;
59
+ if (!snippet) return null;
60
+
61
+ return <div dangerouslySetInnerHTML={{ __html: snippet.html }} />;
62
+ }
63
+ ```
64
+
65
+ ## `<SnippetView />` component
66
+
67
+ Render snippets declaratively with built-in loading and error fallbacks:
68
+
69
+ ```tsx
70
+ import { SnippetView } from '@mzebley/mark-down-react';
71
+
72
+ <SnippetView
73
+ slug="components-button"
74
+ loadingFallback={<p>Loading…</p>}
75
+ errorFallback={<p role="alert">Unable to load snippet.</p>}
76
+ onLoaded={(snippet) => console.log('Rendered', snippet.slug)}
77
+ />;
78
+ ```
79
+
80
+ Features:
81
+
82
+ - Uses DOMPurify under the hood for HTML sanitisation.
83
+ - Accepts `className` for styling and emits `onLoaded(snippet)` once HTML resolves.
84
+ - Customise UX via `loadingFallback` / `errorFallback`, or render the hook directly for complete control.
85
+
86
+ ## Server-side rendering
87
+
88
+ When using Next.js, Remix, or another SSR framework, provide a server-safe fetch implementation:
89
+
90
+ ```tsx
91
+ import fetch from 'node-fetch';
92
+ import { SnippetProvider } from '@mzebley/mark-down-react';
93
+
94
+ <SnippetProvider
95
+ options={{
96
+ manifest: () => import('../snippets-index.json'),
97
+ fetcher: (input, init) => fetch(input as string, init),
98
+ }}
99
+ >
100
+ {children}
101
+ </SnippetProvider>;
102
+ ```
103
+
104
+ Because the adapter defers to the core runtime, SSR works the same way as the base client. Pair with framework-specific data fetching if you prefer to prehydrate snippets.
105
+
106
+ ## TypeScript helpers
107
+
108
+ All exported hooks and components ship with rich TypeScript definitions:
109
+
110
+ - Use the `Snippet` and `SnippetMeta` types from `@mzebley/mark-down` to annotate props.
111
+ - Narrow snippet metadata with generics: `useSnippet<CustomExtra>('slug')`.
112
+ - Leverage the `SnippetContextValue` interface when mocking providers in tests.
113
+
114
+ ## Roadmap
115
+
116
+ - **Collection hooks** – add `useSnippets` for list queries and pagination helpers for design system docs.
117
+ - **Suspense support** – optional wrappers that expose a resource-style API for React 18 concurrent features.
118
+ - **Custom sanitizers** – let consumers inject DOMPurify configs or alternate HTML sanitizers.
119
+ - **Storybook plugin** – surface snippets inside Storybook/Chromatic panels for quick previews.
120
+
121
+ ## Related packages
122
+
123
+ - [Core runtime](../core/README.md)
124
+ - [CLI](../cli/README.md)
125
+ - [Angular adapter](../angular/README.md)
126
+ - [Example app](../../examples/basic/README.md)
127
+ - [Monorepo overview](../../README.md)
package/dist/index.cjs ADDED
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ SnippetProvider: () => SnippetProvider,
34
+ SnippetView: () => SnippetView,
35
+ useSnippet: () => useSnippet,
36
+ useSnippetClient: () => useSnippetClient
37
+ });
38
+ module.exports = __toCommonJS(index_exports);
39
+
40
+ // src/context.tsx
41
+ var import_react = require("react");
42
+ var import_mark_down = require("@mzebley/mark-down");
43
+ var import_jsx_runtime = require("react/jsx-runtime");
44
+ var SnippetClientContext = (0, import_react.createContext)(null);
45
+ function SnippetProvider({ client, options, children }) {
46
+ const value = (0, import_react.useMemo)(() => {
47
+ if (client) {
48
+ return client;
49
+ }
50
+ if (options) {
51
+ return new import_mark_down.SnippetClient(options);
52
+ }
53
+ throw new Error("SnippetProvider requires either a client or options");
54
+ }, [client, options]);
55
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SnippetClientContext.Provider, { value, children });
56
+ }
57
+ function useSnippetClient() {
58
+ const client = (0, import_react.useContext)(SnippetClientContext);
59
+ if (!client) {
60
+ throw new Error("useSnippetClient must be used within a SnippetProvider");
61
+ }
62
+ return client;
63
+ }
64
+
65
+ // src/hooks.ts
66
+ var import_react2 = require("react");
67
+ function useSnippet(slug) {
68
+ const client = useSnippetClient();
69
+ const [result, setResult] = (0, import_react2.useState)(() => ({
70
+ loading: Boolean(slug)
71
+ }));
72
+ (0, import_react2.useEffect)(() => {
73
+ if (!slug) {
74
+ setResult({ loading: false });
75
+ return;
76
+ }
77
+ let cancelled = false;
78
+ setResult((prev) => ({ ...prev, loading: true, error: void 0 }));
79
+ client.get(slug).then((snippet) => {
80
+ if (cancelled) return;
81
+ setResult({ snippet, loading: false });
82
+ }).catch((error) => {
83
+ if (cancelled) return;
84
+ setResult({ loading: false, error });
85
+ });
86
+ return () => {
87
+ cancelled = true;
88
+ };
89
+ }, [client, slug]);
90
+ return result;
91
+ }
92
+
93
+ // src/snippet-view.tsx
94
+ var import_react3 = require("react");
95
+ var import_dompurify = __toESM(require("dompurify"), 1);
96
+ var import_jsx_runtime2 = require("react/jsx-runtime");
97
+ function SnippetView({
98
+ slug,
99
+ className,
100
+ loadingFallback = "Loading\u2026",
101
+ errorFallback = "Unable to load snippet",
102
+ onLoaded
103
+ }) {
104
+ const state = useSnippet(slug);
105
+ const safeHtml = (0, import_react3.useMemo)(() => state.snippet ? import_dompurify.default.sanitize(state.snippet.html) : void 0, [state.snippet]);
106
+ (0, import_react3.useEffect)(() => {
107
+ onLoaded?.(state.snippet);
108
+ }, [state.snippet, onLoaded]);
109
+ if (state.loading) {
110
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className, children: loadingFallback });
111
+ }
112
+ if (state.error) {
113
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className, children: errorFallback });
114
+ }
115
+ if (!state.snippet) {
116
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className, children: "Snippet not found" });
117
+ }
118
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className, dangerouslySetInnerHTML: { __html: safeHtml ?? "" } });
119
+ }
120
+ // Annotate the CommonJS export names for ESM import in node:
121
+ 0 && (module.exports = {
122
+ SnippetProvider,
123
+ SnippetView,
124
+ useSnippet,
125
+ useSnippetClient
126
+ });
@@ -0,0 +1,29 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import { SnippetClient, SnippetClientOptions, Snippet } from '@mzebley/mark-down';
4
+
5
+ interface SnippetProviderProps {
6
+ client?: SnippetClient;
7
+ options?: SnippetClientOptions;
8
+ children: ReactNode;
9
+ }
10
+ declare function SnippetProvider({ client, options, children }: SnippetProviderProps): react_jsx_runtime.JSX.Element;
11
+ declare function useSnippetClient(): SnippetClient;
12
+
13
+ interface UseSnippetResult {
14
+ snippet?: Snippet;
15
+ loading: boolean;
16
+ error?: Error;
17
+ }
18
+ declare function useSnippet(slug?: string | null): UseSnippetResult;
19
+
20
+ interface SnippetViewProps {
21
+ slug: string;
22
+ className?: string;
23
+ loadingFallback?: ReactNode;
24
+ errorFallback?: ReactNode;
25
+ onLoaded?: (snippet?: Snippet) => void;
26
+ }
27
+ declare function SnippetView({ slug, className, loadingFallback, errorFallback, onLoaded }: SnippetViewProps): react_jsx_runtime.JSX.Element;
28
+
29
+ export { SnippetProvider, type SnippetProviderProps, SnippetView, type SnippetViewProps, type UseSnippetResult, useSnippet, useSnippetClient };
@@ -0,0 +1,29 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import { SnippetClient, SnippetClientOptions, Snippet } from '@mzebley/mark-down';
4
+
5
+ interface SnippetProviderProps {
6
+ client?: SnippetClient;
7
+ options?: SnippetClientOptions;
8
+ children: ReactNode;
9
+ }
10
+ declare function SnippetProvider({ client, options, children }: SnippetProviderProps): react_jsx_runtime.JSX.Element;
11
+ declare function useSnippetClient(): SnippetClient;
12
+
13
+ interface UseSnippetResult {
14
+ snippet?: Snippet;
15
+ loading: boolean;
16
+ error?: Error;
17
+ }
18
+ declare function useSnippet(slug?: string | null): UseSnippetResult;
19
+
20
+ interface SnippetViewProps {
21
+ slug: string;
22
+ className?: string;
23
+ loadingFallback?: ReactNode;
24
+ errorFallback?: ReactNode;
25
+ onLoaded?: (snippet?: Snippet) => void;
26
+ }
27
+ declare function SnippetView({ slug, className, loadingFallback, errorFallback, onLoaded }: SnippetViewProps): react_jsx_runtime.JSX.Element;
28
+
29
+ export { SnippetProvider, type SnippetProviderProps, SnippetView, type SnippetViewProps, type UseSnippetResult, useSnippet, useSnippetClient };
package/dist/index.js ADDED
@@ -0,0 +1,86 @@
1
+ // src/context.tsx
2
+ import { createContext, useContext, useMemo } from "react";
3
+ import { SnippetClient } from "@mzebley/mark-down";
4
+ import { jsx } from "react/jsx-runtime";
5
+ var SnippetClientContext = createContext(null);
6
+ function SnippetProvider({ client, options, children }) {
7
+ const value = useMemo(() => {
8
+ if (client) {
9
+ return client;
10
+ }
11
+ if (options) {
12
+ return new SnippetClient(options);
13
+ }
14
+ throw new Error("SnippetProvider requires either a client or options");
15
+ }, [client, options]);
16
+ return /* @__PURE__ */ jsx(SnippetClientContext.Provider, { value, children });
17
+ }
18
+ function useSnippetClient() {
19
+ const client = useContext(SnippetClientContext);
20
+ if (!client) {
21
+ throw new Error("useSnippetClient must be used within a SnippetProvider");
22
+ }
23
+ return client;
24
+ }
25
+
26
+ // src/hooks.ts
27
+ import { useEffect, useState } from "react";
28
+ function useSnippet(slug) {
29
+ const client = useSnippetClient();
30
+ const [result, setResult] = useState(() => ({
31
+ loading: Boolean(slug)
32
+ }));
33
+ useEffect(() => {
34
+ if (!slug) {
35
+ setResult({ loading: false });
36
+ return;
37
+ }
38
+ let cancelled = false;
39
+ setResult((prev) => ({ ...prev, loading: true, error: void 0 }));
40
+ client.get(slug).then((snippet) => {
41
+ if (cancelled) return;
42
+ setResult({ snippet, loading: false });
43
+ }).catch((error) => {
44
+ if (cancelled) return;
45
+ setResult({ loading: false, error });
46
+ });
47
+ return () => {
48
+ cancelled = true;
49
+ };
50
+ }, [client, slug]);
51
+ return result;
52
+ }
53
+
54
+ // src/snippet-view.tsx
55
+ import { useEffect as useEffect2, useMemo as useMemo2 } from "react";
56
+ import DOMPurify from "dompurify";
57
+ import { jsx as jsx2 } from "react/jsx-runtime";
58
+ function SnippetView({
59
+ slug,
60
+ className,
61
+ loadingFallback = "Loading\u2026",
62
+ errorFallback = "Unable to load snippet",
63
+ onLoaded
64
+ }) {
65
+ const state = useSnippet(slug);
66
+ const safeHtml = useMemo2(() => state.snippet ? DOMPurify.sanitize(state.snippet.html) : void 0, [state.snippet]);
67
+ useEffect2(() => {
68
+ onLoaded?.(state.snippet);
69
+ }, [state.snippet, onLoaded]);
70
+ if (state.loading) {
71
+ return /* @__PURE__ */ jsx2("div", { className, children: loadingFallback });
72
+ }
73
+ if (state.error) {
74
+ return /* @__PURE__ */ jsx2("div", { className, children: errorFallback });
75
+ }
76
+ if (!state.snippet) {
77
+ return /* @__PURE__ */ jsx2("div", { className, children: "Snippet not found" });
78
+ }
79
+ return /* @__PURE__ */ jsx2("div", { className, dangerouslySetInnerHTML: { __html: safeHtml ?? "" } });
80
+ }
81
+ export {
82
+ SnippetProvider,
83
+ SnippetView,
84
+ useSnippet,
85
+ useSnippetClient
86
+ };
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@mzebley/mark-down-react",
3
+ "version": "1.0.0",
4
+ "description": "mark↓ React Adapter",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.mjs",
8
+ "types": "dist/index.d.ts",
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "scripts": {
13
+ "build": "tsup src/index.ts --dts --format esm,cjs"
14
+ },
15
+ "dependencies": {
16
+ "@mzebley/mark-down": "file:../core",
17
+ "dompurify": "^3.0.5"
18
+ },
19
+ "peerDependencies": {
20
+ "react": "^18.2.0",
21
+ "react-dom": "^18.2.0"
22
+ }
23
+ }
@@ -0,0 +1,32 @@
1
+ import { createContext, ReactNode, useContext, useMemo } from "react";
2
+ import { SnippetClient, type SnippetClientOptions } from "@mzebley/mark-down";
3
+
4
+ const SnippetClientContext = createContext<SnippetClient | null>(null);
5
+
6
+ export interface SnippetProviderProps {
7
+ client?: SnippetClient;
8
+ options?: SnippetClientOptions;
9
+ children: ReactNode;
10
+ }
11
+
12
+ export function SnippetProvider({ client, options, children }: SnippetProviderProps) {
13
+ const value = useMemo(() => {
14
+ if (client) {
15
+ return client;
16
+ }
17
+ if (options) {
18
+ return new SnippetClient(options);
19
+ }
20
+ throw new Error("SnippetProvider requires either a client or options");
21
+ }, [client, options]);
22
+
23
+ return <SnippetClientContext.Provider value={value}>{children}</SnippetClientContext.Provider>;
24
+ }
25
+
26
+ export function useSnippetClient(): SnippetClient {
27
+ const client = useContext(SnippetClientContext);
28
+ if (!client) {
29
+ throw new Error("useSnippetClient must be used within a SnippetProvider");
30
+ }
31
+ return client;
32
+ }
package/src/hooks.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { useEffect, useState } from "react";
2
+ import type { Snippet } from "@mzebley/mark-down";
3
+ import { useSnippetClient } from "./context";
4
+
5
+ export interface UseSnippetResult {
6
+ snippet?: Snippet;
7
+ loading: boolean;
8
+ error?: Error;
9
+ }
10
+
11
+ export function useSnippet(slug?: string | null): UseSnippetResult {
12
+ const client = useSnippetClient();
13
+ const [result, setResult] = useState<UseSnippetResult>(() => ({
14
+ loading: Boolean(slug)
15
+ }));
16
+
17
+ useEffect(() => {
18
+ if (!slug) {
19
+ setResult({ loading: false });
20
+ return;
21
+ }
22
+
23
+ let cancelled = false;
24
+ setResult((prev) => ({ ...prev, loading: true, error: undefined }));
25
+
26
+ client
27
+ .get(slug)
28
+ .then((snippet) => {
29
+ if (cancelled) return;
30
+ setResult({ snippet, loading: false });
31
+ })
32
+ .catch((error: Error) => {
33
+ if (cancelled) return;
34
+ setResult({ loading: false, error });
35
+ });
36
+
37
+ return () => {
38
+ cancelled = true;
39
+ };
40
+ }, [client, slug]);
41
+
42
+ return result;
43
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./context";
2
+ export * from "./hooks";
3
+ export * from "./snippet-view";
@@ -0,0 +1,41 @@
1
+ import { ReactNode, useEffect, useMemo } from "react";
2
+ import DOMPurify from "dompurify";
3
+ import type { Snippet } from "@mzebley/mark-down";
4
+ import { useSnippet } from "./hooks";
5
+
6
+ export interface SnippetViewProps {
7
+ slug: string;
8
+ className?: string;
9
+ loadingFallback?: ReactNode;
10
+ errorFallback?: ReactNode;
11
+ onLoaded?: (snippet?: Snippet) => void;
12
+ }
13
+
14
+ export function SnippetView({
15
+ slug,
16
+ className,
17
+ loadingFallback = "Loading…",
18
+ errorFallback = "Unable to load snippet",
19
+ onLoaded
20
+ }: SnippetViewProps) {
21
+ const state = useSnippet(slug);
22
+ const safeHtml = useMemo(() => (state.snippet ? DOMPurify.sanitize(state.snippet.html) : undefined), [state.snippet]);
23
+
24
+ useEffect(() => {
25
+ onLoaded?.(state.snippet);
26
+ }, [state.snippet, onLoaded]);
27
+
28
+ if (state.loading) {
29
+ return <div className={className}>{loadingFallback}</div>;
30
+ }
31
+
32
+ if (state.error) {
33
+ return <div className={className}>{errorFallback}</div>;
34
+ }
35
+
36
+ if (!state.snippet) {
37
+ return <div className={className}>Snippet not found</div>;
38
+ }
39
+
40
+ return <div className={className} dangerouslySetInnerHTML={{ __html: safeHtml ?? "" }} />;
41
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "jsx": "react-jsx",
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }