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 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 InstUI icons from `:iconName:` tokens. Supports Line, Solid, and Lucide icon variants. You can omit the `Icon` prefix and the `Line`/`Solid` suffix `:Heart:`, `:HeartLine:`, and `:IconHeartLine:` all resolve to the same icon.
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 | Type | Default | Description |
130
- | --------- | --------- | ----------- | -------------------------------------------- |
131
- | `enabled` | `boolean` | `false` | Renders `:icon:` tokens as InstUI icons |
132
- | `color` | `string` | `undefined` | Default icon color (hex) for all icon tokens |
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 = 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
- const sizeProps = IconComponent.displayName?.startsWith("InstUIIcon_") ? { size: "x-small" } : {};
467
- if (normalizedIconColor) return /* @__PURE__ */ jsx("span", {
468
- style: { color: normalizedIconColor },
469
- children: /* @__PURE__ */ jsx(IconComponent, {
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
- return /* @__PURE__ */ jsx(IconComponent, {
476
- title: iconName,
477
- color: (resolvedColor === "currentColor" ? "inherit" : resolvedColor) ?? "inherit",
478
- ...sizeProps
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.8",
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.7"
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",