Skip to content

Commit b1bdf10

Browse files
feat: Add table support and syntax highlights for markdowns (coder#3910)
1 parent dca24bd commit b1bdf10

File tree

9 files changed

+397
-34
lines changed

9 files changed

+397
-34
lines changed

site/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
"react-i18next": "11.18.4",
5656
"react-markdown": "8.0.3",
5757
"react-router-dom": "^6.3.0",
58+
"react-syntax-highlighter": "15.5.0",
59+
"remark-gfm": "3.0.1",
5860
"sourcemapped-stacktrace": "1.1.11",
5961
"swr": "1.3.0",
6062
"tzdata": "1.0.30",

site/src/AppRouter.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import { NotFoundPage } from "./pages/404Page/404Page"
1616
import { CliAuthenticationPage } from "./pages/CliAuthPage/CliAuthPage"
1717
import { HealthzPage } from "./pages/HealthzPage/HealthzPage"
1818
import { LoginPage } from "./pages/LoginPage/LoginPage"
19-
import { TemplatePage } from "./pages/TemplatePage/TemplatePage"
2019
import TemplatesPage from "./pages/TemplatesPage/TemplatesPage"
2120
import { AccountPage } from "./pages/UserSettingsPage/AccountPage/AccountPage"
2221
import { SecurityPage } from "./pages/UserSettingsPage/SecurityPage/SecurityPage"
@@ -34,6 +33,7 @@ const TerminalPage = lazy(() => import("./pages/TerminalPage/TerminalPage"))
3433
const WorkspacesPage = lazy(() => import("./pages/WorkspacesPage/WorkspacesPage"))
3534
const CreateWorkspacePage = lazy(() => import("./pages/CreateWorkspacePage/CreateWorkspacePage"))
3635
const AuditPage = lazy(() => import("./pages/AuditPage/AuditPage"))
36+
const TemplatePage = lazy(() => import("./pages/TemplatePage/TemplatePage"))
3737

3838
export const AppRouter: FC = () => {
3939
const xServices = useContext(XServiceContext)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { ComponentMeta, Story } from "@storybook/react"
2+
import { Markdown, MarkdownProps } from "./Markdown"
3+
4+
export default {
5+
title: "components/Markdown",
6+
component: Markdown,
7+
} as ComponentMeta<typeof Markdown>
8+
9+
const Template: Story<MarkdownProps> = ({ children }) => <Markdown>{children}</Markdown>
10+
11+
export const WithCode = Template.bind({})
12+
WithCode.args = {
13+
children: `
14+
## Required permissions / policy
15+
16+
The following sample policy allows Coder to create EC2 instances and modify instances provisioned by Coder:
17+
18+
\`\`\`json
19+
{
20+
"Version": "2012-10-17",
21+
"Statement": [
22+
{
23+
"Sid": "VisualEditor0",
24+
"Effect": "Allow",
25+
"Action": [
26+
"ec2:GetDefaultCreditSpecification",
27+
"ec2:DescribeIamInstanceProfileAssociations",
28+
"ec2:DescribeTags",
29+
"ec2:CreateTags",
30+
"ec2:RunInstances",
31+
"ec2:DescribeInstanceCreditSpecifications",
32+
"ec2:DescribeImages",
33+
"ec2:ModifyDefaultCreditSpecification",
34+
"ec2:DescribeVolumes"
35+
],
36+
"Resource": "*"
37+
},
38+
{
39+
"Sid": "CoderResources",
40+
"Effect": "Allow",
41+
"Action": [
42+
"ec2:DescribeInstances",
43+
"ec2:DescribeInstanceAttribute",
44+
"ec2:UnmonitorInstances",
45+
"ec2:TerminateInstances",
46+
"ec2:StartInstances",
47+
"ec2:StopInstances",
48+
"ec2:DeleteTags",
49+
"ec2:MonitorInstances",
50+
"ec2:CreateTags",
51+
"ec2:RunInstances",
52+
"ec2:ModifyInstanceAttribute",
53+
"ec2:ModifyInstanceCreditSpecification"
54+
],
55+
"Resource": "arn:aws:ec2:*:*:instance/*",
56+
"Condition": {
57+
"StringEquals": {
58+
"aws:ResourceTag/Coder_Provisioned": "true"
59+
}
60+
}
61+
}
62+
]
63+
}
64+
\`\`\``,
65+
}
66+
67+
export const WithTable = Template.bind({})
68+
WithTable.args = {
69+
children: `
70+
| heading | b | c | d |
71+
| - | :- | -: | :-: |
72+
| cell 1 | cell 2 | 3 | 4 | `,
73+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import Link from "@material-ui/core/Link"
2+
import { makeStyles, Theme, useTheme } from "@material-ui/core/styles"
3+
import Table from "@material-ui/core/Table"
4+
import TableBody from "@material-ui/core/TableBody"
5+
import TableCell from "@material-ui/core/TableCell"
6+
import TableContainer from "@material-ui/core/TableContainer"
7+
import TableHead from "@material-ui/core/TableHead"
8+
import TableRow from "@material-ui/core/TableRow"
9+
import { FC } from "react"
10+
import ReactMarkdown from "react-markdown"
11+
import SyntaxHighlighter from "react-syntax-highlighter"
12+
import { dracula as dark } from "react-syntax-highlighter/dist/cjs/styles/hljs"
13+
import gfm from "remark-gfm"
14+
15+
export interface MarkdownProps {
16+
children: string
17+
}
18+
19+
export const Markdown: FC<{ children: string }> = ({ children }) => {
20+
const theme: Theme = useTheme()
21+
const styles = useStyles()
22+
23+
return (
24+
<ReactMarkdown
25+
remarkPlugins={[gfm]}
26+
components={{
27+
a: ({ href, target, children }) => (
28+
<Link href={href} target={target}>
29+
{children}
30+
</Link>
31+
),
32+
33+
// Adding node so the ...props don't have it
34+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
35+
code: ({ node, inline, className, children, ...props }) => {
36+
const match = /language-(\w+)/.exec(className || "")
37+
return !inline && match ? (
38+
<SyntaxHighlighter
39+
// Custom style to match our main colors
40+
style={{
41+
...dark,
42+
hljs: {
43+
...dark.hljs,
44+
background: theme.palette.background.default,
45+
borderRadius: theme.shape.borderRadius,
46+
color: theme.palette.text.primary,
47+
},
48+
}}
49+
language={match[1]}
50+
PreTag="div"
51+
{...props}
52+
>
53+
{String(children).replace(/\n$/, "")}
54+
</SyntaxHighlighter>
55+
) : (
56+
<code className={styles.codeWithoutLanguage} {...props}>
57+
{children}
58+
</code>
59+
)
60+
},
61+
62+
table: ({ children }) => {
63+
return (
64+
<TableContainer>
65+
<Table>{children}</Table>
66+
</TableContainer>
67+
)
68+
},
69+
70+
tr: ({ children }) => {
71+
return <TableRow>{children}</TableRow>
72+
},
73+
74+
thead: ({ children }) => {
75+
return <TableHead>{children}</TableHead>
76+
},
77+
78+
tbody: ({ children }) => {
79+
return <TableBody>{children}</TableBody>
80+
},
81+
82+
td: ({ children }) => {
83+
return <TableCell>{children}</TableCell>
84+
},
85+
86+
th: ({ children }) => {
87+
return <TableCell>{children}</TableCell>
88+
},
89+
}}
90+
>
91+
{children}
92+
</ReactMarkdown>
93+
)
94+
}
95+
96+
const useStyles = makeStyles((theme) => ({
97+
codeWithoutLanguage: {
98+
display: "block",
99+
overflowX: "auto",
100+
padding: "0.5em",
101+
background: theme.palette.background.default,
102+
borderRadius: theme.shape.borderRadius,
103+
color: theme.palette.text.primary,
104+
},
105+
}))

site/src/pages/TemplatePage/TemplatePage.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
} from "../../testHelpers/renderHelpers"
1414
import { TemplatePage } from "./TemplatePage"
1515

16+
jest.mock("remark-gfm", () => jest.fn())
17+
1618
Object.defineProperty(window, "ResizeObserver", {
1719
value: ResizeObserver,
1820
})

site/src/pages/TemplatePage/TemplatePage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,5 @@ export const TemplatePage: FC<React.PropsWithChildren<unknown>> = () => {
9292
</>
9393
)
9494
}
95+
96+
export default TemplatePage

site/src/pages/TemplatePage/TemplatePageView.stories.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ You can add instructions here
3939
[Some link info](https://coder.com)
4040
\`\`\`
4141
# This is a really long sentence to test that the code block wraps into a new line properly.
42-
\`\`\``,
42+
\`\`\`
43+
`,
4344
},
4445
templateResources: [Mocks.MockWorkspaceResource, Mocks.MockWorkspaceResource2],
4546
templateVersions: [Mocks.MockTemplateVersion],

site/src/pages/TemplatePage/TemplatePageView.tsx

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import SettingsOutlined from "@material-ui/icons/SettingsOutlined"
77
import { DeleteButton } from "components/DropdownButton/ActionCtas"
88
import { DropdownButton } from "components/DropdownButton/DropdownButton"
99
import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
10+
import { Markdown } from "components/Markdown/Markdown"
1011
import frontMatter from "front-matter"
1112
import { FC } from "react"
12-
import ReactMarkdown from "react-markdown"
1313
import { Link as RouterLink } from "react-router-dom"
1414
import { firstLetter } from "util/firstLetter"
1515
import {
@@ -147,17 +147,7 @@ export const TemplatePageView: FC<React.PropsWithChildren<TemplatePageViewProps>
147147
contentsProps={{ className: styles.readmeContents }}
148148
>
149149
<div className={styles.markdownWrapper}>
150-
<ReactMarkdown
151-
components={{
152-
a: ({ href, target, children }) => (
153-
<Link href={href} target={target}>
154-
{children}
155-
</Link>
156-
),
157-
}}
158-
>
159-
{readme.body}
160-
</ReactMarkdown>
150+
<Markdown>{readme.body}</Markdown>
161151
</div>
162152
</WorkspaceSection>
163153
<WorkspaceSection
@@ -184,12 +174,6 @@ export const useStyles = makeStyles((theme) => {
184174
markdownWrapper: {
185175
background: theme.palette.background.paper,
186176
padding: theme.spacing(3, 4),
187-
188-
// Adds text wrapping to <pre> tag added by ReactMarkdown
189-
"& pre": {
190-
whiteSpace: "pre-wrap",
191-
wordWrap: "break-word",
192-
},
193177
},
194178
versionsTableContents: {
195179
margin: 0,

0 commit comments

Comments
 (0)