Created
February 10, 2020 14:23
-
-
Save phaus/a254f6df498bd7b45489c89a52cc9312 to your computer and use it in GitHub Desktop.
resulting dist/bundle.js
This file contains hidden or 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
(function () { | |
'use strict'; | |
let BLANKS = [undefined, null, false]; | |
function simpleLog(type, msg) { | |
console.log(`[${type}] ${msg}`); // eslint-disable-line no-console | |
} // returns a function that invokes `callback` only after having itself been | |
// invoked `total` times | |
function awaitAll(total, callback) { | |
let i = 0; | |
return _ => { | |
i++; | |
if (i === total) { | |
callback(); | |
} | |
}; | |
} // flattens array while discarding blank values | |
function flatCompact(items) { | |
return items.reduce((memo, item) => { | |
return BLANKS.indexOf(item) !== -1 ? memo : // eslint-disable-next-line indent | |
memo.concat(item.pop ? flatCompact(item) : item); | |
}, []); | |
} | |
function blank(value) { | |
return BLANKS.indexOf(value) !== -1; | |
} | |
function repr(value, jsonify = true) { | |
return `\`${jsonify ? JSON.stringify(value) : value}\``; | |
} | |
function noop() {} | |
let Fragment = {}; // poor man's symbol; used for virtual wrapper elements | |
// cf. https://www.w3.org/TR/html5/syntax.html#void-elements | |
let VOID_ELEMENTS = {}; // poor man's `Set` | |
["area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"].forEach(tag => { | |
VOID_ELEMENTS[tag] = true; | |
}); // generates an "element generator" function which serves as a placeholder and, | |
// when invoked, writes the respective HTML to an output stream | |
// | |
// such an element generator expects three arguments: | |
// * a writable stream (an object with methods `#write`, `#writeln` and `#flush`) | |
// * an options object: | |
// * `nonBlocking`, if truthy, permits non-blocking I/O | |
// * `log` is a logging function with the signature `(level, message)`; note | |
// that violations of HTML semantics are logged as "error", though user | |
// agents might still successfully render the respective document | |
// * a callback function which is invoked upon conclusion, without any arguments | |
// | |
// the indirection via element generators serves two purposes: since this | |
// function implements the signature expected by JSX (which is essentially a DSL | |
// for function invocations), we need to inject additional arguments by other | |
// means - plus we need to defer element creation in order to ensure proper | |
// order and nesting: | |
// | |
// <body id="top"> | |
// <h1>hello world</h1> | |
// </body> | |
// | |
// turns into | |
// | |
// createElement("body", { id: "top" }, | |
// createElement("h1", null, "hello world")); | |
// | |
// without a thunk-style indirection, `<h1>` would be created before `<body>` | |
function generateHTML(tag, params, ...children) { | |
return (stream, options, callback) => { | |
let { | |
nonBlocking, | |
log = simpleLog, | |
_idRegistry = {} | |
} = options || {}; | |
if (tag !== Fragment) { | |
let attribs = generateAttributes(params, { | |
tag, | |
log, | |
_idRegistry | |
}); | |
stream.write(`<${tag}${attribs}>`); | |
} // NB: | |
// * discarding blank values (`undefined`, `null`, `false`) to allow for | |
// conditionals with boolean operators (`condition && value`) | |
// * `children` might contain nested arrays due to the use of | |
// collections within JSX (`{items.map(item => <span>{item}</span>)}`) | |
children = flatCompact(children); | |
let isVoid = VOID_ELEMENTS[tag]; | |
let closingTag = isVoid || tag === Fragment ? null : tag; | |
let total = children.length; | |
if (total === 0) { | |
closeElement(stream, closingTag, callback); | |
} else { | |
if (isVoid) { | |
log("error", `void elements must not have children: \`<${tag}>\``); | |
} | |
let close = awaitAll(total, _ => { | |
closeElement(stream, closingTag, callback); | |
}); | |
processChildren(stream, children, 0, { | |
tag, | |
nonBlocking, | |
log, | |
_idRegistry | |
}, close); | |
} | |
}; | |
} | |
function HTMLString(str) { | |
if (blank(str) || !str.substr) { | |
throw new Error(`invalid ${repr(this.constructor.name, false)}: ${repr(str)}`); | |
} | |
this.value = str; | |
} // adapted from TiddlyWiki <http://tiddlywiki.com> and Python 3's `html` module | |
function htmlEncode(str, attribute) { | |
let res = str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); | |
if (attribute) { | |
res = res.replace(/"/g, """).replace(/'/g, "'"); | |
} | |
return res; | |
} | |
function processChildren(stream, children, startIndex, options, callback) { | |
for (let i = startIndex; i < children.length; i++) { | |
let child = children[i]; | |
if (!child.call) { | |
// leaf node(s) | |
let content = child instanceof HTMLString ? // eslint-disable-next-line indent | |
child.value : htmlEncode(child.toString()); | |
stream.write(content); | |
callback(); | |
continue; | |
} | |
let { | |
nonBlocking, | |
log, | |
_idRegistry | |
} = options; | |
let generatorOptions = { | |
nonBlocking, | |
log, | |
_idRegistry | |
}; | |
if (child.length !== 1) { | |
// element generator -- XXX: brittle heuristic (arity) | |
child(stream, generatorOptions, callback); | |
continue; | |
} // deferred child element | |
let fn = element => { | |
element(stream, generatorOptions, callback); | |
let next = i + 1; | |
if (next < children.length) { | |
processChildren(stream, children, next, options, callback); | |
} | |
}; | |
if (!nonBlocking) { | |
// ensure deferred child element is synchronous | |
let invoked = false; | |
let _fn = fn; | |
fn = function () { | |
invoked = true; | |
return _fn.apply(null, arguments); | |
}; | |
let _child = child; | |
child = function () { | |
let res = _child.apply(null, arguments); | |
if (!invoked) { | |
let msg = "invalid non-blocking operation detected"; | |
throw new Error(`${msg}: \`${options.tag}\``); | |
} | |
return res; | |
}; | |
} | |
child(fn); | |
break; // remainder processing continues recursively above | |
} | |
} | |
function closeElement(stream, tag, callback) { | |
if (tag !== null) { | |
// void elements must not have closing tags | |
stream.write(`</${tag}>`); | |
} | |
stream.flush(); | |
callback(); | |
} | |
function generateAttributes(params, { | |
tag, | |
log, | |
_idRegistry | |
}) { | |
if (!params) { | |
return ""; | |
} | |
if (_idRegistry && params.id !== undefined) { | |
let { | |
id | |
} = params; | |
if (_idRegistry[id]) { | |
log("error", `duplicate HTML element ID: ${repr(params.id)}`); | |
} | |
_idRegistry[id] = true; | |
} | |
let attribs = Object.keys(params).reduce((memo, name) => { | |
let value = params[name]; | |
switch (value) { | |
// blank attributes | |
case null: | |
case undefined: | |
break; | |
// boolean attributes (e.g. `<input … autofocus>`) | |
case true: | |
memo.push(name); | |
break; | |
case false: | |
break; | |
// regular attributes | |
default: | |
// cf. https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 | |
if (/ |"|'|>|'|\/|=/.test(name)) { | |
reportAttribError(`invalid HTML attribute name: ${repr(name)}`, tag, log); | |
break; | |
} | |
if (typeof value === "number") { | |
value = value.toString(); | |
} else if (!value.substr) { | |
reportAttribError(`invalid value for HTML attribute \`${name}\`: ` + `${repr(value)} (expected string)`, tag, log); | |
break; | |
} | |
memo.push(`${name}="${htmlEncode(value, true)}"`); | |
} | |
return memo; | |
}, []); | |
return attribs.length === 0 ? "" : ` ${attribs.join(" ")}`; | |
} | |
function reportAttribError(msg, tag, log) { | |
log("error", `${msg} - did you perhaps intend to use \`${tag}\` as a macro?`); | |
} | |
function createElement(element, params, ...children) { | |
if (element === undefined) { | |
// TODO; provide context by stringifying `params` + `children` via `generateHTML` | |
throw new Error("invalid macro: `undefined`"); | |
} | |
/* eslint-disable indent */ | |
return element.call ? element(params === null ? {} : params, ...flatCompact(children)) : generateHTML(element, params, ...children); | |
/* eslint-enable indent */ | |
} // a renderer typically provides the interface to the host environment | |
// it maps views' string identifiers to the corresponding macros and supports | |
// both HTML documents and fragments | |
// `log` is an optional logging function with the signature `(level, message)` | |
// (cf. `generateHTML`) | |
class Renderer { | |
constructor({ | |
doctype = "<!DOCTYPE html>", | |
log | |
} = {}) { | |
this.doctype = doctype; | |
this.log = log; | |
this._macroRegistry = {}; // bind methods for convenience | |
["registerView", "renderView"].forEach(meth => { | |
this[meth] = this[meth].bind(this); | |
}); | |
} | |
registerView(macro, name = macro.name, replace) { | |
if (!name) { | |
throw new Error(`missing name for macro: \`${macro}\``); | |
} | |
let macros = this._macroRegistry; | |
if (macros[name] && !replace) { | |
throw new Error(`invalid macro name: \`${name}\` already registered`); | |
} | |
macros[name] = macro; | |
return name; // primarily for debugging | |
} // `view` is either a macro function or a string identifying a registered macro | |
// `params` is a mutable key-value object which is passed to the respective macro | |
// `stream` is a writable stream (cf. `generateHTML`) | |
// `fragment` is a boolean determining whether to omit doctype and layout | |
// `callback` is an optional function invoked upon conclusion - if provided, | |
// this activates non-blocking rendering | |
renderView(view, params, stream, { | |
fragment | |
} = {}, callback) { | |
if (!fragment) { | |
stream.writeln(this.doctype); | |
} | |
if (fragment) { | |
if (!params) { | |
params = {}; | |
} | |
params._layout = false; // XXX: hacky? (e.g. might break due to immutability) | |
} // resolve string identifier to corresponding macro | |
let viewName = view && view.substr && view; | |
let macro = viewName ? this._macroRegistry[viewName] : view; | |
if (!macro) { | |
throw new Error(`unknown view macro: \`${view}\` is not registered`); | |
} // augment logging with view context | |
let log = this.log && ((level, message) => this.log(level, `<${viewName || macro.name}> ${message}`)); | |
let element = createElement(macro, params); | |
if (blank(element)) { | |
element = createElement(Fragment); | |
} | |
if (callback) { | |
// non-blocking mode | |
element(stream, { | |
nonBlocking: true, | |
log | |
}, callback); | |
} else { | |
// blocking mode | |
element(stream, { | |
nonBlocking: false, | |
log | |
}, noop); | |
} | |
} | |
} | |
let { | |
renderView | |
} = new Renderer(); | |
}()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment