Skip to content

Commit d886d88

Browse files
authored
API examples syntax highlighting (#11715)
* hightlight api examples codeblocks server side using refactor * use the usual code block on curl snippet * simplify styles * remove unused highlighter * adjust api parameters spacing * fix curl snippet styles * fix Http snippet border in dark mode * remove prismjs direct dependency * add copy paste * refactor code to jsx transformation * remove unused styles
1 parent 947a467 commit d886d88

File tree

8 files changed

+119
-90
lines changed

8 files changed

+119
-90
lines changed

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"esbuild": "^0.19.8",
6666
"framer-motion": "^10.12.16",
6767
"gray-matter": "^4.0.3",
68+
"hast-util-to-jsx-runtime": "^2.3.2",
6869
"hastscript": "^8.0.0",
6970
"image-size": "^1.1.1",
7071
"js-cookie": "^3.0.5",
@@ -80,7 +81,6 @@
8081
"parse-numeric-range": "^1.3.0",
8182
"platformicons": "^6.0.3",
8283
"prism-sentry": "^1.0.2",
83-
"prismjs": "^1.27.0",
8484
"query-string": "^6.13.1",
8585
"react": "^18",
8686
"react-dom": "^18",
@@ -138,4 +138,4 @@
138138
"node": "20.11.0",
139139
"yarn": "1.22.21"
140140
}
141-
}
141+
}

src/components/apiExamples/apiExamples.module.scss

+2-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
.api-block-example {
2+
background-color: var(--code-background);
3+
color: var(--white);
24
border: none;
35
border-bottom-left-radius: 3px;
46
border-bottom-right-radius: 3px;
@@ -10,17 +12,6 @@
1012
padding: 0.75rem;
1113
}
1214

