Skip to content

Commit

Permalink
feat(mdx-code): add sandpack live code editor support (#794)
Browse files Browse the repository at this point in the history
close #777
  • Loading branch information
sabertazimi committed May 4, 2022
1 parent 91eb4e2 commit 1f7945e
Show file tree
Hide file tree
Showing 11 changed files with 733 additions and 3 deletions.
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
"cSpell.words": [
"antd",
"browserslist",
"codesandbox",
"flexbugs",
"gemoji",
"katex",
"monokai",
"nocopy",
"noline",
"rehype",
"sandpack",
"stylelint",
"tailwindcss",
"vercel"
Expand Down
24 changes: 24 additions & 0 deletions components/Editor/Editor.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { render, screen } from '@testing-library/react';
import Editor from './Editor';

describe('Editor', () => {
test('should render sandpack correctly', () => {
render(
<Editor>
<pre>
<code className="tsx">const foo = bar();</code>
</pre>
{/* @ts-expect-error code meta data */}
<pre filename="bar.tsx">
<code className="tsx">const bar = foo();</code>
</pre>
</Editor>
);

expect(screen.getByRole('tablist')).toBeInTheDocument();
expect(screen.getByText('App.tsx')).toBeInTheDocument();
expect(screen.getByText('bar.tsx')).toBeInTheDocument();
expect(screen.getByRole('group')).toBeInTheDocument();
expect(screen.getByText('const foo = bar();')).toBeInTheDocument();
});
});
60 changes: 60 additions & 0 deletions components/Editor/Editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { SandpackPredefinedTemplate } from '@codesandbox/sandpack-react';
import { monokaiProTheme, Sandpack } from '@codesandbox/sandpack-react';
import '@codesandbox/sandpack-react/dist/index.css';
import type { ReactElement, ReactNode } from 'react';
import React from 'react';
import { languageToFilepath, normalizeFilepath } from './utils';

interface Props {
template?: SandpackPredefinedTemplate;
children?: ReactNode;
}

const Editor = ({ template = 'react-ts', children }: Props): JSX.Element => {
const codeSnippets = React.Children.toArray(children);
const files = codeSnippets.reduce(
(
result: {
[key in string]: {
code: any;
};
},
codeSnippet
) => {
const preElement = codeSnippet as ReactElement;
const codeElement = preElement.props.children;

const filename = preElement.props.filename;
const language = codeElement.props.className.replace('language-', '');
const filePath =
normalizeFilepath(filename) || languageToFilepath(language);
const code = codeElement.props.children;

result[filePath] = {
code,
};

return result;
},
{}
);

return (
<Sandpack
template={template}
theme={monokaiProTheme}
customSetup={{
files,
dependencies: {},
}}
options={{
showLineNumbers: true,
showInlineErrors: false,
showTabs: true,
externalResources: [],
}}
/>
);
};

export default Editor;
1 change: 1 addition & 0 deletions components/Editor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './Editor';
47 changes: 47 additions & 0 deletions components/Editor/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { languageToFilepath, normalizeFilepath } from './utils';

describe('languageToFilepath', () => {
test('should return /styles.css if language is css', () => {
expect(languageToFilepath('css')).toBe('/styles.css');
});

test('should return /App.js if language is js', () => {
expect(languageToFilepath('js')).toBe('/App.js');
expect(languageToFilepath('javascript')).toBe('/App.js');
});

test('should return /App.ts if language is ts', () => {
expect(languageToFilepath('ts')).toBe('/App.ts');
expect(languageToFilepath('typescript')).toBe('/App.ts');
});

test('should return /App.jsx if language is jsx', () => {
expect(languageToFilepath('jsx')).toBe('/App.jsx');
});

test('should return /App.tsx if language is tsx', () => {
expect(languageToFilepath('tsx')).toBe('/App.tsx');
});

test('should return /src/App.vue if language is vue', () => {
expect(languageToFilepath('vue')).toBe('/src/App.vue');
});

test('should return /App.tsx if language is not provided', () => {
expect(languageToFilepath()).toBe('/App.tsx');
});
});

describe('normalizeFilepath', () => {
test('should return empty string if no filename is provided', () => {
expect(normalizeFilepath()).toBe('');
});

test('should return filename if it starts with /', () => {
expect(normalizeFilepath('/foo.tsx')).toBe('/foo.tsx');
});

test('should return /filename if it does not start with /', () => {
expect(normalizeFilepath('foo.tsx')).toBe('/foo.tsx');
});
});
34 changes: 34 additions & 0 deletions components/Editor/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const normalizeFilepath = (filename?: string): string => {
if (!filename) {
return '';
}

if (!filename.startsWith('/')) {
return `/${filename}`;
}

return filename;
};

const languageToFilepath = (language?: string): string => {
switch (language) {
case 'css':
return '/styles.css';
case 'js':
case 'javascript':
return '/App.js';
case 'ts':
case 'typescript':
return '/App.ts';
case 'jsx':
return '/App.jsx';
case 'tsx':
return '/App.tsx';
case 'vue':
return '/src/App.vue';
default:
return '/App.tsx';
}
};

export { languageToFilepath, normalizeFilepath };
10 changes: 9 additions & 1 deletion components/MDX/MDX.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ import { Item, Ol, Ul } from '@components/Lists';
import Paragraph from '@components/Paragraph';
import Table from '@components/Table';
import { Anchor, Delete, Emphasis, Strong } from '@components/Texts';
import { dynamic } from '@components/utils';
import Code from './MDXCode';
import Divider from './MDXDivider';
import Input from './MDXInput';
import Pre from './MDXPre';

const Editor = dynamic(() => import('@components/Editor')) as any;

const Headings = {
h1: H1,
h2: H2,
Expand Down Expand Up @@ -40,6 +43,11 @@ const CodeBlocks = {
pre: Pre,
};

const customComponents = {
Button,
Editor,
};

const MDX = {
p: Paragraph,
hr: Divider,
Expand All @@ -53,7 +61,7 @@ const MDX = {
...Texts,
...Lists,
...CodeBlocks,
Button,
...customComponents,
};

export default MDX;
35 changes: 35 additions & 0 deletions contents/implementFancyCodeBlock.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,41 @@ const App = () => {
export default App;
```

Live code editor `<Editor>`:

<Editor>

```tsx
import Greet from './Greet';

export default function MyApp() {
return (
<div>
<h1>My App</h1>
<Greet />
</div>
);
}
```

```tsx filename="Greet.tsx"
function Greeting({ name }) {
return <h3>Hello, {name}!</h3>;
}

export default function Greet() {
return (
<div>
<Greeting name="Nintendo" />
<Greeting name="Sony" />
<Greeting name="Microsoft" />
</div>
);
}
```

</Editor>

## Language

### Markup Code
Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ module.exports = async () => {
const jestConfig = await createJestConfig();
const transformIgnorePatterns = [
// Transform ESM-only modules in `node_modules`.
'/node_modules/(?!next-mdx-remote|@mdx-js)',
'/node_modules/(?!next-mdx-remote|@mdx-js|@react-hook)',
...jestConfig.transformIgnorePatterns.filter(
pattern => pattern !== '/node_modules/'
),
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
},
"dependencies": {
"@ant-design/icons": "^4.7.0",
"@codesandbox/sandpack-react": "^0.19.6",
"@octokit/rest": "^18.12.0",
"antd": "^4.20.2",
"classnames": "^2.3.1",
Expand Down
Loading

1 comment on commit 1f7945e

@vercel
Copy link

@vercel vercel bot commented on 1f7945e May 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

blog – ./

blog-git-main-sabertaz.vercel.app
blog.tazimi.dev
blog-sabertaz.vercel.app

Please sign in to comment.