Skip to content

Commit 8433ff6

Browse files
committed
feat: add 1st iteration of fiddle embedder
1 parent 24ae4e7 commit 8433ff6

File tree

3 files changed

+228
-1
lines changed

3 files changed

+228
-1
lines changed

docusaurus.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const interpolateData = require('./src/transformers/interpolate-data');
22
const importCode = require('./src/transformers/import-code');
33
const partialContent = require('./src/transformers/partial-content');
4+
const fiddleEmbedder = require('./src/transformers/fiddle-embedder.js');
45

56
/** @type {import('@docusaurus/types').DocusaurusConfig} */
67
module.exports = {
@@ -109,7 +110,7 @@ module.exports = {
109110
// Please change this to your repo.
110111
editUrl:
111112
'https://github.com/crossplatform-dev/crossplatform.dev/edit/main/',
112-
remarkPlugins: [importCode, partialContent, interpolateData],
113+
remarkPlugins: [importCode, partialContent, interpolateData, fiddleEmbedder],
113114
},
114115
// blog: {
115116
// showReadingTime: true,

src/components/LaunchButton.jsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//@ts-check
2+
3+
/**
4+
* Original file licensed under MIT in
5+
* https://github.com/electron/electronjs.org-new/blob/85c00545413ca5101955c0cf51f64150ae06e6e4/src/components/LaunchButton.jsx
6+
*/
7+
8+
import React from 'react';
9+
10+
function LaunchButton({ url }) {
11+
return (
12+
<a
13+
target="_blank"
14+
className="button button--block button--lg button--primary"
15+
href={url}
16+
>
17+
Open in Fiddle
18+
</a>
19+
);
20+
}
21+
22+
export default LaunchButton;

src/transformers/fiddle-embedder.js

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
//@ts-check
2+
/**
3+
* Original file licensed under MIT in
4+
* https://github.com/electron/electronjs.org-new/blob/85c00545413ca5101955c0cf51f64150ae06e6e4/src/transformers/fiddle-embedder.js
5+
*/
6+
7+
const visitParents = require('unist-util-visit-parents');
8+
const path = require('path');
9+
const fs = require('fs-extra');
10+
const latestVersion = require('latest-version');
11+
12+
let _version = '';
13+
async function getVersion() {
14+
if (_version === '') {
15+
_version = await latestVersion('electron');
16+
}
17+
18+
return _version;
19+
}
20+
21+
module.exports = function attacher() {
22+
return transformer;
23+
};
24+
25+
/**
26+
* Tests for AST nodes that match the following:
27+
*
28+
* 1) MDX import
29+
*
30+
* 2) Fiddle code block
31+
* \```fiddle path/to/fiddle
32+
*
33+
* \```
34+
* @param {import("unist").Node} node
35+
* @returns boolean
36+
*/
37+
function matchNode(node) {
38+
return (
39+
node.type === 'import' ||
40+
(node.type === 'code' && node.lang === 'fiddle' && !!node.meta)
41+
);
42+
}
43+
44+
const importNode = {
45+
type: 'import',
46+
value:
47+
"import Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n import LaunchButton from '@site/src/components/LaunchButton';",
48+
};
49+
50+
/**
51+
*
52+
* @param {import("unist").Parent} tree
53+
*/
54+
async function transformer(tree) {
55+
let hasExistingImport = false;
56+
const version = await getVersion();
57+
visitParents(tree, matchNode, visitor);
58+
59+
if (!hasExistingImport) {
60+
tree.children.unshift(importNode);
61+
}
62+
63+
/**
64+
*
65+
* @param {*} node
66+
* @param {import("unist").Node[]} ancestors
67+
* @returns { import("unist-util-visit-parents").ActionTuple }
68+
*/
69+
function visitor(node, ancestors) {
70+
if (node.type === 'import') {
71+
if (node.value.includes('@theme/Tabs')) {
72+
hasExistingImport = true;
73+
}
74+
return;
75+
}
76+
77+
const parent = ancestors[0];
78+
// Supported formats are fiddle='<folder>|<option>|option'
79+
// Options must be of the format key=value with no additional quotes.
80+
const [folder, ...others] = node.meta.split('|');
81+
const options = {};
82+
83+
// If there are optional parameters, parse them out to pass to the getFiddleAST method.
84+
if (others.length > 0) {
85+
for (const option of others) {
86+
// Use indexOf to support bizzare combinations like `|key=Myvalue=2` (which will properly
87+
// parse to {'key': 'Myvalue=2'})
88+
const firstEqual = option.indexOf('=');
89+
const key = option.substr(0, firstEqual);
90+
const value = option.substr(firstEqual + 1);
91+
options[key] = value;
92+
}
93+
}
94+
95+
// Find where the Fiddle code block is relative to the parent,
96+
// and splice the children array to insert the embedded Fiddle
97+
if (Array.isArray(parent.children)) {
98+
const index = parent.children.indexOf(node);
99+
const newChildren = getFiddleAST(folder, version, options);
100+
parent.children.splice(index, 1, ...newChildren);
101+
// Return an ActionTuple [Action, Index], where
102+
// Action SKIP means we want to skip visiting these new children
103+
// Index is the index of the AST we want to continue parsing at.
104+
return [visitParents.SKIP, index + newChildren.length];
105+
}
106+
}
107+
}
108+
/**
109+
* From a directory in `/docs/fiddles/`, generate the AST needed
110+
* for the tabbed code MDX structure.
111+
* @param {string} dir
112+
* @param {string} version
113+
*/
114+
function getFiddleAST(dir, version, { focus = 'main.js' }) {
115+
const files = {};
116+
const children = [];
117+
118+
// TODO: non-alphabetic sort
119+
const fileNames = fs.readdirSync(dir);
120+
121+
if (fileNames.length === 0) {
122+
return children;
123+
}
124+
125+
if (!fileNames.includes(focus)) {
126+
throw new Error(
127+
`Provided focus (${focus}) is not an available file in this fiddle (${dir}). Available files are [${fileNames.join(
128+
', '
129+
)}]`
130+
);
131+
}
132+
133+
for (const file of fileNames) {
134+
files[file] = fs.readFileSync(path.join(dir, file)).toString();
135+
}
136+
137+
const tabValues = fileNames.reduce((acc, val) => {
138+
return (acc += `{ label: '${val}', value: '${val}', },`);
139+
}, '');
140+
141+
let index = 0;
142+
143+
// Generate MDXAST structure by iterating through all files in
144+
// the folder and creating <TabItem> components and code blocks
145+
// for each, and bookending those with the <Tabs> component.
146+
147+
// The finished product should look something like:
148+
// <Tabs defaultValue="id1"
149+
// values={[
150+
// {label: 'id1', value: 'id1'},
151+
// {label: 'id2', value: 'id2'}
152+
// ]}>
153+
// <TabItem value="id1">
154+
// ```js
155+
// const cow = 'say';
156+
// ```
157+
// <TabItem>
158+
// <TabItem value="id2">
159+
// ```js
160+
// const hello = 'world';
161+
// ```
162+
// <TabItem>
163+
// <Tabs>
164+
children.push({
165+
type: 'jsx',
166+
value:
167+
`<Tabs defaultValue="${focus}" ` +
168+
`values={[${tabValues}]}>
169+
<TabItem value="${fileNames[index]}">`,
170+
});
171+
172+
while (index < fileNames.length) {
173+
children.push({
174+
type: 'code',
175+
lang: path.extname(fileNames[index]).slice(1),
176+
value: files[fileNames[index]],
177+
});
178+
179+
index++;
180+
181+
if (index < fileNames.length) {
182+
children.push({
183+
type: 'jsx',
184+
value: `</TabItem>\n<TabItem value="${fileNames[index]}">`,
185+
});
186+
} else {
187+
children.push(
188+
{
189+
type: 'jsx',
190+
value: `</TabItem>\n</Tabs>`,
191+
},
192+
{
193+
type: 'jsx',
194+
value: `<LaunchButton url="https://fiddle.electronjs.org/launch?target=electron/v${version}/${dir.replace(
195+
'latest/',
196+
''
197+
)}"/>`,
198+
}
199+
);
200+
}
201+
}
202+
203+
return children;
204+
}

0 commit comments

Comments
 (0)