@pantoken/react-markdown 0.1.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,87 @@
1
+ # @pantoken/react-markdown
2
+
3
+ Render Markdown with [Instructure UI](https://instructure.design) components and pantoken icons —
4
+ a [react-markdown](https://github.com/remarkjs/react-markdown) integration. It maps Markdown
5
+ elements onto InstUI (`Heading`, `Text`, `Link`, `List`, `Table`, `Alert`, …) and adds three
6
+ pantoken-powered inline features:
7
+
8
+ - **`:icon:` tokens** — resolved through `@pantoken/rehype` + `@pantoken/icons`, with optional
9
+ brand icons via `@pantoken/plugin-simple-icons`.
10
+ - **Color swatches** — standalone color values (`#03893D`, `rgb(...)`) render as a swatch plus code.
11
+ - **GitHub alerts** — `> [!NOTE]` blockquotes become InstUI `Alert`s.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ npm i @pantoken/react-markdown
17
+ ```
18
+
19
+ `react`, `react-markdown`, `remark-gfm`, and the Instructure UI packages it maps to are peers, so
20
+ bring your own. `@mdx-js/react` is an optional peer, needed only for the `/mdx` entry.
21
+
22
+ Also available as `pantoken/react-markdown`.
23
+
24
+ ## Usage
25
+
26
+ ```mdx
27
+ import { InstuiMarkdown } from "@pantoken/react-markdown";
28
+
29
+ <InstuiMarkdown>
30
+ # Title
31
+
32
+ Go :arrow-left: back. Brand is #03893D.
33
+
34
+ > [!TIP]
35
+ > Helpful.
36
+
37
+ </InstuiMarkdown>;
38
+ ```
39
+
40
+ With brand icons:
41
+
42
+ ```mdx
43
+ import { InstuiMarkdown } from "@pantoken/react-markdown";
44
+ import { simpleIcons } from "@pantoken/plugin-simple-icons";
45
+ import * as registry from "simple-icons";
46
+
47
+ <InstuiMarkdown renderOptions={{ icons: { plugins: [simpleIcons({ registry })] } }}>
48
+ Star us on :github:
49
+ </InstuiMarkdown>
50
+ ;
51
+ ```
52
+
53
+ For MDX content, wrap it with the provider from the `/mdx` entry (needs the `@mdx-js/react` peer):
54
+
55
+ ```tsx
56
+ import { InstuiMdxProvider } from "@pantoken/react-markdown/mdx";
57
+ ```
58
+
59
+ `renderOptions` tunes `link` (external affordance, permalinks), `code` (language hint), `icons`
60
+ (enable, color, resolvers, plugins), `color` (swatches), `alerts` (GitHub alerts), and
61
+ `tableCaption`. See `InstuiMarkdownRenderOptions`.
62
+
63
+ ## API
64
+
65
+ - **`InstuiMarkdown(props)`** — the component; renders a Markdown string with InstUI mappings and
66
+ pantoken icon and color tokens.
67
+ - **`createInstuiMarkdownComponents(options?): Components`** — build the react-markdown component
68
+ map for a set of render options.
69
+ - **`instuiMarkdownComponents`** — the default component map (no options).
70
+ - **`buildIconResolver(options?): IconResolver`** — assemble the icon-resolver chain (plugin
71
+ resolvers, explicit resolvers, then the built-in set).
72
+ - **`rehypeColorCodes()` / `rehypeGithubAlerts()`** — the rehype plugins that tag color values and
73
+ GitHub-alert blockquotes.
74
+ - **`parseAlertMarker(text)`** — detect a `[!NOTE]`-style marker; **`isColorValue(value)`** — test
75
+ whether a string is a standalone CSS color.
76
+ - **`AlertMarker`, `InstuiMarkdownProps`, `InstuiMarkdownRenderOptions`** — public types.
77
+ - **`./mdx`** — an MDX provider (`InstuiMdxProvider`) that supplies the InstUI component map to MDX
78
+ content. Kept separate so `@mdx-js/react` stays optional.
79
+
80
+ ## Related
81
+
82
+ - Built on `@pantoken/rehype` and `@pantoken/icons` for the inline `:icon:` pipeline.
83
+ - Add brand icons with `@pantoken/plugin-simple-icons`.
84
+
85
+ ## License
86
+
87
+ MIT
@@ -0,0 +1,373 @@
1
+ import { Alert } from "@instructure/ui-alerts";
2
+ import { Heading } from "@instructure/ui-heading";
3
+ import { Img } from "@instructure/ui-img";
4
+ import { Link } from "@instructure/ui-link";
5
+ import { List } from "@instructure/ui-list";
6
+ import { Table } from "@instructure/ui-table";
7
+ import { Text } from "@instructure/ui-text";
8
+ import { View } from "@instructure/ui-view";
9
+ import { createElement } from "react";
10
+ import { resolve } from "@pantoken/icons";
11
+ import { visit } from "unist-util-visit";
12
+ import { jsx } from "react/jsx-runtime";
13
+ //#region src/helpers.ts
14
+ /**
15
+ * Pure, framework-free helpers: the icon-resolver chain, the color-code rehype plugin, alert-marker
16
+ * parsing, and value sniffing. Kept separate from the React layer so the logic is unit-testable.
17
+ *
18
+ * @module
19
+ */
20
+ /**
21
+ * Build the icon-resolver chain: plugin `rehype` resolvers first, then explicit `resolvers`, then
22
+ * the built-in `@pantoken/icons` set. The first match wins.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { buildIconResolver } from "@pantoken/react-markdown";
27
+ *
28
+ * const resolve = buildIconResolver();
29
+ * resolve("arrow-left"); // an IconEntry from the built-in set, or undefined
30
+ * ```
31
+ */
32
+ function buildIconResolver(options) {
33
+ const resolvers = [];
34
+ for (const plugin of options?.icons?.plugins ?? []) {
35
+ const contributed = plugin.rehype?.({ resolve });
36
+ if (contributed?.resolve) resolvers.push(contributed.resolve);
37
+ }
38
+ for (const resolver of options?.icons?.resolvers ?? []) resolvers.push(resolver);
39
+ resolvers.push(resolve);
40
+ return (code) => {
41
+ for (const resolver of resolvers) {
42
+ const hit = resolver(code);
43
+ if (hit) return hit;
44
+ }
45
+ };
46
+ }
47
+ const ALERT_RE = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*/;
48
+ /**
49
+ * Detect a GitHub alert marker at the start of text; returns the marker and the remaining text.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * import { parseAlertMarker } from "@pantoken/react-markdown";
54
+ *
55
+ * parseAlertMarker("[!TIP] Helpful"); // { marker: "TIP", rest: "Helpful" }
56
+ * parseAlertMarker("Plain text"); // undefined
57
+ * ```
58
+ */
59
+ function parseAlertMarker(text) {
60
+ const match = ALERT_RE.exec(text);
61
+ if (!match) return void 0;
62
+ return {
63
+ marker: match[1],
64
+ rest: text.slice(match[0].length)
65
+ };
66
+ }
67
+ const COLOR_RE = /^(#[0-9a-f]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\([^)]*\))$/i;
68
+ /**
69
+ * True when a string is a standalone CSS color value (hex or a color function).
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * import { isColorValue } from "@pantoken/react-markdown";
74
+ *
75
+ * isColorValue("#03893D"); // true
76
+ * isColorValue("oklch(0.7 0.1 200)"); // true
77
+ * isColorValue("hello"); // false
78
+ * ```
79
+ */
80
+ function isColorValue(value) {
81
+ return COLOR_RE.test(value.trim());
82
+ }
83
+ const INLINE_COLOR_RE = /(#[0-9a-f]{3,8}\b|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\([^)]*\))/gi;
84
+ /**
85
+ * A rehype plugin that wraps standalone color values in `<span data-color-code="…">`, so the React
86
+ * layer can render a swatch. Mirrors the icon-token pipeline.
87
+ *
88
+ * @example
89
+ * ```tsx
90
+ * import Markdown from "react-markdown";
91
+ * import { rehypeColorCodes } from "@pantoken/react-markdown";
92
+ *
93
+ * <Markdown rehypePlugins={[rehypeColorCodes]}>Brand is #03893D.</Markdown>;
94
+ * ```
95
+ */
96
+ function rehypeColorCodes() {
97
+ return (tree) => {
98
+ visit(tree, "text", (node, index, parent) => {
99
+ if (index === void 0 || !parent) return;
100
+ const text = node.value;
101
+ INLINE_COLOR_RE.lastIndex = 0;
102
+ if (!INLINE_COLOR_RE.test(text)) return;
103
+ INLINE_COLOR_RE.lastIndex = 0;
104
+ const out = [];
105
+ let last = 0;
106
+ let match;
107
+ while (match = INLINE_COLOR_RE.exec(text)) {
108
+ if (match.index > last) out.push({
109
+ type: "text",
110
+ value: text.slice(last, match.index)
111
+ });
112
+ out.push({
113
+ type: "element",
114
+ tagName: "span",
115
+ properties: { "data-color-code": match[1] },
116
+ children: [{
117
+ type: "text",
118
+ value: match[1]
119
+ }]
120
+ });
121
+ last = match.index + match[0].length;
122
+ }
123
+ if (last < text.length) out.push({
124
+ type: "text",
125
+ value: text.slice(last)
126
+ });
127
+ parent.children.splice(index, 1, ...out);
128
+ return index + out.length;
129
+ });
130
+ };
131
+ }
132
+ function findFirstText(node) {
133
+ if (!node || typeof node !== "object") return void 0;
134
+ const n = node;
135
+ if (n.type === "text") return node;
136
+ for (const child of n.children ?? []) {
137
+ const found = findFirstText(child);
138
+ if (found) return found;
139
+ }
140
+ }
141
+ /**
142
+ * A rehype plugin that tags GitHub-alert blockquotes: it detects a `[!NOTE]`-style marker at the
143
+ * start of a blockquote, records it as `data-alert="note"`, and strips the marker text. The React
144
+ * `blockquote` component then renders an InstUI `Alert`.
145
+ *
146
+ * @example
147
+ * ```tsx
148
+ * import Markdown from "react-markdown";
149
+ * import { rehypeGithubAlerts } from "@pantoken/react-markdown";
150
+ *
151
+ * <Markdown rehypePlugins={[rehypeGithubAlerts]}>{"> [!NOTE]\n> Heads up."}</Markdown>;
152
+ * ```
153
+ */
154
+ function rehypeGithubAlerts() {
155
+ return (tree) => {
156
+ visit(tree, "element", (node) => {
157
+ if (node.tagName !== "blockquote") return;
158
+ const first = findFirstText(node);
159
+ if (!first) return;
160
+ const parsed = parseAlertMarker(first.value.replace(/^\s+/, ""));
161
+ if (!parsed) return;
162
+ node.properties = {
163
+ ...node.properties,
164
+ "data-alert": parsed.marker.toLowerCase()
165
+ };
166
+ first.value = parsed.rest;
167
+ });
168
+ };
169
+ }
170
+ /** Map an alert marker to an InstUI `Alert` variant. */
171
+ function alertVariant(marker) {
172
+ switch (marker) {
173
+ case "TIP": return "success";
174
+ case "WARNING": return "warning";
175
+ case "CAUTION": return "error";
176
+ default: return "info";
177
+ }
178
+ }
179
+ //#endregion
180
+ //#region src/components.tsx
181
+ /**
182
+ * Maps Markdown (hast) elements onto Instructure UI components, plus the inline `:icon:` and
183
+ * color-swatch renderers. Returned as a react-markdown `components` map.
184
+ *
185
+ * @module
186
+ */
187
+ const TableHead = Table.Head;
188
+ const TableBody = Table.Body;
189
+ const TableRow = Table.Row;
190
+ const TableColHeader = Table.ColHeader;
191
+ const TableCell = Table.Cell;
192
+ function str(value) {
193
+ return typeof value === "string" ? value : void 0;
194
+ }
195
+ /** Best-effort extraction of a plain-text string from arbitrary React children. */
196
+ function textOf(children) {
197
+ if (typeof children === "string") return children;
198
+ if (Array.isArray(children)) return children.map(textOf).join("");
199
+ return "";
200
+ }
201
+ function slug(value) {
202
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "col";
203
+ }
204
+ function iconSpan(resolve, color) {
205
+ return function IconOrColorSpan(props) {
206
+ const iconName = str(props["data-pantoken-icon"]);
207
+ if (iconName) {
208
+ const icon = resolve(iconName);
209
+ const svg = icon?.svg ?? (icon?.path ? `<svg viewBox="0 0 24 24"><path d="${icon.path}" fill="currentColor"/></svg>` : void 0);
210
+ if (svg) return createElement("span", {
211
+ role: "img",
212
+ "aria-label": iconName,
213
+ className: "pantoken-icon",
214
+ style: {
215
+ display: "inline-flex",
216
+ width: "1em",
217
+ height: "1em",
218
+ verticalAlign: "-0.125em",
219
+ color
220
+ },
221
+ dangerouslySetInnerHTML: { __html: svg }
222
+ });
223
+ }
224
+ const colorCode = str(props["data-color-code"]);
225
+ if (colorCode) return createElement("span", {
226
+ className: "pantoken-color",
227
+ style: {
228
+ display: "inline-flex",
229
+ alignItems: "center",
230
+ gap: "0.35em"
231
+ }
232
+ }, createElement("span", {
233
+ "aria-hidden": true,
234
+ style: {
235
+ display: "inline-block",
236
+ width: "0.85em",
237
+ height: "0.85em",
238
+ borderRadius: "2px",
239
+ border: "1px solid rgba(0,0,0,0.2)",
240
+ background: colorCode
241
+ }
242
+ }), createElement("code", null, colorCode));
243
+ return createElement("span", null, props.children);
244
+ };
245
+ }
246
+ function heading(level) {
247
+ return ({ children }) => /* @__PURE__ */ jsx(Heading, {
248
+ level,
249
+ children
250
+ });
251
+ }
252
+ /**
253
+ * Build the react-markdown component map for a set of render options.
254
+ *
255
+ * @param options - {@link InstuiMarkdownRenderOptions}.
256
+ * @returns A react-markdown `components` map backed by Instructure UI.
257
+ *
258
+ * @example Pass the map straight to react-markdown
259
+ * ```tsx
260
+ * import Markdown from "react-markdown";
261
+ * import { createInstuiMarkdownComponents } from "@pantoken/react-markdown";
262
+ *
263
+ * const components = createInstuiMarkdownComponents({ tableCaption: "Grades" });
264
+ * <Markdown components={components}># Report</Markdown>;
265
+ * ```
266
+ */
267
+ function createInstuiMarkdownComponents(options = {}) {
268
+ const Span = iconSpan(buildIconResolver(options), options.icons?.color);
269
+ const showExternal = options.link?.external ?? true;
270
+ const showLanguage = options.code?.language ?? true;
271
+ const caption = options.tableCaption ?? "Table";
272
+ return {
273
+ h1: heading("h1"),
274
+ h2: heading("h2"),
275
+ h3: heading("h3"),
276
+ h4: heading("h4"),
277
+ h5: heading("h5"),
278
+ h6: heading("h6"),
279
+ p: ({ children }) => /* @__PURE__ */ jsx(Text, {
280
+ as: "p",
281
+ children
282
+ }),
283
+ strong: ({ children }) => /* @__PURE__ */ jsx(Text, {
284
+ as: "span",
285
+ weight: "bold",
286
+ children
287
+ }),
288
+ em: ({ children }) => /* @__PURE__ */ jsx(Text, {
289
+ as: "span",
290
+ fontStyle: "italic",
291
+ children
292
+ }),
293
+ a: ({ children, ...props }) => {
294
+ const href = str(props.href);
295
+ return /* @__PURE__ */ jsx(Link, {
296
+ href,
297
+ ...showExternal && !!href && /^https?:\/\//.test(href) ? { target: "_blank" } : {},
298
+ children
299
+ });
300
+ },
301
+ ul: ({ children }) => /* @__PURE__ */ jsx(List, {
302
+ as: "ul",
303
+ children
304
+ }),
305
+ ol: ({ children }) => /* @__PURE__ */ jsx(List, {
306
+ as: "ol",
307
+ isUnstyled: false,
308
+ children
309
+ }),
310
+ li: ({ children }) => /* @__PURE__ */ jsx(List.Item, { children }),
311
+ blockquote: ({ children, ...props }) => {
312
+ const alert = str(props["data-alert"]);
313
+ if (options.alerts?.enabled !== false && alert) return /* @__PURE__ */ jsx(Alert, {
314
+ variant: alertVariant(alert.toUpperCase()),
315
+ margin: "small 0",
316
+ children
317
+ });
318
+ return /* @__PURE__ */ jsx(View, {
319
+ as: "blockquote",
320
+ display: "block",
321
+ borderWidth: "none none none large",
322
+ padding: "small medium",
323
+ children
324
+ });
325
+ },
326
+ code: ({ children, ...props }) => {
327
+ const className = str(props.className);
328
+ if (!className && !textOf(children).includes("\n")) return /* @__PURE__ */ jsx(Text, {
329
+ as: "code",
330
+ children
331
+ });
332
+ return createElement("code", {
333
+ className,
334
+ "data-language": showLanguage ? className?.replace(/.*language-/, "") : void 0
335
+ }, children);
336
+ },
337
+ pre: ({ children }) => /* @__PURE__ */ jsx(View, {
338
+ as: "pre",
339
+ display: "block",
340
+ background: "secondary",
341
+ padding: "small",
342
+ borderRadius: "medium",
343
+ children
344
+ }),
345
+ img: ({ ...props }) => /* @__PURE__ */ jsx(Img, {
346
+ src: str(props.src) ?? "",
347
+ alt: str(props.alt) ?? ""
348
+ }),
349
+ hr: () => /* @__PURE__ */ jsx(View, {
350
+ as: "hr",
351
+ display: "block",
352
+ margin: "medium 0"
353
+ }),
354
+ table: ({ children }) => /* @__PURE__ */ jsx(Table, {
355
+ caption,
356
+ layout: "auto",
357
+ children
358
+ }),
359
+ thead: ({ children }) => /* @__PURE__ */ jsx(TableHead, { children }),
360
+ tbody: ({ children }) => /* @__PURE__ */ jsx(TableBody, { children }),
361
+ tr: ({ children }) => /* @__PURE__ */ jsx(TableRow, { children }),
362
+ th: ({ children }) => /* @__PURE__ */ jsx(TableColHeader, {
363
+ id: slug(textOf(children)),
364
+ children
365
+ }),
366
+ td: ({ children }) => /* @__PURE__ */ jsx(TableCell, { children }),
367
+ span: Span
368
+ };
369
+ }
370
+ /** The default component map (no options). */
371
+ const instuiMarkdownComponents = createInstuiMarkdownComponents();
372
+ //#endregion
373
+ export { parseAlertMarker as a, isColorValue as i, instuiMarkdownComponents as n, rehypeColorCodes as o, buildIconResolver as r, rehypeGithubAlerts as s, createInstuiMarkdownComponents as t };
@@ -0,0 +1,124 @@
1
+ import { n as InstuiMarkdownProps, r as InstuiMarkdownRenderOptions, t as AlertMarker } from "./types-CpY9ftGp.mjs";
2
+ import { Components } from "react-markdown";
3
+ import { IconResolver } from "@pantoken/model";
4
+
5
+ //#region src/instui-markdown.d.ts
6
+ /**
7
+ * Render Markdown with Instructure UI element mappings and pantoken icon/color tokens.
8
+ *
9
+ * @param props - {@link InstuiMarkdownProps}.
10
+ *
11
+ * @example Basic
12
+ * ```tsx
13
+ * import { InstuiMarkdown } from "@pantoken/react-markdown";
14
+ *
15
+ * <InstuiMarkdown>{"Go :arrow-left: back. Brand is #03893D.\n\n> [!TIP]\n> Helpful."}</InstuiMarkdown>;
16
+ * ```
17
+ *
18
+ * @example With brand icons via a plugin
19
+ * ```tsx
20
+ * import { InstuiMarkdown } from "@pantoken/react-markdown";
21
+ * import { simpleIcons } from "@pantoken/plugin-simple-icons";
22
+ * import * as registry from "simple-icons";
23
+ *
24
+ * <InstuiMarkdown renderOptions={{ icons: { plugins: [simpleIcons({ registry })] } }}>
25
+ * {"Star us on :github:"}
26
+ * </InstuiMarkdown>;
27
+ * ```
28
+ */
29
+ declare function InstuiMarkdown({
30
+ children,
31
+ renderOptions
32
+ }: InstuiMarkdownProps): React.ReactNode;
33
+ //#endregion
34
+ //#region src/components.d.ts
35
+ /**
36
+ * Build the react-markdown component map for a set of render options.
37
+ *
38
+ * @param options - {@link InstuiMarkdownRenderOptions}.
39
+ * @returns A react-markdown `components` map backed by Instructure UI.
40
+ *
41
+ * @example Pass the map straight to react-markdown
42
+ * ```tsx
43
+ * import Markdown from "react-markdown";
44
+ * import { createInstuiMarkdownComponents } from "@pantoken/react-markdown";
45
+ *
46
+ * const components = createInstuiMarkdownComponents({ tableCaption: "Grades" });
47
+ * <Markdown components={components}># Report</Markdown>;
48
+ * ```
49
+ */
50
+ declare function createInstuiMarkdownComponents(options?: InstuiMarkdownRenderOptions): Components;
51
+ /** The default component map (no options). */
52
+ declare const instuiMarkdownComponents: Components;
53
+ //#endregion
54
+ //#region src/helpers.d.ts
55
+ /**
56
+ * Build the icon-resolver chain: plugin `rehype` resolvers first, then explicit `resolvers`, then
57
+ * the built-in `@pantoken/icons` set. The first match wins.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * import { buildIconResolver } from "@pantoken/react-markdown";
62
+ *
63
+ * const resolve = buildIconResolver();
64
+ * resolve("arrow-left"); // an IconEntry from the built-in set, or undefined
65
+ * ```
66
+ */
67
+ declare function buildIconResolver(options?: InstuiMarkdownRenderOptions): IconResolver;
68
+ /**
69
+ * Detect a GitHub alert marker at the start of text; returns the marker and the remaining text.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * import { parseAlertMarker } from "@pantoken/react-markdown";
74
+ *
75
+ * parseAlertMarker("[!TIP] Helpful"); // { marker: "TIP", rest: "Helpful" }
76
+ * parseAlertMarker("Plain text"); // undefined
77
+ * ```
78
+ */
79
+ declare function parseAlertMarker(text: string): {
80
+ marker: AlertMarker;
81
+ rest: string;
82
+ } | undefined;
83
+ /**
84
+ * True when a string is a standalone CSS color value (hex or a color function).
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * import { isColorValue } from "@pantoken/react-markdown";
89
+ *
90
+ * isColorValue("#03893D"); // true
91
+ * isColorValue("oklch(0.7 0.1 200)"); // true
92
+ * isColorValue("hello"); // false
93
+ * ```
94
+ */
95
+ declare function isColorValue(value: string): boolean;
96
+ /**
97
+ * A rehype plugin that wraps standalone color values in `<span data-color-code="…">`, so the React
98
+ * layer can render a swatch. Mirrors the icon-token pipeline.
99
+ *
100
+ * @example
101
+ * ```tsx
102
+ * import Markdown from "react-markdown";
103
+ * import { rehypeColorCodes } from "@pantoken/react-markdown";
104
+ *
105
+ * <Markdown rehypePlugins={[rehypeColorCodes]}>Brand is #03893D.</Markdown>;
106
+ * ```
107
+ */
108
+ declare function rehypeColorCodes(): (tree: unknown) => void;
109
+ /**
110
+ * A rehype plugin that tags GitHub-alert blockquotes: it detects a `[!NOTE]`-style marker at the
111
+ * start of a blockquote, records it as `data-alert="note"`, and strips the marker text. The React
112
+ * `blockquote` component then renders an InstUI `Alert`.
113
+ *
114
+ * @example
115
+ * ```tsx
116
+ * import Markdown from "react-markdown";
117
+ * import { rehypeGithubAlerts } from "@pantoken/react-markdown";
118
+ *
119
+ * <Markdown rehypePlugins={[rehypeGithubAlerts]}>{"> [!NOTE]\n> Heads up."}</Markdown>;
120
+ * ```
121
+ */
122
+ declare function rehypeGithubAlerts(): (tree: unknown) => void;
123
+ //#endregion
124
+ export { type AlertMarker, InstuiMarkdown, type InstuiMarkdownProps, type InstuiMarkdownRenderOptions, buildIconResolver, createInstuiMarkdownComponents, instuiMarkdownComponents, isColorValue, parseAlertMarker, rehypeColorCodes, rehypeGithubAlerts };
package/dist/index.mjs ADDED
@@ -0,0 +1,59 @@
1
+ import { a as parseAlertMarker, i as isColorValue, n as instuiMarkdownComponents, o as rehypeColorCodes, r as buildIconResolver, s as rehypeGithubAlerts, t as createInstuiMarkdownComponents } from "./components-DWmL2y3Q.mjs";
2
+ import { rehypePantokenIcons } from "@pantoken/rehype";
3
+ import Markdown from "react-markdown";
4
+ import remarkGfm from "remark-gfm";
5
+ import { jsx } from "react/jsx-runtime";
6
+ //#region src/instui-markdown.tsx
7
+ /**
8
+ * The `InstuiMarkdown` React component: renders a Markdown string with Instructure UI components
9
+ * and pantoken icons. It wires react-markdown with GFM, the GitHub-alert and color-code rehype
10
+ * steps, and `@pantoken/rehype` for inline `:icon:` tokens.
11
+ *
12
+ * @module
13
+ */
14
+ /**
15
+ * Render Markdown with Instructure UI element mappings and pantoken icon/color tokens.
16
+ *
17
+ * @param props - {@link InstuiMarkdownProps}.
18
+ *
19
+ * @example Basic
20
+ * ```tsx
21
+ * import { InstuiMarkdown } from "@pantoken/react-markdown";
22
+ *
23
+ * <InstuiMarkdown>{"Go :arrow-left: back. Brand is #03893D.\n\n> [!TIP]\n> Helpful."}</InstuiMarkdown>;
24
+ * ```
25
+ *
26
+ * @example With brand icons via a plugin
27
+ * ```tsx
28
+ * import { InstuiMarkdown } from "@pantoken/react-markdown";
29
+ * import { simpleIcons } from "@pantoken/plugin-simple-icons";
30
+ * import * as registry from "simple-icons";
31
+ *
32
+ * <InstuiMarkdown renderOptions={{ icons: { plugins: [simpleIcons({ registry })] } }}>
33
+ * {"Star us on :github:"}
34
+ * </InstuiMarkdown>;
35
+ * ```
36
+ */
37
+ function InstuiMarkdown({ children, renderOptions }) {
38
+ const components = createInstuiMarkdownComponents(renderOptions);
39
+ const iconsEnabled = renderOptions?.icons?.enabled !== false;
40
+ const colorEnabled = renderOptions?.color?.enabled !== false;
41
+ const alertsEnabled = renderOptions?.alerts?.enabled !== false;
42
+ const resolve = buildIconResolver(renderOptions);
43
+ const rehypePlugins = [
44
+ ...alertsEnabled ? [rehypeGithubAlerts] : [],
45
+ ...iconsEnabled ? [[rehypePantokenIcons, {
46
+ resolve,
47
+ plugins: renderOptions?.icons?.plugins
48
+ }]] : [],
49
+ ...colorEnabled ? [rehypeColorCodes] : []
50
+ ];
51
+ return /* @__PURE__ */ jsx(Markdown, {
52
+ remarkPlugins: [remarkGfm],
53
+ rehypePlugins,
54
+ components,
55
+ children
56
+ });
57
+ }
58
+ //#endregion
59
+ export { InstuiMarkdown, buildIconResolver, createInstuiMarkdownComponents, instuiMarkdownComponents, isColorValue, parseAlertMarker, rehypeColorCodes, rehypeGithubAlerts };
package/dist/mdx.d.mts ADDED
@@ -0,0 +1,30 @@
1
+ import { r as InstuiMarkdownRenderOptions } from "./types-CpY9ftGp.mjs";
2
+ import { ReactNode } from "react";
3
+
4
+ //#region src/mdx.d.ts
5
+ /** Props for {@link InstuiMdxProvider}. */
6
+ interface InstuiMdxProviderProps {
7
+ children: ReactNode;
8
+ renderOptions?: InstuiMarkdownRenderOptions;
9
+ }
10
+ /**
11
+ * Provide the Instructure UI component map to MDX content.
12
+ *
13
+ * @param props - {@link InstuiMdxProviderProps}.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * import { InstuiMdxProvider } from "@pantoken/react-markdown/mdx";
18
+ * import Content from "./doc.mdx";
19
+ *
20
+ * <InstuiMdxProvider>
21
+ * <Content />
22
+ * </InstuiMdxProvider>;
23
+ * ```
24
+ */
25
+ declare function InstuiMdxProvider({
26
+ children,
27
+ renderOptions
28
+ }: InstuiMdxProviderProps): ReactNode;
29
+ //#endregion
30
+ export { InstuiMdxProvider, InstuiMdxProvider as default, InstuiMdxProviderProps };
package/dist/mdx.mjs ADDED
@@ -0,0 +1,33 @@
1
+ import { t as createInstuiMarkdownComponents } from "./components-DWmL2y3Q.mjs";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { MDXProvider } from "@mdx-js/react";
4
+ //#region src/mdx.tsx
5
+ /**
6
+ * `@pantoken/react-markdown/mdx` — an MDX provider that supplies the Instructure UI component map
7
+ * to MDX content. Kept in a separate entry so `@mdx-js/react` stays an optional peer.
8
+ *
9
+ * @module
10
+ */
11
+ /**
12
+ * Provide the Instructure UI component map to MDX content.
13
+ *
14
+ * @param props - {@link InstuiMdxProviderProps}.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * import { InstuiMdxProvider } from "@pantoken/react-markdown/mdx";
19
+ * import Content from "./doc.mdx";
20
+ *
21
+ * <InstuiMdxProvider>
22
+ * <Content />
23
+ * </InstuiMdxProvider>;
24
+ * ```
25
+ */
26
+ function InstuiMdxProvider({ children, renderOptions }) {
27
+ return /* @__PURE__ */ jsx(MDXProvider, {
28
+ components: createInstuiMarkdownComponents(renderOptions),
29
+ children
30
+ });
31
+ }
32
+ //#endregion
33
+ export { InstuiMdxProvider, InstuiMdxProvider as default };
@@ -0,0 +1,44 @@
1
+ import { IconResolver, PantokenPlugin } from "@pantoken/model";
2
+
3
+ //#region src/types.d.ts
4
+ /** GitHub-style blockquote alert markers (`> [!NOTE]`). */
5
+ type AlertMarker = "NOTE" | "TIP" | "IMPORTANT" | "WARNING" | "CAUTION";
6
+ /** Render options that tune how Markdown maps onto Instructure UI. */
7
+ interface InstuiMarkdownRenderOptions {
8
+ /** Link behavior. */
9
+ link?: {
10
+ /** Show an external-link affordance on off-site links (default: true). */external?: boolean; /** Add permalink anchors to headings (default: false). */
11
+ permalinks?: boolean; /** Class name for permalink anchors (default: `pantoken-heading-anchor`). */
12
+ permalinkClassName?: string;
13
+ };
14
+ /** Fenced code block behavior. */
15
+ code?: {
16
+ /** Preserve the language hint as a `data-language` attribute (default: true). */language?: boolean;
17
+ };
18
+ /** Inline `:icon:` token behavior. */
19
+ icons?: {
20
+ /** Enable `:icon:` rendering (default: true). */enabled?: boolean; /** CSS color applied to rendered icons. */
21
+ color?: string; /** Extra resolvers, tried before the built-in pantoken icon set. */
22
+ resolvers?: IconResolver[]; /** Plugins whose `rehype` hooks contribute resolvers (e.g. simple-icons). */
23
+ plugins?: PantokenPlugin[];
24
+ };
25
+ /** Inline color-code swatches (e.g. `#03893D`). */
26
+ color?: {
27
+ /** Enable color swatches (default: true). */enabled?: boolean;
28
+ };
29
+ /** GitHub-style blockquote alerts. */
30
+ alerts?: {
31
+ /** Enable `> [!NOTE]` → InstUI Alert mapping (default: true). */enabled?: boolean;
32
+ };
33
+ /** Caption used for rendered tables (required by InstUI Table for a11y). */
34
+ tableCaption?: string;
35
+ }
36
+ /** Props for the {@link InstuiMarkdown} component. */
37
+ interface InstuiMarkdownProps {
38
+ /** The Markdown source. */
39
+ children: string;
40
+ /** Render options. */
41
+ renderOptions?: InstuiMarkdownRenderOptions;
42
+ }
43
+ //#endregion
44
+ export { InstuiMarkdownProps as n, InstuiMarkdownRenderOptions as r, AlertMarker as t };
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@pantoken/react-markdown",
3
+ "version": "0.1.0",
4
+ "description": "react-markdown integration for pantoken: render Markdown with Instructure UI components and pantoken icons.",
5
+ "license": "MIT",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "type": "module",
10
+ "exports": {
11
+ ".": "./dist/index.mjs",
12
+ "./mdx": "./dist/mdx.mjs",
13
+ "./package.json": "./package.json"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "dependencies": {
19
+ "unist-util-visit": "^5.1.0",
20
+ "@pantoken/icons": "0.1.0",
21
+ "@pantoken/model": "0.1.0",
22
+ "@pantoken/rehype": "0.1.0"
23
+ },
24
+ "devDependencies": {
25
+ "@instructure/ui-alerts": "^11.7.3",
26
+ "@instructure/ui-heading": "^11.7.3",
27
+ "@instructure/ui-img": "^11.7.3",
28
+ "@instructure/ui-link": "^11.7.3",
29
+ "@instructure/ui-list": "^11.7.3",
30
+ "@instructure/ui-table": "^11.7.3",
31
+ "@instructure/ui-text": "^11.7.3",
32
+ "@instructure/ui-view": "^11.7.3",
33
+ "@mdx-js/react": "^3.1.1",
34
+ "@types/node": "^24.13.3",
35
+ "@types/react": "^19.2.17",
36
+ "@types/react-dom": "^19.2.3",
37
+ "core-js": "^3.49.0",
38
+ "react": "^19.2.7",
39
+ "react-dom": "^19.2.7",
40
+ "react-markdown": "^10.1.0",
41
+ "remark-gfm": "^4.0.1",
42
+ "simple-icons": "^15.22.0",
43
+ "typescript": "^6.0.3",
44
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.2.4",
45
+ "vite-plus": "0.2.4",
46
+ "@pantoken/plugin-simple-icons": "0.1.0"
47
+ },
48
+ "peerDependencies": {
49
+ "@instructure/ui-alerts": "*",
50
+ "@instructure/ui-heading": "*",
51
+ "@instructure/ui-img": "*",
52
+ "@instructure/ui-link": "*",
53
+ "@instructure/ui-list": "*",
54
+ "@instructure/ui-table": "*",
55
+ "@instructure/ui-text": "*",
56
+ "@instructure/ui-view": "*",
57
+ "@mdx-js/react": "*",
58
+ "core-js": ">=3",
59
+ "react": ">=18",
60
+ "react-markdown": ">=9",
61
+ "remark-gfm": ">=4"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "@mdx-js/react": {
65
+ "optional": true
66
+ }
67
+ },
68
+ "pantoken": {
69
+ "key": "react-markdown",
70
+ "kind": "subpath"
71
+ },
72
+ "scripts": {
73
+ "build": "vp pack",
74
+ "dev": "vp pack --watch",
75
+ "test": "vp test",
76
+ "check": "vp check"
77
+ }
78
+ }