@surrealdb/ui 1.2.2 → 1.2.4

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 (65) hide show
  1. package/.picto-fix-png/Cursor.png +0 -0
  2. package/.picto-fix-png/DocumentGradient.png +0 -0
  3. package/.picto-fix-png/FileGradient.png +0 -0
  4. package/.picto-fix-png/HealthBubbleGradient.png +0 -0
  5. package/.picto-fix-png/LearnGradient.png +0 -0
  6. package/.picto-fix-png/MediaFileGradient.png +0 -0
  7. package/README.md +16 -0
  8. package/REVIEW.md +2 -2
  9. package/dist/assets/00f479574770406b.webp +0 -0
  10. package/dist/assets/3e3150054c482d19.webp +0 -0
  11. package/dist/assets/4ab8aa6d05f81b2d.webp +0 -0
  12. package/dist/assets/9af7119697945bfe.webp +0 -0
  13. package/dist/assets/b8c39a75a6dfb50a.webp +0 -0
  14. package/dist/assets/b9123e22ce5345b5.webp +0 -0
  15. package/dist/assets/bb014a59e501b200.webp +0 -0
  16. package/dist/assets/bffb52641c4c3861.webp +0 -0
  17. package/dist/assets/ca65cd02fa429a01.webp +0 -0
  18. package/dist/assets/d95b9e2207cd1bbb.webp +0 -0
  19. package/dist/assets/e58704a124b17f9c.webp +0 -0
  20. package/dist/assets/e731247869f3839b.webp +0 -0
  21. package/dist/assets/f4cdd6dcd9f393b8.webp +0 -0
  22. package/dist/assets/fd4bfe9daca141f1.webp +0 -0
  23. package/dist/ui.css +1 -1
  24. package/dist/ui.d.ts +285 -110
  25. package/dist/ui.js +7036 -6280
  26. package/dist/ui.js.map +1 -1
  27. package/package.json +1 -1
  28. package/tests/e2e/MarkdownEditor/__screenshots__/jsx-inline-click.test.tsx/MarkdownEditor---inline-JSX-click-reveal-reveals-the-raw-source-when-an-inline-component-badge-is-clicked-1.png +0 -0
  29. package/tests/e2e/MarkdownEditor/__screenshots__/jsx-inline-click.test.tsx/MarkdownEditor---inline-JSX-click-reveal-reveals-the-raw-source-when-an-inline-component-badge-is-clicked-2.png +0 -0
  30. package/tests/e2e/MarkdownEditor/__screenshots__/jsx-inline-click.test.tsx/MarkdownEditor---inline-JSX-click-reveal-reveals-the-raw-source-when-an-inline-component-badge-is-clicked-3.png +0 -0
  31. package/tests/e2e/MarkdownEditor/image-on-image.test.tsx +72 -0
  32. package/tests/e2e/MarkdownEditor/jsx-inline-click.test.tsx +53 -0
  33. package/tests/e2e/MarkdownEditor/onimage-parity.test.tsx +49 -0
  34. package/tests/e2e/MarkdownEditor/regressions.test.tsx +40 -0
  35. package/tests/unit/MarkdownConformance/error-isolation.test.tsx +43 -0
  36. package/tests/unit/MarkdownConformance/indentation.test.tsx +29 -0
  37. package/tests/unit/MarkdownConformance/jsx-attributes.test.tsx +56 -0
  38. package/tests/unit/MarkdownConformance/jsx-fuzz.test.tsx +165 -0
  39. package/tests/unit/MarkdownConformance/links.test.tsx +71 -0
  40. package/tests/unit/MarkdownConformance/render-helpers.tsx +88 -0
  41. package/tests/unit/MarkdownEditor/decorations.test.ts +2 -2
  42. package/tests/unit/MarkdownEditor/indented-fence.test.ts +103 -0
  43. package/tests/unit/MarkdownEditor/jsx-attr-scan.test.ts +1 -1
  44. package/tests/unit/MarkdownEditor/jsx-block-widget.test.ts +92 -0
  45. package/tests/unit/MarkdownEditor/jsx-tag-grammar.test.ts +2 -2
  46. package/tests/unit/MarkdownEditor/list-indent.test.ts +2 -2
  47. package/tests/unit/MarkdownEditor/slash-commands.test.ts +2 -2
  48. package/tests/unit/MarkdownEditor/triggers.test.ts +2 -2
  49. package/tests/unit/MarkdownViewer/callout.test.tsx +60 -0
  50. package/tests/unit/MarkdownViewer/code-highlight.test.tsx +61 -0
  51. package/tests/unit/MarkdownViewer/features.test.tsx +55 -1
  52. package/tests/unit/MarkdownViewer/indented-jsx-children.test.tsx +98 -0
  53. package/tests/unit/MarkdownViewer/jsx-block-nesting.test.tsx +234 -0
  54. package/tests/unit/MarkdownViewer/jsx.test.tsx +2 -2
  55. package/tests/unit/MarkdownViewer/preserve-newlines.test.tsx +143 -0
  56. package/tests/unit/MarkdownViewer/read-time.test.ts +21 -0
  57. package/tests/unit/MarkdownViewer/renderers.test.tsx +16 -0
  58. package/tests/unit/MarkdownViewer/syntax-highlighting.test.tsx +73 -0
  59. package/tests/unit/MarkdownViewer/tabs-fenced-code.test.tsx +196 -0
  60. package/tests/unit/MarkdownViewer/tabs.test.tsx +115 -0
  61. package/tools/duplicate-vector-mappings.json +79 -0
  62. package/tools/glass-export-overrides.json +8 -0
  63. package/tools/integration-gradient-exports.json +14 -1
  64. package/tools/picto-sync.ts +2 -1
  65. package/dist/assets/b5cb190d5047855e.webp +0 -0
