Skip to content

Commit 8b3d5ef

Browse files
authored
Generate snack embeds and links based on code snippet (#1294)
1 parent 9d13c4d commit 8b3d5ef

File tree

4 files changed

+297
-0
lines changed

4 files changed

+297
-0
lines changed

docusaurus.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import path from 'path';
22
import remarkNpm2Yarn from '@docusaurus/remark-plugin-npm2yarn';
3+
import rehypeCodeblockMeta from './src/plugins/rehype-codeblock-meta.mjs';
34

45
export default {
56
title: 'React Navigation',
@@ -146,6 +147,9 @@ export default {
146147
includeCurrentVersion: false,
147148
lastVersion: '6.x',
148149
remarkPlugins: [[remarkNpm2Yarn, { sync: true }]],
150+
rehypePlugins: [
151+
[rehypeCodeblockMeta, { match: { snack: true } }],
152+
],
149153
},
150154
blog: {
151155
remarkPlugins: [[remarkNpm2Yarn, { sync: true }]],

src/components/Pre.js

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { useColorMode } from '@docusaurus/theme-common';
2+
import MDXPre from '@theme-original/MDXComponents/Pre';
3+
import CodeBlock from '@theme-original/CodeBlock';
4+
import React from 'react';
5+
6+
const peers = {
7+
'react-native-safe-area-context': '*',
8+
'react-native-screens': '*',
9+
};
10+
11+
const versions = {
12+
7: {
13+
'@react-navigation/bottom-tabs': ['7.0.0-alpha.7', peers],
14+
'@react-navigation/core': '7.0.0-alpha.6',
15+
'@react-navigation/native': '7.0.0-alpha.6',
16+
'@react-navigation/drawer': [
17+
'7.0.0-alpha.7',
18+
{
19+
...peers,
20+
'react-native-reanimated': '*',
21+
},
22+
],
23+
'@react-navigation/elements': ['2.0.0-alpha.4', peers],
24+
'@react-navigation/material-top-tabs': [
25+
'7.0.0-alpha.6',
26+
{
27+
...peers,
28+
'react-native-pager-view': '*',
29+
},
30+
],
31+
'@react-navigation/native-stack': ['7.0.0-alpha.7', peers],
32+
'@react-navigation/routers': '7.0.0-alpha.4',
33+
'@react-navigation/stack': [
34+
'7.0.0-alpha.7',
35+
{
36+
...peers,
37+
'react-native-gesture-handler': '*',
38+
},
39+
],
40+
'react-native-drawer-layout': [
41+
'4.0.0-alpha.3',
42+
{
43+
'react-native-gesture-handler': '*',
44+
'react-native-reanimated': '*',
45+
},
46+
],
47+
'react-native-tab-view': [
48+
'4.0.0-alpha.2',
49+
{
50+
'react-native-pager-view': '*',
51+
},
52+
],
53+
},
54+
};
55+
56+
export default function Pre({
57+
children,
58+
'data-name': name,
59+
'data-snack': snack,
60+
'data-version': version,
61+
'data-dependencies': deps,
62+
...rest
63+
}) {
64+
const { colorMode } = useColorMode();
65+
66+
if (snack) {
67+
const code = React.Children.only(children).props.children;
68+
69+
if (typeof code !== 'string') {
70+
throw new Error(
71+
'Playground code must be a string, but received ' + typeof code
72+
);
73+
}
74+
75+
const dependencies = deps
76+
? Object.fromEntries(deps.split(',').map((entry) => entry.split('@')))
77+
: {};
78+
79+
Object.assign(
80+
dependencies,
81+
Object.entries(versions[version]).reduce((acc, [key, value]) => {
82+
if (code.includes(`from '${key}'`)) {
83+
if (Array.isArray(value)) {
84+
const [version, peers] = value;
85+
86+
Object.assign(acc, {
87+
[key]: version,
88+
...peers,
89+
});
90+
} else {
91+
acc[key] = value;
92+
}
93+
}
94+
95+
return acc;
96+
}, {})
97+
);
98+
99+
// FIXME: use staging for now since react-navigation fails to build on prod
100+
const url = new URL('https://staging-snack.expo.dev');
101+
102+
if (name) {
103+
url.searchParams.set('name', name);
104+
}
105+
106+
url.searchParams.set(
107+
'code',
108+
// Remove highlight and codeblock focus comments from code
109+
code
110+
.split('\n')
111+
.filter((line) =>
112+
[
113+
'// highlight-start',
114+
'// highlight-end',
115+
'// highlight-next-line',
116+
'// codeblock-focus-start',
117+
'// codeblock-focus-end',
118+
].every((comment) => line.trim() !== comment)
119+
)
120+
.join('\n')
121+
);
122+
123+
url.searchParams.set(
124+
'dependencies',
125+
Object.entries(dependencies)
126+
.map(([key, value]) => `${key}@${value}`)
127+
.join(',')
128+
);
129+
130+
url.searchParams.set('platform', 'web');
131+
url.searchParams.set('supportedPlatforms', 'ios,android,web');
132+
url.searchParams.set('preview', 'true');
133+
url.searchParams.set('hideQueryParams', 'true');
134+
135+
if (snack === 'embed') {
136+
url.searchParams.set('theme', colorMode === 'dark' ? 'dark' : 'light');
137+
url.pathname = 'embedded';
138+
139+
return (
140+
<iframe
141+
src={url.href}
142+
style={{
143+
width: '100%',
144+
height: 660,
145+
border: 'none',
146+
border: '1px solid var(--ifm-table-border-color)',
147+
borderRadius: 'var(--ifm-global-radius)',
148+
overflow: 'hidden',
149+
}}
150+
/>
151+
);
152+
}
153+
154+
// Only keep the lines between `// codeblock-focus-{start,end} comments
155+
if (code.includes('// codeblock-focus-start')) {
156+
const lines = code.split('\n');
157+
158+
let content = '';
159+
let focus = false;
160+
let indent;
161+
162+
for (const line of lines) {
163+
if (line.trim() === '// codeblock-focus-start') {
164+
focus = true;
165+
} else if (line.trim() === '// codeblock-focus-end') {
166+
focus = false;
167+
} else if (focus) {
168+
if (indent === undefined) {
169+
indent = line.match(/^\s*/)[0];
170+
}
171+
172+
if (line.startsWith(indent)) {
173+
content += line.slice(indent.length) + '\n';
174+
} else {
175+
content += line + '\n';
176+
}
177+
}
178+
}
179+
180+
children = React.Children.map(children, (child) =>
181+
React.cloneElement(child, { children: content })
182+
);
183+
}
184+
185+
return (
186+
<>
187+
<MDXPre {...rest}>{children}</MDXPre>
188+
<a
189+
className="snack-sample-link"
190+
data-snack="true"
191+
target="_blank"
192+
href={url.href}
193+
>
194+
Try this example on Snack{' '}
195+
<svg
196+
width="14px"
197+
height="14px"
198+
viewBox="0 0 16 16"
199+
style={{ verticalAlign: '-1px' }}
200+
>
201+
<g stroke="none" strokeWidth="1" fill="none">
202+
<polyline
203+
stroke="currentColor"
204+
points="8.5 0.5 15.5 0.5 15.5 7.5"
205+
/>
206+
<path d="M8,8 L15.0710678,0.928932188" stroke="currentColor" />
207+
<polyline
208+
stroke="currentColor"
209+
points="9.06944444 3.5 1.5 3.5 1.5 14.5 12.5 14.5 12.5 6.93055556"
210+
/>
211+
</g>
212+
</svg>
213+
</a>
214+
</>
215+
);
216+
}
217+
218+
return <MDXPre {...rest}>{children}</MDXPre>;
219+
}

src/plugins/rehype-codeblock-meta.mjs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { visit } from 'unist-util-visit';
2+
3+
/**
4+
* Plugin to process codeblock meta
5+
*
6+
* @param {{ match: { [key: string]: string }, element: JSX.ElementType }} options
7+
*/
8+
export default function rehypeCodeblockMeta(options) {
9+
if (!options?.match) {
10+
throw new Error('rehype-codeblock-meta: `match` option is required');
11+
}
12+
13+
return (tree) => {
14+
visit(tree, 'element', (node) => {
15+
if (
16+
node.tagName === 'pre' &&
17+
node.children?.length === 1 &&
18+
node.children[0].tagName === 'code'
19+
) {
20+
const codeblock = node.children[0];
21+
const meta = codeblock.data?.meta;
22+
23+
if (meta) {
24+
let segments = [];
25+
26+
// Walk through meta string and split it into segments based on space unless it's inside quotes
27+
for (let i = 0; i < meta.length; i++) {
28+
let segment = '';
29+
let quote = false;
30+
31+
for (; i < meta.length; i++) {
32+
if (meta[i] === '"') {
33+
quote = !quote;
34+
} else if (meta[i] === ' ' && !quote) {
35+
break;
36+
}
37+
38+
segment += meta[i];
39+
}
40+
41+
segments.push(segment);
42+
}
43+
44+
const attributes = segments.reduce((acc, attribute) => {
45+
const [key, value = 'true'] = attribute.split('=');
46+
47+
return Object.assign(acc, {
48+
[`data-${key}`]: value.replace(/^"(.+(?="$))"$/, '$1'),
49+
});
50+
}, {});
51+
52+
if (
53+
Object.entries(options.match).some(([key, value]) => {
54+
if (value === true) {
55+
return attributes[`data-${key}`];
56+
} else {
57+
return attributes[`data-${key}`] === value;
58+
}
59+
})
60+
) {
61+
Object.assign(node.properties, attributes);
62+
}
63+
}
64+
}
65+
});
66+
};
67+
}

src/theme/MDXComponents.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import MDXComponents from '@theme-original/MDXComponents';
2+
import Pre from '../components/Pre';
3+
4+
export default {
5+
...MDXComponents,
6+
pre: Pre,
7+
};

0 commit comments

Comments
 (0)