13-
.api-block-example.request {
14-
color: var(--white);
15-
background-color: #2d2d2d;
16-
border-radius: 4px;
17-
}
18-
19-
.api-block-example.response {
20-
background: #2d2d2d;
21-
color: var(--white);
22-
}
23-
2415
.api-params dd {
2516
padding: 0;
2617

src/components/apiExamples/apiExamples.tsx

+77-71
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,24 @@
11
'use client';
22

3-
import {Fragment, useEffect, useRef, useState} from 'react';
3+
import {Fragment, useEffect, useState} from 'react';
4+
import {jsx, jsxs} from 'react/jsx-runtime';
5+
import {Clipboard} from 'react-feather';
6+
import {toJsxRuntime} from 'hast-util-to-jsx-runtime';
7+
import {Nodes} from 'hastscript/lib/create-h';
8+
import bash from 'refractor/lang/bash.js';
9+
import json from 'refractor/lang/json.js';
10+
import {refractor} from 'refractor/lib/core.js';
411

512
import {type API} from 'sentry-docs/build/resolveOpenAPI';
613

14+
import codeBlockStyles from '../codeBlock/code-blocks.module.scss';
715
import styles from './apiExamples.module.scss';
816

9-
type ExampleProps = {
10-
api: API;
11-
selectedResponse: number;
12-
selectedTabView: number;
13-
};
14-
15-
const requestStyles = `${styles['api-block-example']} ${styles.request}`;
16-
const responseStyles = `${styles['api-block-example']} ${styles.response}`;
17-
18-
// overwriting global code block font size
19-
const jsonCodeBlockStyles = `!text-[0.8rem] language-json`;
20-
21-
function Example({api, selectedTabView, selectedResponse}: ExampleProps) {
22-
const ref = useRef(null);
23-
let exampleJson: any;
24-
if (api.responses[selectedResponse].content?.examples) {
25-
exampleJson = Object.values(
26-
api.responses[selectedResponse].content?.examples ?? {}
27-
).map(e => e.value)[0];
28-
} else if (api.responses[selectedResponse].content?.example) {
29-
exampleJson = api.responses[selectedResponse].content?.example;
30-
}
31-
32-
// load prism dynamically for these codeblocks,
33-
// otherwise the highlighting applies globally
34-
useEffect(() => {
35-
(async () => {
36-
const {highlightAllUnder} = await import('prismjs');
37-
await import('prismjs/components/prism-json');
38-
if (ref.current) {
39-
highlightAllUnder(ref.current);
40-
}
41-
})();
42-
}, [selectedResponse, selectedTabView]);
17+
import {CodeBlock} from '../codeBlock';
18+
import {CodeTabs} from '../codeTabs';
4319

44-
return (
45-
<pre className={responseStyles} ref={ref}>
46-
{selectedTabView === 0 &&
47-
(exampleJson ? (
48-
<code
49-
className={jsonCodeBlockStyles}
50-
dangerouslySetInnerHTML={{
51-
__html: JSON.stringify(exampleJson, null, 2),
52-
}}
53-
/>
54-
) : (
55-
strFormat(api.responses[selectedResponse].description)
56-
))}
57-
{selectedTabView === 1 && (
58-
<code
59-
className={jsonCodeBlockStyles}
60-
dangerouslySetInnerHTML={{
61-
__html: JSON.stringify(
62-
api.responses[selectedResponse].content?.schema,
63-
null,
64-
2
65-
),
66-
}}
67-
/>
68-
)}
69-
</pre>
70-
);
71-
}
20+
refractor.register(bash);
21+
refractor.register(json);
7222

7323
const strFormat = (str: string) => {
7424
const s = str.trim();
@@ -82,6 +32,10 @@ type Props = {
8232
api: API;
8333
};
8434

35+
const codeToJsx = (code: string, lang = 'json') => {
36+
return toJsxRuntime(refractor.highlight(code, lang) as Nodes, {Fragment, jsx, jsxs});
37+
};
38+
8539
export function ApiExamples({api}: Props) {
8640
const apiExample = [
8741
`curl https://sentry.io${api.apiPath}`,
@@ -112,11 +66,43 @@ export function ApiExamples({api}: Props) {
11266
? ['RESPONSE', 'SCHEMA']
11367
: ['RESPONSE'];
11468

69+
const [showCopied, setShowCopied] = useState(false);
70+
71+
// Show the copy button after js has loaded
72+
// otherwise the copy button will not work
73+
const [showCopyButton, setShowCopyButton] = useState(false);
74+
useEffect(() => {
75+
setShowCopyButton(true);
76+
}, []);
77+
async function copyCode(code: string) {
78+
await navigator.clipboard.writeText(code);
79+
setShowCopied(true);
80+
setTimeout(() => setShowCopied(false), 1200);
81+
}
82+
83+
let exampleJson: any;
84+
if (api.responses[selectedResponse].content?.examples) {
85+
exampleJson = Object.values(
86+
api.responses[selectedResponse].content?.examples ?? {}
87+
).map(e => e.value)[0];
88+
} else if (api.responses[selectedResponse].content?.example) {
89+
exampleJson = api.responses[selectedResponse].content?.example;
90+
}
91+
92+
const codeToCopy =
93+
selectedTabView === 0
94+
? exampleJson
95+
? JSON.stringify(exampleJson, null, 2)
96+
: strFormat(api.responses[selectedResponse].description)
97+
: JSON.stringify(api.responses[selectedResponse].content?.schema, null, 2);
98+
11599
return (
116100
<Fragment>
117-
<div className="api-block">
118-
<pre className={requestStyles}>{apiExample.join(' \\\n')}</pre>
119-
</div>
101+
<CodeTabs>
102+
<CodeBlock language="bash">
103+
<pre>{codeToJsx(apiExample.join(' \\\n'), 'bash')}</pre>
104+
</CodeBlock>
105+
</CodeTabs>
120106
<div className="api-block">
121107
<div className="api-block-header response">
122108
<div className="tabs-group">
@@ -149,12 +135,32 @@ export function ApiExamples({api}: Props) {
149135
)
150136
)}
151137
</div>
138+
139+
<button className={styles.copy} onClick={() => copyCode(codeToCopy)}>
140+
{showCopyButton && <Clipboard size={16} />}
141+
</button>
152142
</div>
153-
<Example
154-
api={api}
155-
selectedTabView={selectedTabView}
156-
selectedResponse={selectedResponse}
157-
/>
143+
<pre className={`${styles['api-block-example']} relative`}>
144+
<div className={codeBlockStyles.copied} style={{opacity: showCopied ? 1 : 0}}>
145+
Copied
146+
</div>
147+
{selectedTabView === 0 &&
148+
(exampleJson ? (
149+
<code className="!text-[0.8rem]">
150+
{codeToJsx(JSON.stringify(exampleJson, null, 2), 'json')}
151+
</code>
152+
) : (
153+
strFormat(api.responses[selectedResponse].description)
154+
))}
155+
{selectedTabView === 1 && (
156+
<code className="!text-[0.8rem]">
157+
{codeToJsx(
158+
JSON.stringify(api.responses[selectedResponse].content?.schema, null, 2),
159+
'json'
160+
)}
161+
</code>
162+
)}
163+
</pre>
158164
</div>
159165
</Fragment>
160166
);

src/components/apiPage/styles.scss

+7-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
box-shadow: rgba(0, 0, 0, 0.07) 0px 0px 0px 1px;
88
margin-bottom: var(--paragraph-margin-bottom);
99
}
10+
.dark .api-block {
11+
border: 1px solid var(--border-color);
12+
box-shadow: none;
13+
}
1014

1115
.api-block-header {
1216
border-top-left-radius: 3px;
@@ -31,7 +35,7 @@
3135
}
3236

3337
.api-block-header.response {
34-
background: #2d2d2d;
38+
background-color: var(--code-background);
3539
border-bottom: 1px solid #444;
3640
color: var(--white);
3741
display: flex;
@@ -57,6 +61,8 @@
5761

5862
.response-status-btn-group {
5963
border-radius: 3px;
64+
margin-left: auto;
65+
margin-right: 1rem;
6066
}
6167

6268
.response-status-btn {

src/components/codeBlock/code-blocks.module.scss

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
}
55

66
pre {
7-
background: #251f3d;
8-
border: 1px solid #40364a;
9-
border-radius: 0;
7+
background-color: var(--code-background);
8+
border-radius: 0 0 0.25rem 0.25rem;
109
margin-top: 0;
1110
margin-bottom: 0;
1211
}

src/components/codeTabs.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ const Container = styled('div')`
136136
`;
137137

138138
const TabBar = styled('div')`
139-
background: #251f3d;
139+
background: var(--code-background);
140140
border-bottom: 1px solid #40364a;
141141
height: 36px;
142142
display: flex;

src/components/docPage/type.scss

+6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
.prose {
1111
--heading-color: var(--darkPurple);
1212
--link-decoration: none;
13+
--code-background: #251f3d;
1314
h1,
1415
h2,
1516
h3,
@@ -178,9 +179,14 @@
178179
}
179180

180181
dt + dd {
182+
margin-top: 0.25rem;
181183
margin-bottom: var(--paragraph-margin-bottom);
182184
}
183185

186+
dd > p {
187+
margin-top: 0;
188+
}
189+
184190
[data-onboarding-option].hidden {
185191
display: none;
186192
}

yarn.lock

+22-1
Original file line numberDiff line numberDiff line change
@@ -7594,6 +7594,27 @@ hast-util-to-jsx-runtime@^2.0.0:
75947594
unist-util-position "^5.0.0"
75957595
vfile-message "^4.0.0"
75967596

7597+
hast-util-to-jsx-runtime@^2.3.2:
7598+
version "2.3.2"
7599+
resolved "https://registry.yarnpkg.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz#6d11b027473e69adeaa00ca4cfb5bb68e3d282fa"
7600+
integrity sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==
7601+
dependencies:
7602+
"@types/estree" "^1.0.0"
7603+
"@types/hast" "^3.0.0"
7604+
"@types/unist" "^3.0.0"
7605+
comma-separated-tokens "^2.0.0"
7606+
devlop "^1.0.0"
7607+
estree-util-is-identifier-name "^3.0.0"
7608+
hast-util-whitespace "^3.0.0"
7609+
mdast-util-mdx-expression "^2.0.0"
7610+
mdast-util-mdx-jsx "^3.0.0"
7611+
mdast-util-mdxjs-esm "^2.0.0"
7612+
property-information "^6.0.0"
7613+
space-separated-tokens "^2.0.0"
7614+
style-to-object "^1.0.0"
7615+
unist-util-position "^5.0.0"
7616+
vfile-message "^4.0.0"
7617+
75977618
hast-util-to-parse5@^8.0.0:
75987619
version "8.0.0"
75997620
resolved "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz"
@@ -11004,7 +11025,7 @@ prisma@^5.8.1:
1100411025
dependencies:
1100511026
"@prisma/engines" "5.12.1"
1100611027

11007-
prismjs@^1.23.0, prismjs@^1.27.0:
11028+
prismjs@^1.23.0:
1100811029
version "1.29.0"
1100911030
resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz"
1101011031
integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==

0 commit comments

Comments
 (0)