Last active
October 11, 2019 08:56
-
-
Save jamesseanwright/6b76b5d26776f7796f5fbc479d22a868 to your computer and use it in GitHub Desktop.
Toy, stack-based HTML renderer with React.createElement-compliant element creator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
const indentWidth = 2; | |
const selfClosingElements = [ | |
'meta', | |
'link', | |
]; | |
const indent = (string, depth) => | |
`${' '.repeat(depth * indentWidth)}${string}`; | |
const serialiseAttrs = attrs => { | |
const entries = Object.entries(attrs); | |
return entries.length | |
? ` ${entries.map(([name, value]) => `${name}="${value}"`)}` | |
: ''; | |
}; | |
const isFunctionalComponent = element => | |
typeof element.type === 'function'; | |
const renderWithChildren = (element, stack, depth) => { | |
stack.push(indent(`<${element.type}${serialiseAttrs(element.props)}>`, depth)); | |
element.children.forEach(child => | |
typeof child === 'string' | |
? stack.push(indent(child, depth + 1)) | |
: render(child, stack, depth + 1) | |
); | |
stack.push(indent(`</${element.type}>`, depth)); | |
}; | |
const render = (element, stack, depth) => { | |
if (isFunctionalComponent(element)) { | |
render(element.type(element.props), stack, depth); | |
} else if (selfClosingElements.includes(element.type)) { | |
stack.push(indent(`<${element.type}${serialiseAttrs(element.props)} />`, depth)); | |
} else { | |
renderWithChildren(element, stack, depth); | |
} | |
return stack.join('\n'); | |
}; | |
const renderToHtml = element => render(element, [], 0); | |
/* React.createElement's props argument is mandatory, | |
* transpiled to `null` by @babel/plugin-transform-react-jsx | |
* whenever an element in JSX has no props. Since default | |
* values only apply for `undefined` values, we explicitly | |
* check for its existence with the ternary operator */ | |
const createElement = (type, props, ...children) => ({ | |
type, | |
props: props ? props : {}, // nullish coalescing would be sick here | |
children, | |
}); | |
const App = ({ greeting }) => ( | |
<main data-foo="bar"> | |
<h1>{greeting}</h1> | |
</main> | |
); | |
/* To transpile our JSX to createElement, | |
* we can use @babel/plugin-react-transform-jsx | |
* i.e: | |
* | |
* .babelrc: | |
* { | |
* "plugins": [ | |
* ["@babel/transform-react-jsx", { | |
* "pragma": "createElement" | |
* }] | |
* ] | |
* }*/ | |
const tree = ( | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8" /> {/* TODO: propname mappings i.e. charSet */} | |
<title>Hello world!</title> | |
</head> | |
<body> | |
<App greeting="Hello world!" /> | |
</body> | |
</html> | |
); | |
console.log(renderToHtml(tree)); | |
/* => | |
<html lang="en"> | |
<head> | |
<title> | |
Hello world! | |
</title> | |
</head> | |
<body> | |
<main data-foo="bar"> | |
<h1> | |
Hello world! | |
</h1> | |
</main> | |
</body> | |
</html> | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment