instui-markdown 0.0.8 → 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 +38 -5
- package/dist/index.mjs +144 -29
- package/index.d.mts +25 -0
- package/index.d.ts +25 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -120,16 +120,49 @@ Wraps color literals in a visual swatch. Supports `#hex`, `rgb()`, `rgba()`, `hs
|
|
|
120
120
|
|
|
121
121
|
### `icons`
|
|
122
122
|
|
|
123
|
-
Renders
|
|
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
124
|
|
|
125
125
|
Add an optional hex color with a pipe: `:Heart|#F00:`.
|
|
126
126
|
|
|
127
127
|
Skips icon tokens inside code fences.
|
|
128
128
|
|
|
129
|
-
| Option
|
|
130
|
-
|
|
|
131
|
-
| `enabled`
|
|
132
|
-
| `color`
|
|
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.
|
|
133
166
|
|
|
134
167
|
## Peer dependencies
|
|
135
168
|
|
package/dist/index.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import rehypeRaw from "rehype-raw";
|
|
|
5
5
|
import rehypeSlug from "rehype-slug";
|
|
6
6
|
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
|
7
7
|
import { rehypeColorCodes, rehypeInstUIIconTokens, rehypeUnwrapBlockquoteParagraphs } from "rehype-instui-markdown";
|
|
8
|
+
import * as SimpleIcons from "simple-icons";
|
|
8
9
|
import { Text } from "@instructure/ui-text/v11_7";
|
|
9
10
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
11
|
import { Link } from "@instructure/ui-link/v11_7";
|
|
@@ -22,6 +23,50 @@ import { Badge } from "@instructure/ui-badge/v11_7";
|
|
|
22
23
|
import { SourceCodeEditor } from "@instructure/ui-source-code-editor/v11_7";
|
|
23
24
|
import { Alert } from "@instructure/ui-alerts/v11_7";
|
|
24
25
|
import { Table } from "@instructure/ui-table/v11_7";
|
|
26
|
+
//#region src/simple-icons-resolver.ts
|
|
27
|
+
function isSimpleIconLike(value) {
|
|
28
|
+
return typeof value === "object" && value !== null && "path" in value && "title" in value && typeof value.path === "string";
|
|
29
|
+
}
|
|
30
|
+
function normalizeSimpleIconCode(code) {
|
|
31
|
+
return code.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
32
|
+
}
|
|
33
|
+
function toSimpleIconExportName(code) {
|
|
34
|
+
return `si${code.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^a-zA-Z0-9]+/).filter(Boolean).map((word) => word.toLowerCase()).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("")}`;
|
|
35
|
+
}
|
|
36
|
+
function buildSimpleIconLookup(iconsRegistry) {
|
|
37
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
38
|
+
for (const [exportName, iconCandidate] of Object.entries(iconsRegistry)) {
|
|
39
|
+
if (!exportName.startsWith("si") || !isSimpleIconLike(iconCandidate)) continue;
|
|
40
|
+
const slug = iconCandidate.slug ?? exportName.slice(2);
|
|
41
|
+
lookup.set(normalizeSimpleIconCode(slug), {
|
|
42
|
+
title: iconCandidate.title,
|
|
43
|
+
path: iconCandidate.path,
|
|
44
|
+
viewBox: iconCandidate.viewBox ?? "0 0 24 24"
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return lookup;
|
|
48
|
+
}
|
|
49
|
+
function createSimpleIconsResolver(iconsRegistry) {
|
|
50
|
+
const lookup = buildSimpleIconLookup(iconsRegistry);
|
|
51
|
+
return (code) => {
|
|
52
|
+
const normalizedCode = normalizeSimpleIconCode(code);
|
|
53
|
+
const normalizedWithoutPrefix = normalizedCode.startsWith("si") ? normalizedCode.slice(2) : normalizedCode;
|
|
54
|
+
const bySlug = lookup.get(normalizedCode) ?? (normalizedCode.startsWith("si") ? lookup.get(normalizedWithoutPrefix) : void 0);
|
|
55
|
+
if (bySlug) return bySlug;
|
|
56
|
+
const exportNameCandidates = [toSimpleIconExportName(code)];
|
|
57
|
+
if (normalizedCode.startsWith("si")) exportNameCandidates.push(toSimpleIconExportName(normalizedWithoutPrefix));
|
|
58
|
+
for (const exportName of exportNameCandidates) {
|
|
59
|
+
const iconCandidate = iconsRegistry[exportName];
|
|
60
|
+
if (isSimpleIconLike(iconCandidate)) return {
|
|
61
|
+
title: iconCandidate.title,
|
|
62
|
+
path: iconCandidate.path,
|
|
63
|
+
viewBox: iconCandidate.viewBox ?? "0 0 24 24"
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const resolveSimpleIconToken = createSimpleIconsResolver(SimpleIcons);
|
|
69
|
+
//#endregion
|
|
25
70
|
//#region src/components/text-components.tsx
|
|
26
71
|
function createEmComponent() {
|
|
27
72
|
return ({ children }) => /* @__PURE__ */ jsx(Text, {
|
|
@@ -442,41 +487,104 @@ function createBlockquoteComponent(options) {
|
|
|
442
487
|
}
|
|
443
488
|
//#endregion
|
|
444
489
|
//#region src/components/span-component.tsx
|
|
490
|
+
const INSTUI_ICON_LOOKUP = new Map(Object.entries(InstUIIcons).map(([name, value]) => [name.toLowerCase(), value]));
|
|
491
|
+
function findInstUIIcon(candidates) {
|
|
492
|
+
for (const candidate of candidates) {
|
|
493
|
+
const IconComponent = INSTUI_ICON_LOOKUP.get(candidate.toLowerCase());
|
|
494
|
+
if (IconComponent) return IconComponent;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function parseIconTokenName(iconName) {
|
|
498
|
+
const withoutPrefix = iconName.startsWith("Icon") ? iconName.slice(4) : iconName;
|
|
499
|
+
const isSolid = withoutPrefix.endsWith("Solid");
|
|
500
|
+
const isInstUI = withoutPrefix.endsWith("InstUIIcon");
|
|
501
|
+
let root = withoutPrefix;
|
|
502
|
+
if (isSolid) root = withoutPrefix.slice(0, -5);
|
|
503
|
+
else if (isInstUI) root = withoutPrefix.slice(0, -10);
|
|
504
|
+
else if (withoutPrefix.endsWith("Line")) root = withoutPrefix.slice(0, -4);
|
|
505
|
+
return {
|
|
506
|
+
withoutPrefix,
|
|
507
|
+
isSolid,
|
|
508
|
+
isInstUI,
|
|
509
|
+
root
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function getSimpleIconCodes(iconName, root, withoutPrefix) {
|
|
513
|
+
const lowerWords = root.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^a-zA-Z0-9]+/).filter(Boolean).map((word) => word.toLowerCase());
|
|
514
|
+
const compact = lowerWords.join("");
|
|
515
|
+
const kebab = lowerWords.join("-");
|
|
516
|
+
const camel = root.charAt(0).toLowerCase() + root.slice(1);
|
|
517
|
+
const lower = root.toLowerCase();
|
|
518
|
+
return Array.from(new Set([
|
|
519
|
+
iconName,
|
|
520
|
+
withoutPrefix,
|
|
521
|
+
root,
|
|
522
|
+
camel,
|
|
523
|
+
lower,
|
|
524
|
+
compact,
|
|
525
|
+
kebab
|
|
526
|
+
]));
|
|
527
|
+
}
|
|
445
528
|
function createSpanComponent(options) {
|
|
446
529
|
return ({ children, className, ...props }) => {
|
|
447
530
|
if (className?.includes("icon-token") && options.showIcons) {
|
|
448
531
|
const iconName = props["data-icon"];
|
|
449
532
|
const inlineIconColor = props["data-icon-color"];
|
|
450
533
|
if (iconName) {
|
|
451
|
-
const withoutPrefix
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
534
|
+
const { withoutPrefix, isSolid, isInstUI, root } = parseIconTokenName(iconName);
|
|
535
|
+
if (options.enableInstuiIcons) {
|
|
536
|
+
const IconComponent = findInstUIIcon(isSolid ? [`Icon${root}Solid`] : isInstUI ? [`${root}InstUIIcon`] : [
|
|
537
|
+
`${root}InstUIIcon`,
|
|
538
|
+
`Icon${root}Line`,
|
|
539
|
+
`Icon${root}`
|
|
540
|
+
]);
|
|
541
|
+
if (IconComponent) {
|
|
542
|
+
const resolvedColor = inlineIconColor ?? options.iconColor;
|
|
543
|
+
const normalizedIconColor = resolvedColor && isLiteralColorValue(resolvedColor) ? normalizeColorForSwatch(resolvedColor) : void 0;
|
|
544
|
+
const sizeProps = IconComponent.displayName?.startsWith("InstUIIcon_") ? { size: "x-small" } : {};
|
|
545
|
+
if (normalizedIconColor) return /* @__PURE__ */ jsx("span", {
|
|
546
|
+
style: { color: normalizedIconColor },
|
|
547
|
+
children: /* @__PURE__ */ jsx(IconComponent, {
|
|
548
|
+
title: iconName,
|
|
549
|
+
color: "inherit",
|
|
550
|
+
...sizeProps
|
|
551
|
+
})
|
|
552
|
+
});
|
|
553
|
+
return /* @__PURE__ */ jsx(IconComponent, {
|
|
470
554
|
title: iconName,
|
|
471
|
-
color: "inherit",
|
|
555
|
+
color: (resolvedColor === "currentColor" ? "inherit" : resolvedColor) ?? "inherit",
|
|
472
556
|
...sizeProps
|
|
473
|
-
})
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (options.enableSimpleIcons && options.resolveSimpleIcon) {
|
|
561
|
+
const simpleIcon = getSimpleIconCodes(iconName, root, withoutPrefix).map((code) => options.resolveSimpleIcon?.(code)).find(Boolean);
|
|
562
|
+
if (simpleIcon?.path) {
|
|
563
|
+
const resolvedColor = inlineIconColor ?? options.simpleIconColor ?? options.iconColor;
|
|
564
|
+
const normalizedIconColor = resolvedColor && isLiteralColorValue(resolvedColor) ? normalizeColorForSwatch(resolvedColor) : void 0;
|
|
565
|
+
const label = simpleIcon.title ?? root;
|
|
566
|
+
return /* @__PURE__ */ jsx("span", {
|
|
567
|
+
style: {
|
|
568
|
+
display: "inline-flex",
|
|
569
|
+
alignItems: "center",
|
|
570
|
+
verticalAlign: "middle",
|
|
571
|
+
lineHeight: 1,
|
|
572
|
+
...normalizedIconColor ? { color: normalizedIconColor } : {}
|
|
573
|
+
},
|
|
574
|
+
children: /* @__PURE__ */ jsxs(InlineSVG, {
|
|
575
|
+
inline: true,
|
|
576
|
+
viewBox: simpleIcon.viewBox ?? "0 0 24 24",
|
|
577
|
+
width: simpleIcon.width ?? "1.125em",
|
|
578
|
+
height: simpleIcon.height ?? "1.125em",
|
|
579
|
+
role: "img",
|
|
580
|
+
"aria-label": label,
|
|
581
|
+
children: [/* @__PURE__ */ jsx("title", { children: label }), /* @__PURE__ */ jsx("path", {
|
|
582
|
+
d: simpleIcon.path,
|
|
583
|
+
fill: "currentColor"
|
|
584
|
+
})]
|
|
585
|
+
})
|
|
586
|
+
});
|
|
587
|
+
}
|
|
480
588
|
}
|
|
481
589
|
}
|
|
482
590
|
}
|
|
@@ -654,11 +762,18 @@ function createInstuiMarkdownComponents(renderOptions = {}) {
|
|
|
654
762
|
const showColorCodes = Boolean(renderOptions.color?.enabled);
|
|
655
763
|
const showIcons = Boolean(renderOptions.icons?.enabled);
|
|
656
764
|
const iconColor = renderOptions.icons?.color;
|
|
765
|
+
const enableInstuiIcons = renderOptions.icons?.providers?.instui ?? true;
|
|
766
|
+
const enableSimpleIcons = renderOptions.icons?.providers?.simpleIcons ?? true;
|
|
767
|
+
const simpleIconColor = renderOptions.icons?.simpleIcons?.color;
|
|
657
768
|
return {
|
|
658
769
|
span: createSpanComponent({
|
|
659
770
|
showColorCodes,
|
|
660
771
|
showIcons,
|
|
661
|
-
iconColor
|
|
772
|
+
iconColor,
|
|
773
|
+
enableInstuiIcons,
|
|
774
|
+
enableSimpleIcons,
|
|
775
|
+
simpleIconColor,
|
|
776
|
+
resolveSimpleIcon: renderOptions.icons?.simpleIcons?.resolve ?? resolveSimpleIconToken
|
|
662
777
|
}),
|
|
663
778
|
a: createLinkComponent({
|
|
664
779
|
showExternalIcon,
|
|
@@ -753,4 +868,4 @@ function InstuiMdxProvider({ children, renderOptions }) {
|
|
|
753
868
|
});
|
|
754
869
|
}
|
|
755
870
|
//#endregion
|
|
756
|
-
export { InstuiMarkdown, InstuiMdxProvider, createInstuiMarkdownComponents, instuiMarkdownComponents };
|
|
871
|
+
export { InstuiMarkdown, InstuiMdxProvider, createInstuiMarkdownComponents, createSimpleIconsResolver, instuiMarkdownComponents, resolveSimpleIconToken };
|
package/index.d.mts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import type { Components } from "react-markdown";
|
|
3
3
|
|
|
4
|
+
export type SimpleIconResolver = (code: string) => SimpleIconTokenData | undefined;
|
|
5
|
+
export type SimpleIconsRegistry = Record<string, unknown>;
|
|
6
|
+
|
|
4
7
|
export interface InstuiMarkdownRenderOptions {
|
|
5
8
|
alert?: {
|
|
6
9
|
closeButton?: boolean;
|
|
@@ -30,9 +33,25 @@ export interface InstuiMarkdownRenderOptions {
|
|
|
30
33
|
icons?: {
|
|
31
34
|
enabled?: boolean;
|
|
32
35
|
color?: string;
|
|
36
|
+
providers?: {
|
|
37
|
+
instui?: boolean;
|
|
38
|
+
simpleIcons?: boolean;
|
|
39
|
+
};
|
|
40
|
+
simpleIcons?: {
|
|
41
|
+
color?: string;
|
|
42
|
+
resolve?: (code: string) => SimpleIconTokenData | undefined;
|
|
43
|
+
};
|
|
33
44
|
};
|
|
34
45
|
}
|
|
35
46
|
|
|
47
|
+
export interface SimpleIconTokenData {
|
|
48
|
+
path: string;
|
|
49
|
+
title?: string;
|
|
50
|
+
viewBox?: string;
|
|
51
|
+
width?: string | number;
|
|
52
|
+
height?: string | number;
|
|
53
|
+
}
|
|
54
|
+
|
|
36
55
|
export interface InstuiMarkdownProps {
|
|
37
56
|
children: string;
|
|
38
57
|
renderOptions?: InstuiMarkdownRenderOptions;
|
|
@@ -52,3 +71,9 @@ export declare function createInstuiMarkdownComponents(
|
|
|
52
71
|
export declare function InstuiMarkdown(props: InstuiMarkdownProps): ReactNode;
|
|
53
72
|
|
|
54
73
|
export declare function InstuiMdxProvider(props: InstuiMdxProviderProps): ReactNode;
|
|
74
|
+
|
|
75
|
+
export declare function createSimpleIconsResolver(
|
|
76
|
+
iconsRegistry: SimpleIconsRegistry,
|
|
77
|
+
): SimpleIconResolver;
|
|
78
|
+
|
|
79
|
+
export declare const resolveSimpleIconToken: SimpleIconResolver;
|
package/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import type { Components } from "react-markdown";
|
|
3
3
|
|
|
4
|
+
export type SimpleIconResolver = (code: string) => SimpleIconTokenData | undefined;
|
|
5
|
+
export type SimpleIconsRegistry = Record<string, unknown>;
|
|
6
|
+
|
|
4
7
|
export interface InstuiMarkdownRenderOptions {
|
|
5
8
|
alert?: {
|
|
6
9
|
closeButton?: boolean;
|
|
@@ -30,9 +33,25 @@ export interface InstuiMarkdownRenderOptions {
|
|
|
30
33
|
icons?: {
|
|
31
34
|
enabled?: boolean;
|
|
32
35
|
color?: string;
|
|
36
|
+
providers?: {
|
|
37
|
+
instui?: boolean;
|
|
38
|
+
simpleIcons?: boolean;
|
|
39
|
+
};
|
|
40
|
+
simpleIcons?: {
|
|
41
|
+
color?: string;
|
|
42
|
+
resolve?: (code: string) => SimpleIconTokenData | undefined;
|
|
43
|
+
};
|
|
33
44
|
};
|
|
34
45
|
}
|
|
35
46
|
|
|
47
|
+
export interface SimpleIconTokenData {
|
|
48
|
+
path: string;
|
|
49
|
+
title?: string;
|
|
50
|
+
viewBox?: string;
|
|
51
|
+
width?: string | number;
|
|
52
|
+
height?: string | number;
|
|
53
|
+
}
|
|
54
|
+
|
|
36
55
|
export interface InstuiMarkdownProps {
|
|
37
56
|
children: string;
|
|
38
57
|
renderOptions?: InstuiMarkdownRenderOptions;
|
|
@@ -52,3 +71,9 @@ export declare function createInstuiMarkdownComponents(
|
|
|
52
71
|
export declare function InstuiMarkdown(props: InstuiMarkdownProps): ReactNode;
|
|
53
72
|
|
|
54
73
|
export declare function InstuiMdxProvider(props: InstuiMdxProviderProps): ReactNode;
|
|
74
|
+
|
|
75
|
+
export declare function createSimpleIconsResolver(
|
|
76
|
+
iconsRegistry: SimpleIconsRegistry,
|
|
77
|
+
): SimpleIconResolver;
|
|
78
|
+
|
|
79
|
+
export declare const resolveSimpleIconToken: SimpleIconResolver;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "instui-markdown",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
|
24
|
+
"rehype-instui-markdown": "0.1.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^24",
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
"rehype-autolink-headings": "*",
|
|
52
52
|
"rehype-raw": "*",
|
|
53
53
|
"rehype-slug": "*",
|
|
54
|
-
"remark-gfm": "*"
|
|
54
|
+
"remark-gfm": "*",
|
|
55
|
+
"simple-icons": "*"
|
|
55
56
|
},
|
|
56
57
|
"scripts": {
|
|
57
58
|
"build": "vp pack",
|