@heliosgraphics/ui 2.0.1-alpha.4 → 2.0.1-alpha.6
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/LICENSE.md +1 -1
- package/components/Heading/Heading.tsx +7 -8
- package/components/Text/Text.tsx +1 -4
- package/components/Text/Text.utils.ts +54 -4
- package/package.json +1 -1
- package/utils/markdown.spec.ts +30 -0
- package/utils/markdown.ts +14 -1
package/LICENSE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getTypographyUtility } from "../Text/Text.utils"
|
|
1
|
+
import { getTypographyUtility, stripTypographyProps } from "../Text/Text.utils"
|
|
2
2
|
import { getClasses } from "@heliosgraphics/utils"
|
|
3
3
|
import { H0 } from "./components/H0"
|
|
4
4
|
import { H1 } from "./components/H1"
|
|
@@ -9,7 +9,7 @@ import { H5 } from "./components/H5"
|
|
|
9
9
|
import { H6 } from "./components/H6"
|
|
10
10
|
import styles from "./Heading.module.css"
|
|
11
11
|
import type { HeadingProps } from "./Heading.types"
|
|
12
|
-
import type { FC, CSSProperties } from "react"
|
|
12
|
+
import type { FC, CSSProperties, HTMLAttributes } from "react"
|
|
13
13
|
|
|
14
14
|
export const Heading: FC<HeadingProps> = (props) => {
|
|
15
15
|
const { level, lineClamp, lineHeight, style, className, ...rest } = props
|
|
@@ -19,10 +19,7 @@ export const Heading: FC<HeadingProps> = (props) => {
|
|
|
19
19
|
[styles.headingSecondary]: props.emphasis === "secondary",
|
|
20
20
|
[styles.headingTertiary]: props.emphasis === "tertiary",
|
|
21
21
|
})
|
|
22
|
-
const utility: string = getTypographyUtility(
|
|
23
|
-
...props,
|
|
24
|
-
className: headingClasses,
|
|
25
|
-
})
|
|
22
|
+
const utility: string = getTypographyUtility(props, headingClasses)
|
|
26
23
|
const lineClampStyle: CSSProperties | undefined = lineClamp
|
|
27
24
|
? {
|
|
28
25
|
display: "-webkit-box",
|
|
@@ -38,9 +35,11 @@ export const Heading: FC<HeadingProps> = (props) => {
|
|
|
38
35
|
style || lineClampStyle || lineHeightStyle
|
|
39
36
|
? { ...(style || {}), ...(lineClampStyle || {}), ...(lineHeightStyle || {}) }
|
|
40
37
|
: undefined
|
|
38
|
+
const safeHeadingProps: Omit<HeadingProps, "level" | "lineClamp" | "lineHeight" | "className" | "style"> =
|
|
39
|
+
stripTypographyProps(rest)
|
|
41
40
|
|
|
42
|
-
const allProps:
|
|
43
|
-
...
|
|
41
|
+
const allProps: HTMLAttributes<HTMLHeadingElement> = {
|
|
42
|
+
...safeHeadingProps,
|
|
44
43
|
children: props.children,
|
|
45
44
|
style: mergedStyle,
|
|
46
45
|
className: utility,
|
package/components/Text/Text.tsx
CHANGED
|
@@ -17,10 +17,7 @@ export const Text: FC<TextProps> = (props) => {
|
|
|
17
17
|
[styles.textInherit]: props.emphasis === "inherit",
|
|
18
18
|
})
|
|
19
19
|
|
|
20
|
-
const utility: string = getTypographyUtility(
|
|
21
|
-
...props,
|
|
22
|
-
className: textClasses,
|
|
23
|
-
})
|
|
20
|
+
const utility: string = getTypographyUtility(props, textClasses)
|
|
24
21
|
const lineClampStyle: object | undefined = props.lineClamp
|
|
25
22
|
? {
|
|
26
23
|
display: "-webkit-box",
|
|
@@ -1,7 +1,54 @@
|
|
|
1
|
-
import type { TextProps } from "./Text.types"
|
|
1
|
+
import type { TextBaseProps, TextProps } from "./Text.types"
|
|
2
2
|
import type { HeadingProps } from "../Heading/Heading.types"
|
|
3
3
|
|
|
4
|
-
export
|
|
4
|
+
export interface TypographyUtilityInput {
|
|
5
|
+
emphasis?: TextBaseProps["emphasis"] | undefined
|
|
6
|
+
fontFamily?: TextBaseProps["fontFamily"] | undefined
|
|
7
|
+
fontStyle?: TextBaseProps["fontStyle"] | undefined
|
|
8
|
+
fontWeight?: TextBaseProps["fontWeight"] | undefined
|
|
9
|
+
isBalanced?: TextBaseProps["isBalanced"] | undefined
|
|
10
|
+
isEllipsis?: TextBaseProps["isEllipsis"] | undefined
|
|
11
|
+
isNonSelectable?: TextBaseProps["isNonSelectable"] | undefined
|
|
12
|
+
textAlign?: TextBaseProps["textAlign"] | undefined
|
|
13
|
+
textDecoration?: TextBaseProps["textDecoration"] | undefined
|
|
14
|
+
whiteSpace?: TextBaseProps["whiteSpace"] | undefined
|
|
15
|
+
wordWrap?: TextBaseProps["wordWrap"] | undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const stripTypographyProps = <T extends TypographyUtilityInput>(
|
|
19
|
+
props: T,
|
|
20
|
+
): Omit<T, keyof TypographyUtilityInput> => {
|
|
21
|
+
const {
|
|
22
|
+
emphasis,
|
|
23
|
+
fontFamily,
|
|
24
|
+
fontStyle,
|
|
25
|
+
fontWeight,
|
|
26
|
+
isBalanced,
|
|
27
|
+
isEllipsis,
|
|
28
|
+
isNonSelectable,
|
|
29
|
+
textAlign,
|
|
30
|
+
textDecoration,
|
|
31
|
+
whiteSpace,
|
|
32
|
+
wordWrap,
|
|
33
|
+
...rest
|
|
34
|
+
} = props
|
|
35
|
+
|
|
36
|
+
void emphasis
|
|
37
|
+
void fontFamily
|
|
38
|
+
void fontStyle
|
|
39
|
+
void fontWeight
|
|
40
|
+
void isBalanced
|
|
41
|
+
void isEllipsis
|
|
42
|
+
void isNonSelectable
|
|
43
|
+
void textAlign
|
|
44
|
+
void textDecoration
|
|
45
|
+
void whiteSpace
|
|
46
|
+
void wordWrap
|
|
47
|
+
|
|
48
|
+
return rest
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const _getFontWeight = (fw: TextBaseProps["fontWeight"]): string => {
|
|
5
52
|
switch (fw) {
|
|
6
53
|
case "thin":
|
|
7
54
|
return "fw-thin"
|
|
@@ -27,7 +74,10 @@ export const _getFontWeight = (fw: TextProps["fontWeight"]): string => {
|
|
|
27
74
|
}
|
|
28
75
|
}
|
|
29
76
|
|
|
30
|
-
export const getTypographyUtility = (
|
|
77
|
+
export const getTypographyUtility = (
|
|
78
|
+
props: TextProps | HeadingProps | TypographyUtilityInput,
|
|
79
|
+
className?: string,
|
|
80
|
+
): string => {
|
|
31
81
|
const typoClasses: Array<string> = []
|
|
32
82
|
|
|
33
83
|
const fontFamily = props.fontFamily ? props.fontFamily : "sans"
|
|
@@ -36,7 +86,7 @@ export const getTypographyUtility = (props: TextProps | HeadingProps): string =>
|
|
|
36
86
|
typoClasses.push(fontFamily)
|
|
37
87
|
typoClasses.push(fontWeight)
|
|
38
88
|
|
|
39
|
-
if (
|
|
89
|
+
if (className) typoClasses.push(className)
|
|
40
90
|
if (props.fontStyle) typoClasses.push(props.fontStyle)
|
|
41
91
|
if (props.isBalanced) typoClasses.push("text-balanced")
|
|
42
92
|
if (props.isEllipsis) typoClasses.push("ellipsis")
|
package/package.json
CHANGED
package/utils/markdown.spec.ts
CHANGED
|
@@ -5,7 +5,37 @@ 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 MALICIOUS_SAMPLE = `Hello <script>alert(1)</script> **world**`
|
|
9
|
+
const MALICIOUS_SAMPLE_OUTPUT = `<p>Hello <strong>world</strong></p>`
|
|
10
|
+
const MALICIOUS_LINK_SAMPLE = `[x](data:text/html,<script>alert(1)</script>)`
|
|
11
|
+
const MALICIOUS_LINK_SAMPLE_OUTPUT = `<p>x</p>\n`
|
|
12
|
+
const MALICIOUS_EVENT_HANDLER_SAMPLE = `<div onclick="alert(1)">hi</div>`
|
|
13
|
+
const MALICIOUS_EVENT_HANDLER_SAMPLE_OUTPUT = `<div>hi</div>`
|
|
8
14
|
|
|
9
15
|
it("Returns", () => expect(renderMarkdown(SAMPLE)).toEqual(SAMPLE_OUTPUT))
|
|
16
|
+
|
|
17
|
+
it("strips unsafe html from markdown output", () => {
|
|
18
|
+
const result = renderMarkdown(MALICIOUS_SAMPLE)
|
|
19
|
+
|
|
20
|
+
expect(result).toContain(MALICIOUS_SAMPLE_OUTPUT)
|
|
21
|
+
expect(result).not.toContain("<script")
|
|
22
|
+
expect(result).not.toContain("alert(1)")
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("strips unsafe link protocols without leaving broken markup", () => {
|
|
26
|
+
const result = renderMarkdown(MALICIOUS_LINK_SAMPLE)
|
|
27
|
+
|
|
28
|
+
expect(result).toEqual(MALICIOUS_LINK_SAMPLE_OUTPUT)
|
|
29
|
+
expect(result).not.toContain("data:")
|
|
30
|
+
expect(result).not.toContain("<script")
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("removes inline event handler attributes from raw html", () => {
|
|
34
|
+
const result = renderMarkdown(MALICIOUS_EVENT_HANDLER_SAMPLE)
|
|
35
|
+
|
|
36
|
+
expect(result).toEqual(MALICIOUS_EVENT_HANDLER_SAMPLE_OUTPUT)
|
|
37
|
+
expect(result).not.toContain("onclick")
|
|
38
|
+
expect(result).not.toContain("alert(1)")
|
|
39
|
+
})
|
|
10
40
|
})
|
|
11
41
|
})
|
package/utils/markdown.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { Marked } from "marked"
|
|
2
2
|
import { markedXhtml } from "marked-xhtml"
|
|
3
3
|
import markedLinkifyIt from "marked-linkify-it"
|
|
4
|
+
import { sanitizeText } from "@heliosgraphics/utils"
|
|
4
5
|
|
|
5
6
|
const marked = new Marked({ breaks: true, gfm: true })
|
|
7
|
+
const SAFE_MARKDOWN_LINK_PATTERN: RegExp = /^(?:https?:|mailto:|tel:|\/|#)/i
|
|
6
8
|
|
|
7
9
|
marked.use(markedXhtml())
|
|
8
10
|
marked.use(markedLinkifyIt())
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
const stripUnsafeMarkdownLinks = (html: string): string => {
|
|
13
|
+
return html.replace(/<a\b[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, (match: string, href: string, text: string) => {
|
|
14
|
+
return SAFE_MARKDOWN_LINK_PATTERN.test(href) ? match : text
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const renderMarkdown = (text: string): string => {
|
|
19
|
+
const renderedMarkdown: string = marked.parse(text) as string
|
|
20
|
+
const sanitizedMarkdown: string = sanitizeText(stripUnsafeMarkdownLinks(renderedMarkdown))
|
|
21
|
+
|
|
22
|
+
return renderedMarkdown.endsWith("\n") ? `${sanitizedMarkdown}\n` : sanitizedMarkdown
|
|
23
|
+
}
|