@@ -0,0 +1,234 @@
1
+ import { MantineProvider, v8CssVariablesResolver } from "@mantine/core";
2
+ import type { MarkdownComponents } from "@src/lib/markdown";
3
+ import { MarkdownViewer } from "@src/primitives/MarkdownViewer";
4
+ import { MANTINE_THEME } from "@src/theme/mantine";
5
+ import { Children, isValidElement, type ReactElement, type ReactNode } from "react";
6
+ import { renderToString } from "react-dom/server";
7
+ import { beforeAll, describe, expect, it } from "vitest";
8
+
9
+ beforeAll(() => {
10
+ if (typeof window !== "undefined" && typeof window.matchMedia !== "function") {
11
+ Object.defineProperty(window, "matchMedia", {
12
+ writable: true,
13
+ value: (query: string) => ({
14
+ matches: false,
15
+ media: query,
16
+ onchange: null,
17
+ addListener: () => {},
18
+ removeListener: () => {},
19
+ addEventListener: () => {},
20
+ removeEventListener: () => {},
21
+ dispatchEvent: () => false,
22
+ }),
23
+ });
24
+ }
25
+ });
26
+
27
+ function shell(node: ReactElement): string {
28
+ return renderToString(
29
+ <MantineProvider
30
+ theme={MANTINE_THEME}
31
+ cssVariablesResolver={v8CssVariablesResolver}
32
+ forceColorScheme="dark"
33
+ >
34
+ {node}
35
+ </MantineProvider>,
36
+ );
37
+ }
38
+
39
+ /**
40
+ * A `<Tabs>`-shaped block component that renders every item's label AND content
41
+ * (unlike Mantine's `Tabs`, which omits inactive panels in SSR). Lets a single
42
+ * `renderToString` assert the full parsed structure of all tab bodies.
43
+ */
44
+ function EagerGroup({ children }: { children?: ReactNode }): ReactElement {
45
+ const items = Children.toArray(children).filter(isValidElement);
46
+ return (
47
+ <div data-eager-group>
48
+ <div
49
+ data-labels={items.map((el) => (el.props as { label?: string }).label).join("|")}
50
+ />
51
+ {items.map((el, i) => (
52
+ <section
53
+ data-panel={i}
54
+ key={(el.props as { label?: string }).label ?? i}
55
+ >
56
+ {(el.props as { children?: ReactNode }).children}
57
+ </section>
58
+ ))}
59
+ </div>
60
+ );
61
+ }
62
+
63
+ function EagerItem({ children }: { children?: ReactNode }): ReactElement {
64
+ return <>{children}</>;
65
+ }
66
+
67
+ const EAGER_COMPONENTS: MarkdownComponents = {
68
+ Tabs: { component: EagerGroup, block: true },
69
+ TabItem: { component: EagerItem, block: true },
70
+ };
71
+
72
+ function stripTags(html: string): string {
73
+ return html.replace(/<[^>]+>/g, "");
74
+ }
75
+
76
+ describe("MarkdownViewer / JSX block nesting", () => {
77
+ it("pairs a Tabs block into exactly its authored TabItems (no synthetic tabs)", () => {
78
+ const content = `<Tabs syncKey="demo">
79
+ <TabItem label="Create an instance">
80
+ \tSurrealDB Cloud allows you to connect.
81
+
82
+ \t**1. Create Instance.**
83
+
84
+ \tWhen creating an instance, you enter the name.
85
+ </TabItem>
86
+ <TabItem label="Connect to your instance">
87
+ \tYou can connect via [Surrealist](/docs/x).
88
+
89
+ \tSince SurrealDB Cloud is closely integrated.
90
+ </TabItem>
91
+ </Tabs>`;
92
+ const html = shell(<MarkdownViewer content={content} />);
93
+
94
+ // Exactly two tab buttons with the authored labels — no paragraphs
95
+ // leaking out as extra "Tab N" children.
96
+ const tabButtons = html.match(/mantine-Tabs-tab\b/g) ?? [];
97
+ expect(tabButtons).toHaveLength(2);
98
+ const labels = [...html.matchAll(/mantine-Tabs-tabLabel">([^<]*)</g)].map((m) => m[1]);
99
+ expect(labels).toEqual(["Create an instance", "Connect to your instance"]);
100
+
101
+ // The active tab renders its full multi-paragraph markdown body.
102
+ const text = stripTags(html);
103
+ expect(text).toContain("SurrealDB Cloud allows you to connect.");
104
+ expect(text).toContain("1. Create Instance.");
105
+ expect(text).toContain("When creating an instance");
106
+ });
107
+
108
+ it("renders full multi-paragraph markdown for every TabItem (via eager group)", () => {
109
+ const content = `<Tabs syncKey="demo">
110
+ <TabItem label="Create an instance">
111
+ \tSurrealDB Cloud allows you to connect.
112
+
113
+ \t**1. Create Instance.**
114
+
115
+ \tWhen creating an instance, you enter the name.
116
+ </TabItem>
117
+ <TabItem label="Connect to your instance">
118
+ \tYou can connect via [Surrealist](/docs/x).
119
+
120
+ \t[Connect via Surrealist](/docs/y)
121
+
122
+ \tSince SurrealDB Cloud is closely integrated.
123
+ </TabItem>
124
+ </Tabs>`;
125
+ const html = shell(
126
+ <MarkdownViewer
127
+ content={content}
128
+ components={EAGER_COMPONENTS}
129
+ />,
130
+ );
131
+
132
+ expect(html).toContain('data-labels="Create an instance|Connect to your instance"');
133
+ expect(html).not.toContain("Tab 2");
134
+
135
+ // Both panels rendered, in order, with all their paragraphs.
136
+ const text = stripTags(html);
137
+ expect(text).toContain("SurrealDB Cloud allows you to connect.");
138
+ expect(text).toContain("1. Create Instance.");
139
+ expect(text).toContain("When creating an instance");
140
+ expect(text).toContain("You can connect via Surrealist");
141
+ expect(text).toContain("closely integrated");
142
+ // Links inside both tabs survive.
143
+ expect(html).toContain('href="/docs/x"');
144
+ expect(html).toContain('href="/docs/y"');
145
+ });
146
+
147
+ it("renders a single-paragraph Tabs block (regression)", () => {
148
+ const content = `<Tabs syncKey="s">
149
+ <TabItem label="First">
150
+ Alpha content.
151
+ </TabItem>
152
+ <TabItem label="Second">
153
+ Beta content.
154
+ </TabItem>
155
+ </Tabs>`;
156
+ const html = shell(
157
+ <MarkdownViewer
158
+ content={content}
159
+ components={EAGER_COMPONENTS}
160
+ />,
161
+ );
162
+ expect(html).toContain('data-labels="First|Second"');
163
+ expect(html).not.toContain("Tab 2");
164
+ const text = stripTags(html);
165
+ expect(text).toContain("Alpha content.");
166
+ expect(text).toContain("Beta content.");
167
+ });
168
+
169
+ it("renders block content around a Tabs block", () => {
170
+ const content = `# Heading
171
+
172
+ Intro paragraph.
173
+
174
+ <Tabs syncKey="s">
175
+ <TabItem label="One">
176
+ First tab.
177
+ </TabItem>
178
+ <TabItem label="Two">
179
+ Second tab.
180
+ </TabItem>
181
+ </Tabs>
182
+
183
+ Outro paragraph.`;
184
+ const html = shell(
185
+ <MarkdownViewer
186
+ content={content}
187
+ components={EAGER_COMPONENTS}
188
+ />,
189
+ );
190
+ const text = stripTags(html);
191
+ expect(text).toContain("Heading");
192
+ expect(text).toContain("Intro paragraph.");
193
+ expect(text).toContain("Outro paragraph.");
194
+ expect(html).toContain('data-labels="One|Two"');
195
+ expect(text).toContain("First tab.");
196
+ expect(text).toContain("Second tab.");
197
+ });
198
+
199
+ it("renders nested block components (Tabs inside a TabItem)", () => {
200
+ const content = `<Tabs syncKey="outer">
201
+ <TabItem label="Outer A">
202
+ Outer A body.
203
+
204
+ <Tabs syncKey="inner">
205
+ <TabItem label="Inner X">
206
+ Inner X body.
207
+ </TabItem>
208
+ <TabItem label="Inner Y">
209
+ Inner Y body.
210
+ </TabItem>
211
+ </Tabs>
212
+ </TabItem>
213
+ <TabItem label="Outer B">
214
+ Outer B body.
215
+ </TabItem>
216
+ </Tabs>`;
217
+ const html = shell(
218
+ <MarkdownViewer
219
+ content={content}
220
+ components={EAGER_COMPONENTS}
221
+ />,
222
+ );
223
+ // Outer group has exactly the two authored tabs.
224
+ expect(html).toContain('data-labels="Outer A|Outer B"');
225
+ // Inner group nested inside Outer A has its own two tabs.
226
+ expect(html).toContain('data-labels="Inner X|Inner Y"');
227
+ expect(html).not.toContain("Tab 2");
228
+ const text = stripTags(html);
229
+ expect(text).toContain("Outer A body.");
230
+ expect(text).toContain("Outer B body.");
231
+ expect(text).toContain("Inner X body.");
232
+ expect(text).toContain("Inner Y body.");
233
+ });
234
+ });
@@ -99,12 +99,12 @@ Hidden **bold** text
99
99
  expect(headings[1]?.text).toBe("Sub");
100
100
  });
101
101
 
102
- it("onImage rewrites image src", () => {
102
+ it("onResolveMedia rewrites image src", () => {
103
103
  const html = shell(
104
104
  <MarkdownViewer
105
105
  content='<img src="@ui/pictoAI" />'
106
106
  jsxMode="render"
107
- onImage={(node) => ({ ...node, src: "https://example.com/resolved.png" })}
107
+ onResolveMedia={(node) => ({ ...node, src: "https://example.com/resolved.png" })}
108
108
  />,
109
109
  );
110
110
  expect(html).toContain("https://example.com/resolved.png");
@@ -0,0 +1,143 @@
1
+ import { MantineProvider, v8CssVariablesResolver } from "@mantine/core";
2
+ import type { MarkdownComponents } from "@src/lib/markdown";
3
+ import { MarkdownViewer } from "@src/primitives/MarkdownViewer";
4
+ import { MANTINE_THEME } from "@src/theme/mantine";
5
+ import type { ReactElement, ReactNode } from "react";
6
+ import { renderToString } from "react-dom/server";
7
+ import { beforeAll, describe, expect, it } from "vitest";
8
+
9
+ beforeAll(() => {
10
+ if (typeof window !== "undefined" && typeof window.matchMedia !== "function") {
11
+ Object.defineProperty(window, "matchMedia", {
12
+ writable: true,
13
+ value: (query: string) => ({
14
+ matches: false,
15
+ media: query,
16
+ onchange: null,
17
+ addListener: () => {},
18
+ removeListener: () => {},
19
+ addEventListener: () => {},
20
+ removeEventListener: () => {},
21
+ dispatchEvent: () => false,
22
+ }),
23
+ });
24
+ }
25
+ });
26
+
27
+ function shell(node: ReactElement): string {
28
+ return renderToString(
29
+ <MantineProvider
30
+ theme={MANTINE_THEME}
31
+ cssVariablesResolver={v8CssVariablesResolver}
32
+ forceColorScheme="dark"
33
+ >
34
+ {node}
35
+ </MantineProvider>,
36
+ );
37
+ }
38
+
39
+ function TwoColumn({ children }: { children?: ReactNode }): ReactElement {
40
+ return <div data-testid="two-column">{children}</div>;
41
+ }
42
+
43
+ function ColBox({ children }: { children?: ReactNode }): ReactElement {
44
+ return <div data-testid="col-box">{children}</div>;
45
+ }
46
+
47
+ const LAYOUT = `<TwoColumn>
48
+ <ColBox></ColBox>
49
+ <ColBox></ColBox>
50
+ <ColBox></ColBox>
51
+ </TwoColumn>`;
52
+
53
+ describe("MarkdownViewer / custom component preserveNewlines", () => {
54
+ it("does not inject <br /> between block children (they render as blocks)", () => {
55
+ const components: MarkdownComponents = {
56
+ TwoColumn: { component: TwoColumn, block: true },
57
+ ColBox: { component: ColBox, block: true },
58
+ };
59
+ const html = shell(
60
+ <MarkdownViewer
61
+ content={LAYOUT}
62
+ components={components}
63
+ />,
64
+ );
65
+
66
+ // Three boxes still render, in order.
67
+ expect(html.match(/data-testid="col-box"/g)?.length).toBe(3);
68
+ // Block-level children are carved out and rendered as their own blocks:
69
+ // the newlines that merely separate them must NOT become <br /> elements.
70
+ // A stray <br /> here would also be counted as an extra child by
71
+ // introspecting parents (e.g. <Tabs> reading each <TabItem>), so the two
72
+ // behaviours are unified — `preserveNewlines` governs prose line breaks
73
+ // (see the prose test below), not gaps between block children.
74
+ const inner = html.slice(
75
+ html.indexOf('data-testid="two-column"'),
76
+ html.lastIndexOf("</div>"),
77
+ );
78
+ expect(inner).not.toMatch(/<br\s*\/?>/);
79
+ });
80
+
81
+ it("omits <br /> between block children when preserveNewlines is false", () => {
82
+ const components: MarkdownComponents = {
83
+ TwoColumn: { component: TwoColumn, block: true, preserveNewlines: false },
84
+ ColBox: { component: ColBox, block: true },
85
+ };
86
+ const html = shell(
87
+ <MarkdownViewer
88
+ content={LAYOUT}
89
+ components={components}
90
+ />,
91
+ );
92
+
93
+ // All three boxes still render.
94
+ expect(html.match(/data-testid="col-box"/g)?.length).toBe(3);
95
+ // The TwoColumn wrapper is present.
96
+ expect(html).toContain('data-testid="two-column"');
97
+
98
+ // No stray line breaks injected between the child blocks.
99
+ const inner = html.slice(
100
+ html.indexOf('data-testid="two-column"'),
101
+ html.lastIndexOf("</div>"),
102
+ );
103
+ expect(inner).not.toMatch(/<br\s*\/?>/);
104
+ });
105
+
106
+ it("still preserves newlines for prose components by default", () => {
107
+ function Prose({ children }: { children?: ReactNode }): ReactElement {
108
+ return <section data-testid="prose">{children}</section>;
109
+ }
110
+ const components: MarkdownComponents = {
111
+ Prose: { component: Prose, block: true },
112
+ };
113
+ const html = shell(
114
+ <MarkdownViewer
115
+ content={"<Prose>\nLine one\nLine two\n</Prose>"}
116
+ components={components}
117
+ />,
118
+ );
119
+ expect(html).toContain("Line one");
120
+ expect(html).toContain("Line two");
121
+ // Multi-line prose keeps its line break.
122
+ expect(html).toMatch(/<br\s*\/?>/);
123
+ });
124
+
125
+ it("disabling preserveNewlines collapses multi-line text to a single run", () => {
126
+ function Prose({ children }: { children?: ReactNode }): ReactElement {
127
+ return <section data-testid="prose">{children}</section>;
128
+ }
129
+ const components: MarkdownComponents = {
130
+ Prose: { component: Prose, block: true, preserveNewlines: false },
131
+ };
132
+ const html = shell(
133
+ <MarkdownViewer
134
+ content={"<Prose>\nLine one\nLine two\n</Prose>"}
135
+ components={components}
136
+ />,
137
+ );
138
+ expect(html).toContain("Line one");
139
+ expect(html).toContain("Line two");
140
+ const inner = html.slice(html.indexOf('data-testid="prose"'));
141
+ expect(inner).not.toMatch(/<br\s*\/?>/);
142
+ });
143
+ });
@@ -0,0 +1,21 @@
1
+ import {
2
+ estimateMarkdownReadTimeMinutes,
3
+ formatMarkdownReadTime,
4
+ markdownToPlainText,
5
+ } from "@src/lib/markdown/read-time";
6
+ import { describe, expect, it } from "vitest";
7
+
8
+ describe("markdown read time", () => {
9
+ it("strips markdown syntax for word counting", () => {
10
+ expect(markdownToPlainText("# Title\n\n[link](https://example.com)")).toBe("Title link");
11
+ });
12
+
13
+ it("estimates at least one minute", () => {
14
+ expect(estimateMarkdownReadTimeMinutes("")).toBe(1);
15
+ });
16
+
17
+ it("formats a read-time label", () => {
18
+ const words = Array.from({ length: 400 }, () => "word").join(" ");
19
+ expect(formatMarkdownReadTime(words)).toBe("2 min read");
20
+ });
21
+ });
@@ -63,6 +63,22 @@ describe("MarkdownViewer / MarkdownViewerRenderers", () => {
63
63
  expect(html).not.toContain("https://example.com");
64
64
  });
65
65
 
66
+ it("uses table renderer override without the scroll wrapper", () => {
67
+ const html = shell(
68
+ <MarkdownViewer
69
+ content={`| A | B |
70
+ | - | - |
71
+ | 1 | 2 |`}
72
+ renderers={{
73
+ table: () => <span data-custom-table>TABLE_MARKER</span>,
74
+ }}
75
+ />,
76
+ );
77
+ expect(html).toContain("TABLE_MARKER");
78
+ expect(html).not.toContain("<table");
79
+ expect(html).not.toContain("mantine-TableScrollContainer-scrollContainer");
80
+ });
81
+
66
82
  it("uses image renderer override", () => {
67
83
  const html = shell(
68
84
  <MarkdownViewer
@@ -0,0 +1,73 @@
1
+ import { MantineProvider, v8CssVariablesResolver } from "@mantine/core";
2
+ import { MarkdownViewer } from "@src/primitives/MarkdownViewer";
3
+ import { MANTINE_THEME } from "@src/theme/mantine";
4
+ import type { ReactElement } from "react";
5
+ import { renderToString } from "react-dom/server";
6
+ import { beforeAll, describe, expect, it } from "vitest";
7
+ import { md } from "../../_setup/markdown-classes";
8
+
9
+ beforeAll(() => {
10
+ if (typeof window !== "undefined" && typeof window.matchMedia !== "function") {
11
+ Object.defineProperty(window, "matchMedia", {
12
+ writable: true,
13
+ value: (query: string) => ({
14
+ matches: false,
15
+ media: query,
16
+ onchange: null,
17
+ addListener: () => {},
18
+ removeListener: () => {},
19
+ addEventListener: () => {},
20
+ removeEventListener: () => {},
21
+ dispatchEvent: () => false,
22
+ }),
23
+ });
24
+ }
25
+ });
26
+
27
+ function shell(node: ReactElement): string {
28
+ return renderToString(
29
+ <MantineProvider
30
+ theme={MANTINE_THEME}
31
+ cssVariablesResolver={v8CssVariablesResolver}
32
+ forceColorScheme="dark"
33
+ >
34
+ {node}
35
+ </MantineProvider>,
36
+ );
37
+ }
38
+
39
+ describe("MarkdownViewer / syntax highlighting", () => {
40
+ it("highlights bash and sh fenced blocks", () => {
41
+ const html = shell(
42
+ <MarkdownViewer
43
+ content={`\`\`\`bash
44
+ cd project
45
+ \`\`\`
46
+
47
+ \`\`\`sh
48
+ export FOO=bar
49
+ \`\`\`
50
+ `}
51
+ />,
52
+ );
53
+
54
+ expect(html).toContain(md.codeBlock);
55
+ expect(html).toMatch(/style="color:#ffd000"/);
56
+ });
57
+
58
+ it("highlights json fenced blocks", () => {
59
+ const html = shell(
60
+ <MarkdownViewer
61
+ content={`\`\`\`json
62
+ {
63
+ "name": "example"
64
+ }
65
+ \`\`\`
66
+ `}
67
+ />,
68
+ );
69
+
70
+ expect(html).toContain(md.codeBlock);
71
+ expect(html).toMatch(/style="color:#00ff6e"/);
72
+ });
73
+ });