-
Notifications
You must be signed in to change notification settings - Fork 62.1k
A quick experiment to get allow React components in Markdown pages and throughout the site #610
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b781e43
797adb1
c5a34e4
d264c65
d51688b
35c38e5
b1fa93f
548853f
6e5673c
8ef2822
89d745b
3c71bc7
b8c8e0f
7573ff2
fe81204
a48998c
561bd33
74d43b5
c084360
0c3309d
07a7822
d830284
84473cb
791c7c6
8731b68
632ef6c
2d07086
f89c983
92ba8d9
c410020
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -89,6 +89,9 @@ const schema = { | |
href: { type: 'string' } | ||
} | ||
} | ||
}, | ||
interactive: { | ||
type: 'boolean' | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -16,6 +16,7 @@ const pathUtils = require('./path-utils') | |||||
const Permalink = require('./permalink') | ||||||
const languages = require('./languages') | ||||||
const renderContent = require('./render-content') | ||||||
const { renderReact } = require('./react/engine') | ||||||
const frontmatter = require('./frontmatter') | ||||||
const products = require('./all-products') | ||||||
const slash = require('slash') | ||||||
|
@@ -133,10 +134,31 @@ class Page { | |||||
this.title = await renderContent(this.rawTitle, context, { textOnly: true, encodeEntities: true }) | ||||||
this.shortTitle = await renderContent(this.shortTitle, context, { textOnly: true, encodeEntities: true }) | ||||||
|
||||||
const markdown = this.mapTopic | ||||||
let markdown = this.mapTopic | ||||||
? getMapTopicContent(this, context.pages, context.redirects) | ||||||
: this.markdown | ||||||
|
||||||
// If the article is interactive parse the React! | ||||||
if (this.interactive) { | ||||||
// Search for the react code comments to find the react components | ||||||
const reactComponents = markdown.match(/<!--react-->(.*?)<!--end-react-->/gs) | ||||||
|
||||||
// Render each of the react components in the markdown | ||||||
await Promise.all(reactComponents.map(async (reactComponent) => { | ||||||
let componentStr = reactComponent | ||||||
|
||||||
// Remove the React comment indicators | ||||||
componentStr = componentStr.replace('<!--react-->\n', '').replace('<!--react-->', '') | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possible to use a RegExp with replace, like
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you confirm this works @heiskr and if it does can you commit it? I get confused by the first replace having a new line character and the next one not. I personally like coding verbose. But that's a personal preference. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think its fine as is, just an FYI more than anything. |
||||||
componentStr = componentStr.replace('\n<!--end-react-->', '').replace('<!--end-react-->', '') | ||||||
|
||||||
// Get the rendered component | ||||||
const renderedComponent = await renderReact(componentStr) | ||||||
|
||||||
// Replace the react component with the rendered markdown | ||||||
markdown = markdown.replace(reactComponent, renderedComponent) | ||||||
})) | ||||||
} | ||||||
|
||||||
const html = await renderContent(markdown, context) | ||||||
|
||||||
// product frontmatter may contain liquid | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
const babel = require('@babel/core') | ||
|
||
const reactBabelOptions = { | ||
presets: [ | ||
'@babel/preset-env', | ||
'@babel/preset-react' | ||
], | ||
plugins: [ | ||
'@babel/plugin-transform-react-jsx', | ||
'@babel/plugin-proposal-object-rest-spread', | ||
'@babel/plugin-proposal-class-properties', | ||
'@babel/transform-runtime' | ||
] | ||
} | ||
|
||
const transform = code => | ||
babel.transform(code, reactBabelOptions).code | ||
|
||
module.exports = { | ||
transform: transform, | ||
reactBabelOptions: reactBabelOptions | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
const { renderToString } = require('react-dom/server') | ||
const { transform } = require('./babel') | ||
const React = require('react') | ||
const fs = require('fs') | ||
const path = require('path') | ||
const dirTree = require('directory-tree') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We also have a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You want to try and switch to it @heiskr so we don't have to add another package? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like directory-tree and walk-sync have different output. walk-sync creates a flat array of just the file names, where directory-tree has a nested structure. I think there might be consequences to the difference as this code seems to look at just the first level. I think it's okay to ship directory-tree for now, and we can always change it later if we find another pattern we prefer. |
||
|
||
// Name of directory for saving transformed components that should be gitignored | ||
const dist = 'dist' | ||
|
||
// Build React components | ||
// This loops through the react components and transpiles them to /dist | ||
// so they can be used by Node.js when we do server side rendering | ||
const tree = dirTree('./react/') | ||
if (tree) { | ||
for (const index in tree.children) { | ||
const file = tree.children[index] | ||
if (file.type === 'file' && file.extension === '.js') { | ||
if (!fs.existsSync(path.join(dist, 'react'))) { | ||
fs.mkdirSync(path.join(dist, 'react'), { recursive: true }) | ||
} | ||
const content = transform(fs.readFileSync(file.path, 'utf8')) | ||
fs.writeFileSync(path.join(dist, file.path), content) | ||
} | ||
} | ||
} | ||
// End Build React Components | ||
|
||
// Register components | ||
const components = { | ||
// CodeBlock: require('../../dist/react/CodeBlock'), | ||
// CodeEditor: require('../../dist/react/CodeEditor') | ||
} | ||
|
||
const renderReact = async componentStr => { | ||
// Get component name as string so we can use it in the class name | ||
// which will be needed later if we choose to do client side React hydration | ||
const componentName = componentStr.match(/<([a-zA-Z]+)\s/)[1] | ||
// Add the wrapper and class name so we can later use React hydration on the client | ||
// side | ||
const jsx = `<div className="react-component-${componentName}">\n${componentStr}\n</div>` | ||
|
||
const component = transform(jsx) | ||
|
||
// eslint-disable-next-line | ||
const getComponent = new Function( | ||
'React', | ||
...Object.keys(components), | ||
`${component.replace('React', 'return React')}` | ||
) | ||
|
||
return renderToString(getComponent(React, ...Object.values(components))) | ||
} | ||
|
||
module.exports = { | ||
renderReact | ||
} |
Uh oh!
There was an error while loading. Please reload this page.