instui-markdown 0.0.7 → 0.0.9

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.
Files changed (3) hide show
  1. package/README.md +181 -11
  2. package/dist/index.mjs +86 -26
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,23 +1,193 @@
1
- # vite-plus-starter
1
+ # instui-markdown
2
2
 
3
- A starter for creating a Vite Plus project.
3
+ React components for rendering markdown with [Instructure UI](https://instructure.design) components.
4
4
 
5
- ## Development
6
-
7
- - Install dependencies:
5
+ ## Installation
8
6
 
9
7
  ```bash
10
- vp install
8
+ npm install instui-markdown rehype-instui-markdown
11
9
  ```
12
10
 
13
- - Run the unit tests:
11
+ You also need the InstUI peer dependencies your app will use. At minimum:
14
12
 
15
13
  ```bash
16
- vp test
14
+ npm install react react-markdown remark-gfm rehype-raw rehype-slug \
15
+ @instructure/ui-text @instructure/ui-heading @instructure/ui-link \
16
+ @instructure/ui-list @instructure/ui-view
17
17
  ```
18
18
 
19
- - Build the library:
19
+ ## Usage
20
20
 
21
- ```bash
22
- vp pack
21
+ ### `InstuiMarkdown`
22
+
23
+ Renders a markdown string using InstUI components.
24
+
25
+ ```tsx
26
+ import { InstuiMarkdown } from "instui-markdown";
27
+
28
+ <InstuiMarkdown>{markdownString}</InstuiMarkdown>;
29
+ ```
30
+
31
+ Pass `renderOptions` to control rendering behavior:
32
+
33
+ ```tsx
34
+ <InstuiMarkdown
35
+ renderOptions={{
36
+ link: { permalinks: true, externalIcon: true },
37
+ table: { sortable: true, hover: true },
38
+ code: { lineNumbers: true },
39
+ color: { enabled: true },
40
+ icons: { enabled: true },
41
+ }}
42
+ >
43
+ {markdownString}
44
+ </InstuiMarkdown>
23
45
  ```
46
+
47
+ ### `InstuiMdxProvider`
48
+
49
+ Wraps MDX content and maps MDX elements to InstUI components.
50
+
51
+ ```tsx
52
+ import { InstuiMdxProvider } from "instui-markdown";
53
+ import Content from "./content.mdx";
54
+
55
+ <InstuiMdxProvider renderOptions={renderOptions}>
56
+ <Content />
57
+ </InstuiMdxProvider>;
58
+ ```
59
+
60
+ ### `createInstuiMarkdownComponents`
61
+
62
+ Returns the component map directly if you need to pass it to `react-markdown` or an MDX provider yourself.
63
+
64
+ ```tsx
65
+ import ReactMarkdown from "react-markdown";
66
+ import { createInstuiMarkdownComponents } from "instui-markdown";
67
+
68
+ const components = createInstuiMarkdownComponents(renderOptions);
69
+
70
+ <ReactMarkdown components={components}>{markdown}</ReactMarkdown>;
71
+ ```
72
+
73
+ ## Render options
74
+
75
+ ### `alert`
76
+
77
+ Controls alert-styled blockquotes. Uses [GFM alert syntax](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts): `> [!NOTE]`, `> [!TIP]`, `> [!IMPORTANT]`, `> [!WARNING]`, `> [!CAUTION]`.
78
+
79
+ | Option | Type | Default | Description |
80
+ | ------------- | --------- | ------- | ------------------------------------ |
81
+ | `closeButton` | `boolean` | `false` | Shows a dismiss button on each alert |
82
+
83
+ ### `table`
84
+
85
+ | Option | Type | Default | Description |
86
+ | ---------- | -------------------------------- | -------- | ------------------------------- |
87
+ | `display` | `"auto" \| "stacked" \| "fixed"` | `"auto"` | InstUI table layout mode |
88
+ | `hover` | `boolean` | `false` | Highlights rows on hover |
89
+ | `sortable` | `boolean` | `false` | Enables sortable column headers |
90
+
91
+ ### `link`
92
+
93
+ | Option | Type | Default | Description |
94
+ | -------------------- | --------- | ---------------------- | ------------------------------------------ |
95
+ | `externalIcon` | `boolean` | `false` | Adds an icon to external links |
96
+ | `permalinks` | `boolean` | `false` | Appends a permalink anchor to each heading |
97
+ | `permalinkClassName` | `string` | `"mdx-heading-anchor"` | CSS class on permalink anchor elements |
98
+
99
+ ### `code`
100
+
101
+ Fenced code blocks render using the InstUI `SourceCodeEditor`. Supported languages: `json`, `yaml`, `markdown`, `css`, `html`, `javascript` (and aliases `js`, `jsx`, `ts`, `tsx`, `yml`).
102
+
103
+ | Option | Type | Default | Description |
104
+ | --------------------- | ---------------- | ------- | -------------------------------------- |
105
+ | `editable` | `boolean` | `false` | Allows editing code blocks |
106
+ | `readOnly` | `boolean` | `true` | Marks code blocks as read-only |
107
+ | `lineNumbers` | `boolean` | `false` | Shows line numbers |
108
+ | `lineWrapping` | `boolean` | `false` | Wraps long lines |
109
+ | `spellCheck` | `boolean` | `false` | Enables spell check in editable blocks |
110
+ | `highlightActiveLine` | `boolean` | `false` | Highlights the cursor's line |
111
+ | `direction` | `"ltr" \| "rtl"` | `"ltr"` | Text direction |
112
+
113
+ ### `color`
114
+
115
+ Wraps color literals in a visual swatch. Supports `#hex`, `rgb()`, `rgba()`, `hsl()`, and `hsla()` formats. Skips color values inside code fences.
116
+
117
+ | Option | Type | Default | Description |
118
+ | --------- | --------- | ------- | ----------------------------- |
119
+ | `enabled` | `boolean` | `false` | Renders color swatches inline |
120
+
121
+ ### `icons`
122
+
123
+ Renders icon tokens from `:iconName:` syntax. InstUI lookup runs first, then optional SimpleIcons lookup can run as a fallback through an app-provided resolver. You can omit the `Icon` prefix and the `Line`/`Solid` suffix for InstUI names, so `:Heart:`, `:HeartLine:`, and `:IconHeartLine:` all resolve to the same InstUI icon.
124
+
125
+ Add an optional hex color with a pipe: `:Heart|#F00:`.
126
+
127
+ Skips icon tokens inside code fences.
128
+
129
+ | Option | Type | Default | Description |
130
+ | ----------------------- | --------------------------------------- | ----------- | ------------------------------------------------------------------ |
131
+ | `enabled` | `boolean` | `false` | Enables token-based icon rendering |
132
+ | `color` | `string` | `undefined` | Default color for all icon providers |
133
+ | `providers.instui` | `boolean` | `true` | Enables InstUI lookup (`@instructure/ui-icons`) |
134
+ | `providers.simpleIcons` | `boolean` | `true` | Enables SimpleIcons fallback lookup |
135
+ | `simpleIcons.color` | `string` | `undefined` | Default color for SimpleIcons output (falls back to `icons.color`) |
136
+ | `simpleIcons.resolve` | `(code: string) => SimpleIconTokenData` | `undefined` | App-provided resolver for SimpleIcons SVG path data |
137
+
138
+ Example with SimpleIcons fallback:
139
+
140
+ ```tsx
141
+ <InstuiMarkdown
142
+ renderOptions={{
143
+ icons: {
144
+ enabled: true,
145
+ providers: { instui: true, simpleIcons: true },
146
+ simpleIcons: {
147
+ resolve: (code) => {
148
+ if (code.toLowerCase() === "github") {
149
+ return {
150
+ title: "GitHub",
151
+ path: "M12 0C5.37 0 0 5.37 0 12...",
152
+ viewBox: "0 0 24 24",
153
+ };
154
+ }
155
+ return undefined;
156
+ },
157
+ },
158
+ },
159
+ }}
160
+ >
161
+ {":GitHub: and :HeartLine:"}
162
+ </InstuiMarkdown>
163
+ ```
164
+
165
+ If no enabled provider can resolve a token, the original token text is left in place.
166
+
167
+ ## Peer dependencies
168
+
169
+ `instui-markdown` requires the following peer dependencies. Install only what your content uses — the components map each markdown element to the matching InstUI package.
170
+
171
+ | Package | Used for |
172
+ | ------------------------------------ | ------------------------------------- |
173
+ | `react` | All components |
174
+ | `react-markdown` | Markdown parsing |
175
+ | `remark-gfm` | GFM tables, task lists, strikethrough |
176
+ | `rehype-raw` | Inline HTML in markdown |
177
+ | `rehype-slug` | Heading IDs |
178
+ | `rehype-autolink-headings` | Heading permalink anchors |
179
+ | `@mdx-js/react` | `InstuiMdxProvider` |
180
+ | `@instructure/ui-alerts` | `> [!NOTE]` and other alerts |
181
+ | `@instructure/ui-badge` | Color swatch badges |
182
+ | `@instructure/ui-checkbox` | Task list items |
183
+ | `@instructure/ui-color-utils` | Color contrast for swatches |
184
+ | `@instructure/ui-heading` | `h1`–`h6` |
185
+ | `@instructure/ui-icons` | Icon tokens and permalink icons |
186
+ | `@instructure/ui-img` | Images |
187
+ | `@instructure/ui-link` | Links |
188
+ | `@instructure/ui-list` | Ordered and unordered lists |
189
+ | `@instructure/ui-source-code-editor` | Fenced code blocks |
190
+ | `@instructure/ui-svg-images` | Inline SVG elements |
191
+ | `@instructure/ui-table` | Tables |
192
+ | `@instructure/ui-text` | Paragraphs and inline text |
193
+ | `@instructure/ui-view` | Block wrappers and horizontal rules |
package/dist/index.mjs CHANGED
@@ -442,38 +442,90 @@ function createBlockquoteComponent(options) {
442
442
  }
443
443
  //#endregion
444
444
  //#region src/components/span-component.tsx
445
+ function toPascalCase(str) {
446
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
447
+ }
448
+ function parseIconTokenName(iconName) {
449
+ const withoutPrefix = iconName.startsWith("Icon") ? iconName.slice(4) : iconName;
450
+ const isSolid = withoutPrefix.endsWith("Solid");
451
+ const isInstUI = withoutPrefix.endsWith("InstUIIcon");
452
+ let root = withoutPrefix;
453
+ if (isSolid) root = withoutPrefix.slice(0, -5);
454
+ else if (isInstUI) root = withoutPrefix.slice(0, -10);
455
+ else if (withoutPrefix.endsWith("Line")) root = withoutPrefix.slice(0, -4);
456
+ return {
457
+ withoutPrefix,
458
+ isSolid,
459
+ isInstUI,
460
+ root
461
+ };
462
+ }
463
+ function getSimpleIconCodes(iconName, root, withoutPrefix) {
464
+ const camel = root.charAt(0).toLowerCase() + root.slice(1);
465
+ const lower = root.toLowerCase();
466
+ return Array.from(new Set([
467
+ iconName,
468
+ withoutPrefix,
469
+ root,
470
+ camel,
471
+ lower
472
+ ]));
473
+ }
445
474
  function createSpanComponent(options) {
446
475
  return ({ children, className, ...props }) => {
447
476
  if (className?.includes("icon-token") && options.showIcons) {
448
477
  const iconName = props["data-icon"];
449
478
  const inlineIconColor = props["data-icon-color"];
450
479
  if (iconName) {
451
- const withoutPrefix = iconName.startsWith("Icon") ? iconName.slice(4) : iconName;
452
- const isSolid = withoutPrefix.endsWith("Solid");
453
- const isInstUI = withoutPrefix.endsWith("InstUIIcon");
454
- let root = withoutPrefix;
455
- if (isSolid) root = withoutPrefix.slice(0, -5);
456
- else if (isInstUI) root = withoutPrefix.slice(0, -10);
457
- else if (withoutPrefix.endsWith("Line")) root = withoutPrefix.slice(0, -4);
458
- const IconComponent = (isSolid ? [`Icon${root}Solid`] : isInstUI ? [`${root}InstUIIcon`] : [
459
- `${root}InstUIIcon`,
460
- `Icon${root}Line`,
461
- `Icon${root}`
462
- ]).map((name) => InstUIIcons[name]).find(Boolean);
463
- if (IconComponent) {
464
- const resolvedColor = inlineIconColor ?? options.iconColor;
465
- const normalizedIconColor = resolvedColor && isLiteralColorValue(resolvedColor) ? normalizeColorForSwatch(resolvedColor) : void 0;
466
- if (normalizedIconColor) return /* @__PURE__ */ jsx("span", {
467
- style: { color: normalizedIconColor },
468
- children: /* @__PURE__ */ jsx(IconComponent, {
480
+ const { withoutPrefix, isSolid, isInstUI, root } = parseIconTokenName(iconName);
481
+ if (options.enableInstuiIcons) {
482
+ const normalizedRoot = toPascalCase(root);
483
+ const IconComponent = (isSolid ? [`Icon${normalizedRoot}Solid`] : isInstUI ? [`${normalizedRoot}InstUIIcon`] : [
484
+ `${normalizedRoot}InstUIIcon`,
485
+ `Icon${normalizedRoot}Line`,
486
+ `Icon${normalizedRoot}`
487
+ ]).map((name) => InstUIIcons[name]).find(Boolean);
488
+ if (IconComponent) {
489
+ const resolvedColor = inlineIconColor ?? options.iconColor;
490
+ const normalizedIconColor = resolvedColor && isLiteralColorValue(resolvedColor) ? normalizeColorForSwatch(resolvedColor) : void 0;
491
+ const sizeProps = IconComponent.displayName?.startsWith("InstUIIcon_") ? { size: "x-small" } : {};
492
+ if (normalizedIconColor) return /* @__PURE__ */ jsx("span", {
493
+ style: { color: normalizedIconColor },
494
+ children: /* @__PURE__ */ jsx(IconComponent, {
495
+ title: iconName,
496
+ color: "inherit",
497
+ ...sizeProps
498
+ })
499
+ });
500
+ return /* @__PURE__ */ jsx(IconComponent, {
469
501
  title: iconName,
470
- color: "inherit"
471
- })
472
- });
473
- return /* @__PURE__ */ jsx(IconComponent, {
474
- title: iconName,
475
- color: (resolvedColor === "currentColor" ? "inherit" : resolvedColor) ?? "inherit"
476
- });
502
+ color: (resolvedColor === "currentColor" ? "inherit" : resolvedColor) ?? "inherit",
503
+ ...sizeProps
504
+ });
505
+ }
506
+ }
507
+ if (options.enableSimpleIcons && options.resolveSimpleIcon) {
508
+ const simpleIcon = getSimpleIconCodes(iconName, root, withoutPrefix).map((code) => options.resolveSimpleIcon?.(code)).find(Boolean);
509
+ if (simpleIcon?.path) {
510
+ const resolvedColor = inlineIconColor ?? options.simpleIconColor ?? options.iconColor;
511
+ const normalizedIconColor = resolvedColor && isLiteralColorValue(resolvedColor) ? normalizeColorForSwatch(resolvedColor) : void 0;
512
+ const label = simpleIcon.title ?? root;
513
+ return /* @__PURE__ */ jsx("span", {
514
+ style: normalizedIconColor ? { color: normalizedIconColor } : void 0,
515
+ children: /* @__PURE__ */ jsxs(InlineSVG, {
516
+ inline: false,
517
+ viewBox: simpleIcon.viewBox ?? "0 0 24 24",
518
+ width: simpleIcon.width ?? "1em",
519
+ height: simpleIcon.height ?? "1em",
520
+ role: "img",
521
+ "aria-label": label,
522
+ children: [/* @__PURE__ */ jsx("title", { children: label }), /* @__PURE__ */ jsx("path", {
523
+ d: simpleIcon.path,
524
+ fill: "currentColor"
525
+ })]
526
+ })
527
+ });
528
+ }
477
529
  }
478
530
  }
479
531
  }
@@ -651,11 +703,19 @@ function createInstuiMarkdownComponents(renderOptions = {}) {
651
703
  const showColorCodes = Boolean(renderOptions.color?.enabled);
652
704
  const showIcons = Boolean(renderOptions.icons?.enabled);
653
705
  const iconColor = renderOptions.icons?.color;
706
+ const enableInstuiIcons = renderOptions.icons?.providers?.instui ?? true;
707
+ const enableSimpleIcons = renderOptions.icons?.providers?.simpleIcons ?? true;
708
+ const simpleIconColor = renderOptions.icons?.simpleIcons?.color;
709
+ const resolveSimpleIcon = renderOptions.icons?.simpleIcons?.resolve;
654
710
  return {
655
711
  span: createSpanComponent({
656
712
  showColorCodes,
657
713
  showIcons,
658
- iconColor
714
+ iconColor,
715
+ enableInstuiIcons,
716
+ enableSimpleIcons,
717
+ simpleIconColor,
718
+ resolveSimpleIcon
659
719
  }),
660
720
  a: createLinkComponent({
661
721
  showExternalIcon,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instui-markdown",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "InstUI markdown renderer components",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -21,7 +21,7 @@
21
21
  "access": "public"
22
22
  },
23
23
  "dependencies": {
24
- "rehype-instui-markdown": "0.0.7"
24
+ "rehype-instui-markdown": "0.0.9"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^24",