@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 +87 -0
- package/dist/components-DWmL2y3Q.mjs +373 -0
- package/dist/index.d.mts +124 -0
- package/dist/index.mjs +59 -0
- package/dist/mdx.d.mts +30 -0
- package/dist/mdx.mjs +33 -0
- package/dist/types-CpY9ftGp.d.mts +44 -0
- package/package.json +78 -0
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 };
|
package/dist/index.d.mts
ADDED
|
@@ -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
|
+
}
|