Multilingual codeblocks
Proof of concept
I started off simple, and I use that word because I used a package that provides a tab component in React.
Making one myself was a lot more complex than anticipated I had anticipated, so thank you @reach/tabs
.
My first attempt were 3 tabs with hardcoded labels, very fragile, but perfect as proof of concept.
import { Tabs, TabList, Tab, TabPanels, TabPanel } from "@reach/tabs";import "@reach/tabs/styles.css";const MultiLangCodeBlock = (props) => {return (<Tabs><TabList><Tab>1</Tab><Tab>2</Tab><Tab>3</Tab></TabList><TabPanels><TabPanel>{props.children[0]}</TabPanel><TabPanel>{props.children[1]}</TabPanel><TabPanel>{props.children[2]}</TabPanel></TabPanels></Tabs>);};
I can import that component into a .mdx
blogpost and pass it an array of 3 items to be rendered.
I can even pass a React component as an array item, it’s great.
import { MultiLangCodeBlock } from "./src/components/MultiLangCodeBlock";import { Hobbits } from "./src/components/Hobbits";<MultiLangCodeBlock>{["They're taking the", "Hobbits to Isengard!", <Hobbits />]}</MultiLangCodeBlock>
The result can be seen below. Be sure to check out all tabs.
Codeblocks
Let’s try to pass a codeblock as an item in that array.
const language = "JavaScript";
language = "Python"
let language = "Rust";
That worked!
Be sure to pass those codeblocks as markdown, not as a literal JavaScript array.
<MultiLangCodeBlock>```jsconst language = "JavaScript";``````pythonlanguage = "Python"``````rustlet language = "Rust";```</MultiLangCodeBlock>
Tab labels
I replaced the hardcoded amount of tabs, and made it so the tab labels were the language from the triple backtick codeblock.
Using my single language CodeBlock
component means the fancy options (like highlighting specific lines) work too!
const MultiLangCodeBlock = (props) => {return (<Tabs><TabList>{props.children.map((child) => {const [language] = child.props.children.props.className.replace(/language-/, ``).split(` `);return <Tab>{language}</Tab>;})}</TabList><TabPanels>{props.children.map((child) => {return <TabPanel>{child}</TabPanel>;})}</TabPanels></Tabs>);};
This mdx
will generate the output underneath this block, neat!
<MultiLangCodeBlock>```js title=index.js numberLines hl=1const language = "JavaScript";console.log(language);``````python title=index.py numberLines hl=1language = "Python"print(language)``````rust title=index.rs numberLines hl=1let language = "Rust";println!("{}", language);```</MultiLangCodeBlock>
1const language = "JavaScript";23console.log(language);
1language = "Python"23print(language)
1let language = "Rust";23println!("{}", language);
This doesn’t look too bad.
Buttons inside the codeblock
I wanted the tab buttons to be inside the header of a codeblock, to the right of the title.
The tab buttons being inside the codeblock was a problem. That meant the button for a tab would be inside the content for a tab, that doesn’t sound right.
I decided to manually render my single language CodeBlock
component without a top part.
The CodeBlock
component would no longer be responsible for that header, the MultiLangCodeBlock
would take on that responsability.
The title
option is what causes the single language component to have a header.
So I prevented that option from reaching the single language block by not passing that title as a prop.
I still want the title to be displayed however, so I have to use that title information in the new MultiLangCodeBlock
.
That meant I needed a bit of state management to keep track of the current tab index, the title, and the label for that tab.
The MultiLangCodeBlock
looked great after a little CSS to display the tab buttons to the right of the title and make that entire line look like the top of my single language codeblocks.
const MultiLangCodeBlock = ({ children }) => {const codeTitles = children.map((child) => child?.props?.children?.props?.title);const tabLabels = children.map((child) =>child?.props?.children?.props?.className.replace(/language-/, ``).split(` `));const [tabIndex, setTabindex] = useState(0);const [title, setTitle] = useState(codeTitles[0]);const handleTabsChange = (index) => {setTabindex(index);setTitle(codeTitles[index]);};return (<Tabs index={tabIndex} onChange={handleTabsChange}><div sx={{ display: "flex", variant: `styles.CodeBlock.title` }}><div sx={{ flex: 1 }}>{title}</div><TabListsx={{color: "mutedText","[data-selected]": { color: "mutedPrimary" },}}>{tabLabels.map((label) => (<Tab key={label}>{label}</Tab>))}</TabList></div><TabPanels>{children.map((child) => {// split off title so the CodeBlock from the theme doesn't render a header, this component does thatconst { title, ...blockProps } = child.props.children.props;return (<TabPanel key={blockProps.className}><CodeBlock {...blockProps} /></TabPanel>);})}</TabPanels></Tabs>);};
What’s in the .mdx
file:
<TabsInTitle>```js title=index.js numberLines hl=1const language = "JavaScript";console.log(language);``````python title=index.py numberLines hl=1language = "Python"print(language)``````rust title=index.rs numberLines hl=1let language = "Rust";println!("{}", language);```</TabsInTitle>
The resulting output:
1const language = "JavaScript";23console.log(language);
1language = "Python"23print(language)
1let language = "Rust";23println!("{}", language);