Skip to content

Commit 020ca06

Browse files
custom MDX webpack loader + example usage
1 parent 118288b commit 020ca06

File tree

8 files changed

+247
-103
lines changed

8 files changed

+247
-103
lines changed

examples/MDX/TestMDX.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react';
2+
import slides from './slides.mdx';
3+
import Deck from '../../src/components/Deck';
4+
import Slide from '../../src/components/Slide';
5+
6+
import { MDXProvider } from '@mdx-js/react';
7+
8+
/*
9+
* Note: you can add mappings here to customize how
10+
* MDX will render standard markdown tags
11+
*/
12+
const components = {
13+
// h5: MyH5Component
14+
};
15+
16+
const MDXTest = () => (
17+
<MDXProvider components={components}>
18+
<Deck loop>
19+
{slides.map((S, i) => (
20+
<Slide key={`slide-${i}`} slideNum={i}>
21+
<S />
22+
</Slide>
23+
))}
24+
</Deck>
25+
</MDXProvider>
26+
);
27+
28+
export default MDXTest;

examples/MDX/slides.mdx

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import Test from './test-component';
2+
export const testProp = 50;
3+
4+
# Hello World
5+
6+
- one
7+
- two
8+
- three
9+
10+
---
11+
12+
## Next
13+
14+
<Test height={testProp} />
15+
16+
---
17+
18+
# Write your Spectacle Presentations in Markdown
19+
20+
## And seamlessly use React Components
21+
22+
**How sweet is that**
23+
**(super sweet)**
24+
25+
---
26+
27+
![datboi](https://pbs.twimg.com/media/CkjFUyTXEAEysBY.jpg)
28+
29+
---
30+
31+
###### Typography
32+
33+
# Heading 1
34+
35+
## Heading 2
36+
37+
### Heading 3
38+
39+
#### Heading 4
40+
41+
##### Heading 5
42+
43+
###### Heading 6
44+
45+
Standard Text
46+
47+
---
48+
49+
> Example Quote
50+
51+
---
52+
53+
```javascript
54+
class SuperCoolComponent extends React.Component {
55+
render() {
56+
return <p>code slide works in markdown too whaaaaat</p>;
57+
}
58+
}
59+
```

examples/MDX/test-component.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from 'react';
2+
3+
const Test = ({ height }) => {
4+
return <div style={{ height, width: '100%', backgroundColor: 'yellow' }} />;
5+
};
6+
7+
export default Test;

examples/MDX/test.mdx

-100
This file was deleted.

index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from 'react';
22
import { render } from 'react-dom';
33

44
// START: test components to try rendering:
5-
import MDXDocument from './examples/MDX/test.mdx';
5+
import TestMDX from './examples/MDX/TestMDX';
66
// import TestJs from './examples/JS/TestJS.js';
77
// END: test components to try rendering
88

@@ -13,4 +13,4 @@ import MDXDocument from './examples/MDX/test.mdx';
1313
* to hot-reload with new contents.
1414
*/
1515

16-
render(<MDXDocument />, document.getElementById('root'));
16+
render(<TestMDX />, document.getElementById('root'));

mdx-slide-loader.js

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
const { getOptions } = require('loader-utils');
2+
const mdx = require('@mdx-js/mdx');
3+
const matter = require('gray-matter');
4+
const normalizeNewline = require('normalize-newline');
5+
6+
const EXREG = /export\sdefault\s/g;
7+
const MODREG = /^(import|export)\s/;
8+
const SLIDEREG = /\n---\n/;
9+
10+
const nameForSlide = index => `MDXContentWrapper${index}`;
11+
12+
module.exports = async function(src) {
13+
const { data, content } = matter(src);
14+
15+
const inlineModules = [];
16+
17+
const callback = this.async();
18+
const options = Object.assign({}, getOptions(this), {
19+
filepath: this.resourcePath
20+
});
21+
22+
/*
23+
Step 1:
24+
* Set aside all inline import and export statements from the mdx file.
25+
* When mdx.sync compiles MDX into JSX, it will stub any component that doesn't have a corresponding
26+
* import. Therefore, we will re-add all of the imports/exports to each
27+
* slide (then remove and add them again!).
28+
*/
29+
const slides = normalizeNewline(content)
30+
.split('\n')
31+
.map(line => {
32+
if (MODREG.test(line)) {
33+
inlineModules.push(line);
34+
}
35+
return line;
36+
})
37+
.filter(line => !MODREG.test(line))
38+
.filter(Boolean)
39+
.join('\n')
40+
/*
41+
Step 2:
42+
* Split the MDX file by occurences of `---`. This is a reserved symbol
43+
* to denote slide boundaries.
44+
*/
45+
.split(SLIDEREG)
46+
/*
47+
Step 3:
48+
* As referenced before, we need to add the imports and exports to
49+
* every slide again. That way mdx.sync can find the component definitions
50+
* for any custom components used in the MDX file.
51+
*/
52+
.map(
53+
slide => `
54+
${inlineModules.join('\n')}\n
55+
${slide}`
56+
)
57+
/*
58+
Step 4:
59+
* Use mdx.sync to compile a separate JSX component for each slide
60+
* written in MDX.
61+
*/
62+
.map(slide => mdx.sync(slide, options))
63+
/*
64+
Step 5:
65+
* mdx.sync will attempt to default export the component generated for each
66+
* slide. However, we have multiple slides and thus multiple generated components.
67+
* We can't export multiple defaults, so we must remove all existing occurences of
68+
* `export default`.
69+
*/
70+
.map(slide => slide.replace(EXREG, ''))
71+
/*
72+
Step 6:
73+
* Remove the inline exports/imports again. We don't want duplicate import/export
74+
* statements littered throughout the file output.
75+
*/
76+
.map(slide =>
77+
slide
78+
.split('\n')
79+
.filter(line => !MODREG.test(line))
80+
.filter(Boolean)
81+
.join('\n')
82+
)
83+
.map(slide => slide.trim())
84+
/*
85+
Step 7:
86+
* The generate component from mdx.sync assumes it's the only component that
87+
* will inhabit a file. It has const definitions outside of the auto-named MDXContent
88+
* component. This would be fine if we weren't generating a component for each
89+
* slide. However, in our case we would generate a lot of duplicate variable names.
90+
* Thus, the easiest solution is to wrap each mdx.sync-generated component+const
91+
* definitions in another component that is uniquely named (using slide index).
92+
*/
93+
.map((slide, i) => {
94+
const wrapperName = nameForSlide(i);
95+
return `function ${wrapperName}(props) {
96+
${slide}
97+
return (<MDXContent {...props} />);
98+
};
99+
${wrapperName}.isMDXComponent = true;`;
100+
});
101+
102+
const { modules = [] } = data;
103+
let wrapperNames = [];
104+
/*
105+
Step 8:
106+
* Begin composing the final output. Include React, mdx, modules, and the inline
107+
* export/import statements that we removed in Step 6.
108+
*/
109+
let allCode = `/* @jsx mdx */
110+
import React from 'react'
111+
import { mdx } from '@mdx-js/react'
112+
${modules.join('\n')}
113+
${inlineModules
114+
.filter(function(el, i, arr) {
115+
return arr.indexOf(el) === i;
116+
})
117+
.join('\n')}\n\n`;
118+
/*
119+
Step 9:
120+
* Add in the slide component definitions. Keep track of the component names.
121+
*/
122+
slides.forEach((s, i) => {
123+
allCode += s + '\n\n';
124+
wrapperNames.push(nameForSlide(i));
125+
});
126+
/*
127+
Step 10:
128+
* Finally, declare the default export as an array of the slide components.
129+
* See /examples/MDX/TestMDX.js for how to import and use the generated slide
130+
* components.
131+
*/
132+
const footer = `export const slideCount = ${slides.length};\n\n
133+
export default [${wrapperNames}]`;
134+
allCode += footer;
135+
136+
return callback(null, allCode);
137+
};

package.json

+7
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"eslint-plugin-import": "^2.9.0",
4949
"eslint-plugin-react": "^7.13.0",
5050
"eslint-plugin-react-hooks": "^1.6.0",
51+
"file-loader": "^4.1.0",
5152
"html-webpack-plugin": "^3.2.0",
5253
"prettier": "^1.15.3",
5354
"prop-types": "^15.7.2",
@@ -61,6 +62,12 @@
6162
"webpack-dev-server": "^3.7.1"
6263
},
6364
"dependencies": {
65+
"@mdx-js/mdx": "^1.1.5",
66+
"@mdx-js/react": "^1.1.5",
67+
"@mdx-js/tag": "^0.20.3",
68+
"gray-matter": "^4.0.2",
69+
"loader-utils": "^1.2.3",
70+
"normalize-newline": "^3.0.0",
6471
"react-spring": "^8.0.25",
6572
"styled-components": "^4.3.1"
6673
}

webpack.config.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ module.exports = {
1818
}
1919
}
2020
},
21-
{ test: /\.mdx?$/, use: ['babel-loader', '@mdx-js/loader'] }
21+
{
22+
test: /\.mdx$/,
23+
use: [
24+
{ loader: 'babel-loader' },
25+
{ loader: require.resolve('./mdx-slide-loader') }
26+
]
27+
}
2228
]
2329
},
2430
plugins: [

0 commit comments

Comments
 (0)