Skip to content

API examples syntax highlighting #11715

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"esbuild": "^0.19.8",
"framer-motion": "^10.12.16",
"gray-matter": "^4.0.3",
"hast-util-to-jsx-runtime": "^2.3.2",
"hastscript": "^8.0.0",
"image-size": "^1.1.1",
"js-cookie": "^3.0.5",
Expand All @@ -79,7 +80,6 @@
"parse-numeric-range": "^1.3.0",
"platformicons": "^6.0.3",
"prism-sentry": "^1.0.2",
"prismjs": "^1.27.0",
"query-string": "^6.13.1",
"react": "^18",
"react-dom": "^18",
Expand Down Expand Up @@ -137,4 +137,4 @@
"node": "20.11.0",
"yarn": "1.22.21"
}
}
}
13 changes: 2 additions & 11 deletions src/components/apiExamples/apiExamples.module.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.api-block-example {
background-color: var(--code-background);
color: var(--white);
border: none;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
Expand All @@ -10,17 +12,6 @@
padding: 0.75rem;
}

.api-block-example.request {
color: var(--white);
background-color: #2d2d2d;
border-radius: 4px;
}

.api-block-example.response {
background: #2d2d2d;
color: var(--white);
}

.api-params dd {
padding: 0;

Expand Down
148 changes: 77 additions & 71 deletions src/components/apiExamples/apiExamples.tsx
Original file line number Diff line number Diff line change
@@ -1,74 +1,24 @@
'use client';

import {Fragment, useEffect, useRef, useState} from 'react';
import {Fragment, useEffect, useState} from 'react';
import {jsx, jsxs} from 'react/jsx-runtime';
import {Clipboard} from 'react-feather';
import {toJsxRuntime} from 'hast-util-to-jsx-runtime';
import {Nodes} from 'hastscript/lib/create-h';
import bash from 'refractor/lang/bash.js';
import json from 'refractor/lang/json.js';
import {refractor} from 'refractor/lib/core.js';

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

import codeBlockStyles from '../codeBlock/code-blocks.module.scss';
import styles from './apiExamples.module.scss';

type ExampleProps = {
api: API;
selectedResponse: number;
selectedTabView: number;
};

const requestStyles = `${styles['api-block-example']} ${styles.request}`;
const responseStyles = `${styles['api-block-example']} ${styles.response}`;

// overwriting global code block font size
const jsonCodeBlockStyles = `!text-[0.8rem] language-json`;

function Example({api, selectedTabView, selectedResponse}: ExampleProps) {
const ref = useRef(null);
let exampleJson: any;
if (api.responses[selectedResponse].content?.examples) {
exampleJson = Object.values(
api.responses[selectedResponse].content?.examples ?? {}
).map(e => e.value)[0];
} else if (api.responses[selectedResponse].content?.example) {
exampleJson = api.responses[selectedResponse].content?.example;
}

// load prism dynamically for these codeblocks,
// otherwise the highlighting applies globally
useEffect(() => {
(async () => {
const {highlightAllUnder} = await import('prismjs');
await import('prismjs/components/prism-json');
if (ref.current) {
highlightAllUnder(ref.current);
}
})();
}, [selectedResponse, selectedTabView]);
import {CodeBlock} from '../codeBlock';
import {CodeTabs} from '../codeTabs';

return (
<pre className={responseStyles} ref={ref}>
{selectedTabView === 0 &&
(exampleJson ? (
<code
className={jsonCodeBlockStyles}
dangerouslySetInnerHTML={{
__html: JSON.stringify(exampleJson, null, 2),
}}
/>
) : (
strFormat(api.responses[selectedResponse].description)
))}
{selectedTabView === 1 && (
<code
className={jsonCodeBlockStyles}
dangerouslySetInnerHTML={{
__html: JSON.stringify(
api.responses[selectedResponse].content?.schema,
null,
2
),
}}
/>
)}
</pre>
);
}
refractor.register(bash);
refractor.register(json);

const strFormat = (str: string) => {
const s = str.trim();
Expand All @@ -82,6 +32,10 @@ type Props = {
api: API;
};

const codeToJsx = (code: string, lang = 'json') => {
return toJsxRuntime(refractor.highlight(code, lang) as Nodes, {Fragment, jsx, jsxs});
};

export function ApiExamples({api}: Props) {
const apiExample = [
`curl https://sentry.io${api.apiPath}`,
Expand Down Expand Up @@ -112,11 +66,43 @@ export function ApiExamples({api}: Props) {
? ['RESPONSE', 'SCHEMA']
: ['RESPONSE'];

const [showCopied, setShowCopied] = useState(false);

// Show the copy button after js has loaded
// otherwise the copy button will not work
const [showCopyButton, setShowCopyButton] = useState(false);
useEffect(() => {
setShowCopyButton(true);
}, []);
async function copyCode(code: string) {
await navigator.clipboard.writeText(code);
setShowCopied(true);
setTimeout(() => setShowCopied(false), 1200);
}

let exampleJson: any;
if (api.responses[selectedResponse].content?.examples) {
exampleJson = Object.values(
api.responses[selectedResponse].content?.examples ?? {}
).map(e => e.value)[0];
} else if (api.responses[selectedResponse].content?.example) {
exampleJson = api.responses[selectedResponse].content?.example;
}

const codeToCopy =
selectedTabView === 0
? exampleJson
? JSON.stringify(exampleJson, null, 2)
: strFormat(api.responses[selectedResponse].description)
: JSON.stringify(api.responses[selectedResponse].content?.schema, null, 2);

return (
<Fragment>
<div className="api-block">
<pre className={requestStyles}>{apiExample.join(' \\\n')}</pre>
</div>
<CodeTabs>
<CodeBlock language="bash">
<pre>{codeToJsx(apiExample.join(' \\\n'), 'bash')}</pre>
</CodeBlock>
</CodeTabs>
<div className="api-block">
<div className="api-block-header response">
<div className="tabs-group">
Expand Down Expand Up @@ -149,12 +135,32 @@ export function ApiExamples({api}: Props) {
)
)}
</div>

<button className={styles.copy} onClick={() => copyCode(codeToCopy)}>
{showCopyButton && <Clipboard size={16} />}
</button>
</div>
<Example
api={api}
selectedTabView={selectedTabView}
selectedResponse={selectedResponse}
/>
<pre className={`${styles['api-block-example']} relative`}>
<div className={codeBlockStyles.copied} style={{opacity: showCopied ? 1 : 0}}>
Copied
</div>
{selectedTabView === 0 &&
(exampleJson ? (
<code className="!text-[0.8rem]">
{codeToJsx(JSON.stringify(exampleJson, null, 2), 'json')}
</code>
) : (
strFormat(api.responses[selectedResponse].description)
))}
{selectedTabView === 1 && (
<code className="!text-[0.8rem]">
{codeToJsx(
JSON.stringify(api.responses[selectedResponse].content?.schema, null, 2),
'json'
)}
</code>
)}
</pre>
</div>
</Fragment>
);
Expand Down
8 changes: 7 additions & 1 deletion src/components/apiPage/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
box-shadow: rgba(0, 0, 0, 0.07) 0px 0px 0px 1px;
margin-bottom: var(--paragraph-margin-bottom);
}
.dark .api-block {
border: 1px solid var(--border-color);
box-shadow: none;
}

.api-block-header {
border-top-left-radius: 3px;
Expand All @@ -31,7 +35,7 @@
}

.api-block-header.response {
background: #2d2d2d;
background-color: var(--code-background);
border-bottom: 1px solid #444;
color: var(--white);
display: flex;
Expand All @@ -57,6 +61,8 @@

.response-status-btn-group {
border-radius: 3px;
margin-left: auto;
margin-right: 1rem;
}

.response-status-btn {
Expand Down
5 changes: 2 additions & 3 deletions src/components/codeBlock/code-blocks.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
}

pre {
background: #251f3d;
border: 1px solid #40364a;
border-radius: 0;
background-color: var(--code-background);
border-radius: 0 0 0.25rem 0.25rem;
margin-top: 0;
margin-bottom: 0;
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/codeTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ const Container = styled('div')`
`;

const TabBar = styled('div')`
background: #251f3d;
background: var(--code-background);
border-bottom: 1px solid #40364a;
height: 36px;
display: flex;
Expand Down
6 changes: 6 additions & 0 deletions src/components/docPage/type.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
.prose {
--heading-color: var(--darkPurple);
--link-decoration: none;
--code-background: #251f3d;
h1,
h2,
h3,
Expand Down Expand Up @@ -174,9 +175,14 @@
}

dt + dd {
margin-top: 0.25rem;
margin-bottom: var(--paragraph-margin-bottom);
}

dd > p {
margin-top: 0;
}

[data-onboarding-option].hidden {
display: none;
}
Expand Down
23 changes: 22 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6905,6 +6905,27 @@ hast-util-to-jsx-runtime@^2.0.0:
unist-util-position "^5.0.0"
vfile-message "^4.0.0"

hast-util-to-jsx-runtime@^2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz#6d11b027473e69adeaa00ca4cfb5bb68e3d282fa"
integrity sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==
dependencies:
"@types/estree" "^1.0.0"
"@types/hast" "^3.0.0"
"@types/unist" "^3.0.0"
comma-separated-tokens "^2.0.0"
devlop "^1.0.0"
estree-util-is-identifier-name "^3.0.0"
hast-util-whitespace "^3.0.0"
mdast-util-mdx-expression "^2.0.0"
mdast-util-mdx-jsx "^3.0.0"
mdast-util-mdxjs-esm "^2.0.0"
property-information "^6.0.0"
space-separated-tokens "^2.0.0"
style-to-object "^1.0.0"
unist-util-position "^5.0.0"
vfile-message "^4.0.0"

hast-util-to-parse5@^8.0.0:
version "8.0.0"
resolved "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz"
Expand Down Expand Up @@ -10180,7 +10201,7 @@ prisma@^5.8.1:
dependencies:
"@prisma/engines" "5.12.1"

prismjs@^1.23.0, prismjs@^1.27.0:
prismjs@^1.23.0:
version "1.29.0"
resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz"
integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==
Expand Down
Loading