Skip to content

Commit

Permalink
feat: tabbed CJS/ESM code blocks (#488)
Browse files Browse the repository at this point in the history
* feat: tabbed CJS/ESM code blocks

* chore: rename ESMCodeBlock to JsCodeBlock
  • Loading branch information
dsanders11 authored Jan 17, 2024
1 parent 40d22e3 commit c36cf61
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 0 deletions.
2 changes: 2 additions & 0 deletions docusaurusConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import npm2yarn from '@docusaurus/remark-plugin-npm2yarn';
import apiLabels from './src/transformers/api-labels';
import apiOptionsClass from './src/transformers/api-options-class';
import apiStructurePreviews from './src/transformers/api-structure-previews';
import jsCodeBlocks from './src/transformers/js-code-blocks';
import fiddleEmbedder from './src/transformers/fiddle-embedder';

const config: Config = {
Expand Down Expand Up @@ -205,6 +206,7 @@ const config: Config = {
apiLabels,
apiOptionsClass,
apiStructurePreviews,
jsCodeBlocks,
fiddleEmbedder,
[npm2yarn, { sync: true, converters: ['yarn'] }],
],
Expand Down
31 changes: 31 additions & 0 deletions src/components/JsCodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';

interface JsCodeBlockProps {
cjs: string;
mjs: string;
}

const JsCodeBlock = (props: JsCodeBlockProps) => {
const { cjs, mjs } = props;

const tabValues = [
{ label: 'CJS', value: 'cjs', content: cjs },
{ label: 'ESM', value: 'mjs', content: mjs },
];
return (
<Tabs values={tabValues}>
{tabValues.map(({ content, value }) => {
return (
<TabItem value={value} key={value}>
<CodeBlock className={`language-${value}`}>{content}</CodeBlock>
</TabItem>
);
})}
</Tabs>
);
};

export default JsCodeBlock;
102 changes: 102 additions & 0 deletions src/transformers/js-code-blocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Node, Parent } from 'unist';
import { Code } from 'mdast';

import visitParents, { ActionTuple } from 'unist-util-visit-parents';

import { Import } from '../util/interfaces';

const CJS_PREAMBLE = '// CommonJS\n';
const MJS_PREAMBLE = '// ESM\n';
const OPT_OUT_META = 'no-combine';

export default function attacher() {
return transformer;
}

function matchCjsCodeBlock(node: Node): node is Code {
return isCode(node) && node.lang === 'cjs';
}

function matchMjsCodeBlock(node: Node): node is Code {
return isCode(node) && node.lang === 'mjs';
}

const importNode: Import = {
type: 'import',
value: "import JsCodeBlock from '@site/src/components/JsCodeBlock';",
};

async function transformer(tree: Parent) {
let documentHasExistingImport = false;
visitParents(tree, 'import', checkForJsCodeBlockImport);
visitParents(tree, matchCjsCodeBlock, maybeGenerateJsCodeBlock);

if (!documentHasExistingImport) {
tree.children.unshift(importNode);
}

function checkForJsCodeBlockImport(node: Node) {
if (
isImport(node) &&
node.value.includes('@site/src/components/JsCodeBlock')
) {
documentHasExistingImport = true;
}
}

function maybeGenerateJsCodeBlock(
node: Code,
ancestors: Parent[]
): ActionTuple | void {
const parent = ancestors[0];
const idx = parent.children.indexOf(node);

const cjsCodeBlock = node;
const mjsCodeBlock = parent.children[idx + 1];

// Check if the immediate sibling is the mjs code block
if (!matchMjsCodeBlock(mjsCodeBlock)) {
return;
}

// Let blocks explicitly opt-out of being combined
if (
cjsCodeBlock.meta?.split(' ').includes(OPT_OUT_META) ||
mjsCodeBlock.meta?.split(' ').includes(OPT_OUT_META)
) {
return;
}

let cjs = cjsCodeBlock.value;
if (cjs.startsWith(CJS_PREAMBLE)) {
cjs = cjs.slice(CJS_PREAMBLE.length);
}

let mjs = mjsCodeBlock.value;
if (mjs.startsWith(MJS_PREAMBLE)) {
mjs = mjs.slice(MJS_PREAMBLE.length);
}

// Replace the two code blocks with the JsCodeBlock
parent.children.splice(idx, 2, {
type: 'jsx',
value: `<JsCodeBlock
cjs={${JSON.stringify(cjs)}}
mjs={${JSON.stringify(mjs)}}
/>`,
} as Node);

// Return an ActionTuple [Action, Index], where
// Action SKIP means we want to skip visiting these new children
// Index is the index of the AST we want to continue parsing at.
return [visitParents.SKIP, idx + 1];
}
}

function isImport(node: Node): node is Import {
return node.type === 'import';
}

function isCode(node: Node): node is Code {
return node.type === 'code';
}

0 comments on commit c36cf61

Please sign in to comment.