Created
November 10, 2017 14:53
-
-
Save Lucifier129/1ded759354d42a51123b1120eeb2619d to your computer and use it in GitHub Desktop.
jsx + scheme + renderer
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
/** @jsx h */ | |
class Scheme { | |
constructor(type, props, children) { | |
this.type = type; | |
this.props = props; | |
this.children = children; | |
} | |
} | |
function h(type, props, ...children) { | |
return new Scheme(type, props, flattenList(children)); | |
} | |
function flattenList(list, flatList = []) { | |
for (let i = 0; i < list.length; i++) { | |
let item = list[i]; | |
if (Array.isArray(item)) { | |
flattenList(item, flatList); | |
continue; | |
} | |
if (!(item instanceof Scheme)) { | |
item = new Scheme(null, null, item + ""); | |
} | |
flatList.push(item); | |
} | |
return flatList; | |
} | |
function createRenderer(scheme, Renderer) { | |
let renderer = new Renderer(scheme); | |
if (Array.isArray(scheme.children)) { | |
let children = []; | |
for (let i = 0; i < scheme.children.length; i++) { | |
let current = createRenderer(scheme.children[i], Renderer); | |
let isFirst = i === 0; | |
if (!isFirst) { | |
let previous = children[i - 1]; | |
current.previous = previous; | |
previous.next = current; | |
} | |
current.parent = renderer; | |
current.index = children.length; | |
children.push(current); | |
} | |
renderer.children = children; | |
} | |
return renderer; | |
} | |
class Renderer { | |
constructor(scheme) { | |
this.scheme = scheme; | |
this.isText = !scheme.type; | |
this.parent = null; | |
this.previous = null; | |
this.next = null; | |
this.children = null; | |
this.index = null; | |
} | |
renderText() { | |
return this.text(this.scheme.children); | |
} | |
renderProp(key, value) { | |
this.propStart(); | |
this.prop(key, value); | |
this.propEnd(); | |
} | |
renderProps() { | |
let { props } = this.scheme; | |
this.propsStart(); | |
for (let key in props) { | |
this.renderProp(key, props[key]); | |
} | |
this.propsEnd(); | |
} | |
renderChild(index) { | |
this.childStart(); | |
this.child(this.children[index].render()); | |
this.childEnd(); | |
} | |
renderChildren() { | |
this.childrenStart(); | |
for (let i = 0; i < this.children.length; i++) { | |
this.renderChild(i); | |
} | |
this.childrenEnd(); | |
} | |
renderElement() { | |
let type = this.scheme.type; | |
this.start(type); | |
this.renderProps(); | |
this.renderChildren(); | |
return this.end(type); | |
} | |
render() { | |
if (!this.parent) { | |
this.root(); | |
} | |
if (this.isText) { | |
return this.renderText(); | |
} else { | |
return this.renderElement(); | |
} | |
} | |
root() {} | |
text() {} | |
start() {} | |
propsStart() {} | |
propStart() {} | |
prop() {} | |
propEnd() {} | |
propsEnd() {} | |
childrenStart() {} | |
childStart() {} | |
child() {} | |
childEnd() {} | |
childrenEnd() {} | |
end() {} | |
remove() {} | |
} | |
class StaticHTMLRenderer extends Renderer { | |
constructor(scheme) { | |
super(scheme); | |
this.html = ""; | |
} | |
render() { | |
if (this.html) { | |
return this.html; | |
} | |
return super.render(); | |
} | |
text(text) { | |
this.html = text; | |
return this.html; | |
} | |
start(type) { | |
this.html += `<` + this.scheme.type; | |
} | |
prop(key, value) { | |
if (key === "style") { | |
let style = value; | |
let list = []; | |
for (let name in style) { | |
let styleName = name.replace(/([A-Z])/g, "-$1").toLowerCase(); | |
let item = `${styleName}: ${typeof style[name] === "number" | |
? style[name] + "px" | |
: style[name]};`; | |
list.push(item); | |
} | |
this.html += " " + `style="${list.join(" ")}"`; | |
return; | |
} | |
this.html += " " + `${key}="${value}"`; | |
} | |
propsEnd() { | |
this.html += ">"; | |
} | |
child(child) { | |
this.html += child; | |
} | |
end(type) { | |
this.html += `</${type}>`; | |
return this.html; | |
} | |
} | |
class StaticDOMRenderer extends Renderer { | |
constructor(scheme) { | |
super(scheme); | |
this.node = null; | |
} | |
render() { | |
if (this.node) { | |
return this.node; | |
} | |
return super.render(); | |
} | |
text(text) { | |
this.node = document.createTextNode(text); | |
return this.node; | |
} | |
start(type) { | |
this.node = document.createElement(type); | |
} | |
prop(key, value) { | |
if (key === "style") { | |
let style = value; | |
for (let name in style) { | |
this.node.style[name] = | |
typeof style[name] === "number" ? style[name] + "px" : style[name]; | |
} | |
return; | |
} | |
if (key in this.node) { | |
this.node[key] = value; | |
} else { | |
this.node.setAttribute(key, value + ""); | |
} | |
} | |
child(child) { | |
this.node.appendChild(child); | |
} | |
end() { | |
return this.node; | |
} | |
} | |
class StaticCanvasRenderer extends Renderer { | |
constructor(scheme) { | |
super(scheme); | |
this.canvas = null; | |
this.ctx = null; | |
this.style = scheme.props ? scheme.props.style : null; | |
this.layout = null; | |
} | |
root() { | |
this.canvas = document.createElement("canvas"); | |
this.ctx = this.canvas.getContext("2d"); | |
Object.assign(this.canvas, this.style); | |
} | |
getContext() { | |
if (this.ctx) { | |
return this.ctx; | |
} | |
return this.parent.getContext(); | |
} | |
getLayout() { | |
let { parent, previous } = this; | |
if (this.layout) { | |
return this.layout; | |
} | |
if (!parent) { | |
this.layout = { | |
x: 0, | |
y: 0, | |
...this.style | |
}; | |
return this.layout; | |
} | |
if (!previous) { | |
let parentLayout = parent.getLayout(); | |
this.layout = { | |
x: parentLayout.x, | |
y: parentLayout.y, | |
width: parentLayout.width, | |
...this.style | |
}; | |
if (typeof this.layout.width === "string") { | |
let width = | |
parentLayout.width * Number(this.layout.width.replace("%", "")) / 100; | |
this.layout.width = width; | |
} | |
if (this.style && this.style.marginLeft) { | |
this.layout.x += this.style.marginLeft; | |
} | |
return this.layout; | |
} | |
if (previous) { | |
let parentLayout = parent.getLayout(); | |
let previousLayout = previous.getLayout(); | |
this.layout = { | |
x: parentLayout.x, | |
y: | |
previousLayout.y + | |
previousLayout.height + | |
(previousLayout.marginBottom || 0), | |
...this.style | |
}; | |
if (typeof this.layout.width === "string") { | |
let width = | |
parentLayout.width * Number(this.layout.width.replace("%", "")) / 100; | |
this.layout.width = width; | |
} | |
if (this.style && this.style.marginLeft) { | |
this.layout.x += this.style.marginLeft; | |
} | |
return this.layout; | |
} | |
} | |
drawLayout() { | |
let ctx = this.getContext(); | |
let { x, y, width, height, backgroundColor } = this.getLayout(); | |
ctx.fillStyle = backgroundColor; | |
ctx.fillRect(x, y, width, height); | |
} | |
text(text) { | |
let ctx = this.getContext(); | |
let { x, y, width, height, color } = this.parent.getLayout(); | |
let fontFamily = | |
'-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"'; | |
let lineHeight = this.parent.scheme.type === "h2" ? 24 : 12; | |
ctx.fillStyle = color; | |
ctx.font = `${lineHeight}px ${fontFamily}`; | |
ctx.fillText(text, x, y + lineHeight, width); | |
} | |
start() { | |
this.drawLayout(); | |
} | |
end() { | |
return this.canvas; | |
} | |
} | |
let state = { | |
list: Array.from({ length: 4 }).map((_, index) => ({ | |
title: `title-${index}`, | |
description: `description-${index}` | |
})) | |
}; | |
function StaticApp({ list }) { | |
return ( | |
<ul | |
class="list" | |
style={{ width: 300, height: 500, backgroundColor: "pink" }} | |
> | |
{list.map((item, index) => { | |
return ( | |
<li | |
class="item" | |
data-index={index} | |
style={{ | |
width: "50%", | |
height: 100, | |
marginBottom: 10, | |
marginLeft: 10, | |
backgroundColor: "rgb(234, 234, 234)" | |
}} | |
> | |
<h2 class="title" style={{ height: 60, color: "red" }}> | |
{item.title} | |
</h2> | |
<p class="description" style={{ height: 40, color: "green" }}> | |
{item.description} | |
</p> | |
</li> | |
); | |
})} | |
</ul> | |
); | |
} | |
let appScheme = StaticApp(state); | |
let htmlRenderer = createRenderer(appScheme, StaticHTMLRenderer); | |
let domRenderer = createRenderer(appScheme, StaticDOMRenderer); | |
let canvasRenderer = createRenderer(appScheme, StaticCanvasRenderer); | |
console.time("render to static html"); | |
let html = htmlRenderer.render(); | |
console.timeEnd("render to static html"); | |
console.time("render to static dom"); | |
let dom = domRenderer.render(); | |
console.timeEnd("render to static dom"); | |
console.time("render to static canvas"); | |
let canvas = canvasRenderer.render(); | |
console.timeEnd("render to static canvas"); | |
console.log("isEqual", dom.outerHTML === html); | |
console.log(dom.outerHTML); | |
console.log(html); | |
console.log({ appScheme, htmlRenderer, domRenderer }); | |
let htmlContainer = document.createElement("div"); | |
let root = document.createElement("div"); | |
htmlContainer.innerText = html; | |
root.appendChild(dom); | |
root.appendChild(htmlContainer); | |
root.appendChild(canvas); | |
document.body.innerHTML = ""; | |
document.body.appendChild(root); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment