@heliosgraphics/ui 2.0.1-alpha.7 → 2.0.1-alpha.8
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.
|
@@ -7,16 +7,20 @@
|
|
|
7
7
|
width: 100%;
|
|
8
8
|
|
|
9
9
|
fill: currentcolor;
|
|
10
|
+
stroke: currentcolor;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
.iconPrimary svg {
|
|
13
14
|
fill: var(--ui-text-primary);
|
|
15
|
+
stroke: var(--ui-text-primary);
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
.iconSecondary svg {
|
|
17
19
|
fill: var(--ui-text-secondary);
|
|
20
|
+
stroke: var(--ui-text-secondary);
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
.iconTertiary svg {
|
|
21
24
|
fill: var(--ui-text-tertiary);
|
|
25
|
+
stroke: var(--ui-text-tertiary);
|
|
22
26
|
}
|
|
@@ -16,6 +16,11 @@ export const meta: HeliosAttributeMeta<MarkdownProps> = {
|
|
|
16
16
|
isOptional: true,
|
|
17
17
|
description: "Children to render inside the markdown wrapper",
|
|
18
18
|
},
|
|
19
|
+
allowLinks: {
|
|
20
|
+
type: "boolean",
|
|
21
|
+
isOptional: true,
|
|
22
|
+
description: "Preserves markdown links and auto-linked URLs in sanitized output",
|
|
23
|
+
},
|
|
19
24
|
isNonSelectable: {
|
|
20
25
|
type: "boolean",
|
|
21
26
|
isOptional: true,
|
|
@@ -4,7 +4,7 @@ import styles from "./Markdown.module.css"
|
|
|
4
4
|
import type { FC } from "react"
|
|
5
5
|
import type { MarkdownProps } from "./Markdown.types"
|
|
6
6
|
|
|
7
|
-
export const Markdown: FC<MarkdownProps> = ({ text, children, isNonSelectable }) => {
|
|
7
|
+
export const Markdown: FC<MarkdownProps> = ({ text, children, isNonSelectable, allowLinks }) => {
|
|
8
8
|
if (!text && !children) return null
|
|
9
9
|
|
|
10
10
|
const markdownClasses: string = getClasses(styles.markdown, {
|
|
@@ -12,7 +12,7 @@ export const Markdown: FC<MarkdownProps> = ({ text, children, isNonSelectable })
|
|
|
12
12
|
})
|
|
13
13
|
|
|
14
14
|
if (text) {
|
|
15
|
-
const innerHTML = { __html: renderMarkdown(text) }
|
|
15
|
+
const innerHTML = { __html: renderMarkdown(text, { allowLinks }) }
|
|
16
16
|
|
|
17
17
|
return <div className={markdownClasses} dangerouslySetInnerHTML={innerHTML} data-ui-component="Markdown"></div>
|
|
18
18
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heliosgraphics/ui",
|
|
3
|
-
"version": "2.0.1-alpha.
|
|
3
|
+
"version": "2.0.1-alpha.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"*.css",
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
"ts:watch": "tsc --noEmit --incremental --watch"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@heliosgraphics/css": "^1.0.0-alpha.
|
|
44
|
-
"@heliosgraphics/icons": "^1.0.0-alpha.
|
|
45
|
-
"@heliosgraphics/utils": "^6.0.0-alpha.
|
|
46
|
-
"marked": "^17.0.
|
|
43
|
+
"@heliosgraphics/css": "^1.0.0-alpha.9",
|
|
44
|
+
"@heliosgraphics/icons": "^1.0.0-alpha.14",
|
|
45
|
+
"@heliosgraphics/utils": "^6.0.0-alpha.15",
|
|
46
|
+
"marked": "^17.0.5",
|
|
47
47
|
"marked-linkify-it": "^3.1.14",
|
|
48
48
|
"marked-xhtml": "^1.0.14",
|
|
49
49
|
"react-plock": "^3.6.1"
|
|
@@ -51,13 +51,13 @@
|
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@testing-library/react": "^16.3.2",
|
|
53
53
|
"@types/node": "^25",
|
|
54
|
-
"esbuild": "^0.
|
|
54
|
+
"esbuild": "^0.28.0",
|
|
55
55
|
"esbuild-css-modules-plugin": "^3.1.5",
|
|
56
56
|
"glob": "^13.0.6",
|
|
57
|
-
"jsdom": "^
|
|
58
|
-
"next": "^16.
|
|
57
|
+
"jsdom": "^29.0.1",
|
|
58
|
+
"next": "^16.2.2",
|
|
59
59
|
"prettier": "^3.8.1",
|
|
60
|
-
"typescript": "^
|
|
60
|
+
"typescript": "^6.0.2"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"@types/react": "^19",
|
package/utils/markdown.spec.ts
CHANGED
|
@@ -5,6 +5,10 @@ describe("markdown", () => {
|
|
|
5
5
|
describe("renderMarkdown", () => {
|
|
6
6
|
const SAMPLE = `Hello\n\nHey`
|
|
7
7
|
const SAMPLE_OUTPUT = `<p>Hello</p>\n<p>Hey</p>\n`
|
|
8
|
+
const LINK_SAMPLE = `[x](https://example.com)`
|
|
9
|
+
const LINK_SAMPLE_OUTPUT = `<p><a href="https://example.com">x</a></p>\n`
|
|
10
|
+
const BARE_URL_SAMPLE = `Visit https://example.com/path for details.`
|
|
11
|
+
const BARE_URL_SAMPLE_OUTPUT = `<p>Visit <a href="https://example.com/path">https://example.com/path</a> for details.</p>\n`
|
|
8
12
|
const MALICIOUS_SAMPLE = `Hello <script>alert(1)</script> **world**`
|
|
9
13
|
const MALICIOUS_SAMPLE_OUTPUT = `<p>Hello <strong>world</strong></p>`
|
|
10
14
|
const MALICIOUS_LINK_SAMPLE = `[x](data:text/html,<script>alert(1)</script>)`
|
|
@@ -14,6 +18,19 @@ describe("markdown", () => {
|
|
|
14
18
|
|
|
15
19
|
it("Returns", () => expect(renderMarkdown(SAMPLE)).toEqual(SAMPLE_OUTPUT))
|
|
16
20
|
|
|
21
|
+
it("keeps explicit markdown links when links are allowed", () => {
|
|
22
|
+
expect(renderMarkdown(LINK_SAMPLE, { allowLinks: true })).toEqual(LINK_SAMPLE_OUTPUT)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("autolinks bare urls when links are allowed", () => {
|
|
26
|
+
expect(renderMarkdown(BARE_URL_SAMPLE, { allowLinks: true })).toEqual(BARE_URL_SAMPLE_OUTPUT)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("keeps links disabled by default", () => {
|
|
30
|
+
expect(renderMarkdown(LINK_SAMPLE)).toEqual(`<p>x</p>\n`)
|
|
31
|
+
expect(renderMarkdown(BARE_URL_SAMPLE)).toEqual(`<p>Visit https://example.com/path for details.</p>\n`)
|
|
32
|
+
})
|
|
33
|
+
|
|
17
34
|
it("strips unsafe html from markdown output", () => {
|
|
18
35
|
const result = renderMarkdown(MALICIOUS_SAMPLE)
|
|
19
36
|
|
|
@@ -23,13 +40,19 @@ describe("markdown", () => {
|
|
|
23
40
|
})
|
|
24
41
|
|
|
25
42
|
it("strips unsafe link protocols without leaving broken markup", () => {
|
|
26
|
-
const result = renderMarkdown(MALICIOUS_LINK_SAMPLE)
|
|
43
|
+
const result = renderMarkdown(MALICIOUS_LINK_SAMPLE, { allowLinks: true })
|
|
27
44
|
|
|
28
45
|
expect(result).toEqual(MALICIOUS_LINK_SAMPLE_OUTPUT)
|
|
29
46
|
expect(result).not.toContain("data:")
|
|
30
47
|
expect(result).not.toContain("<script")
|
|
31
48
|
})
|
|
32
49
|
|
|
50
|
+
it("does not autolink urls inside inline code", () => {
|
|
51
|
+
const result = renderMarkdown("`https://example.com`", { allowLinks: true })
|
|
52
|
+
|
|
53
|
+
expect(result).toEqual(`<p><code>https://example.com</code></p>\n`)
|
|
54
|
+
})
|
|
55
|
+
|
|
33
56
|
it("removes inline event handler attributes from raw html", () => {
|
|
34
57
|
const result = renderMarkdown(MALICIOUS_EVENT_HANDLER_SAMPLE)
|
|
35
58
|
|
package/utils/markdown.ts
CHANGED
|
@@ -6,6 +6,10 @@ import { sanitizeText } from "@heliosgraphics/utils"
|
|
|
6
6
|
const marked = new Marked({ breaks: true, gfm: true })
|
|
7
7
|
const SAFE_MARKDOWN_LINK_PATTERN: RegExp = /^(?:https?:|mailto:|tel:|\/|#)/i
|
|
8
8
|
|
|
9
|
+
export interface RenderMarkdownOptions {
|
|
10
|
+
allowLinks?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
marked.use(markedXhtml())
|
|
10
14
|
marked.use(markedLinkifyIt())
|
|
11
15
|
|
|
@@ -15,9 +19,9 @@ const stripUnsafeMarkdownLinks = (html: string): string => {
|
|
|
15
19
|
})
|
|
16
20
|
}
|
|
17
21
|
|
|
18
|
-
export const renderMarkdown = (text: string): string => {
|
|
22
|
+
export const renderMarkdown = (text: string, options: RenderMarkdownOptions = {}): string => {
|
|
19
23
|
const renderedMarkdown: string = marked.parse(text) as string
|
|
20
|
-
const sanitizedMarkdown: string = sanitizeText(stripUnsafeMarkdownLinks(renderedMarkdown))
|
|
24
|
+
const sanitizedMarkdown: string = sanitizeText(stripUnsafeMarkdownLinks(renderedMarkdown), options)
|
|
21
25
|
|
|
22
26
|
return renderedMarkdown.endsWith("\n") ? `${sanitizedMarkdown}\n` : sanitizedMarkdown
|
|
23
27
|
}
|