Skip to content

Instantly share code, notes, and snippets.

@jviide
Last active November 7, 2020 22:39
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jviide/66e3451674183c06ae0bc48cc0ebf748 to your computer and use it in GitHub Desktop.
Save jviide/66e3451674183c06ae0bc48cc0ebf748 to your computer and use it in GitHub Desktop.

A Small HTM VM FTW OMG BBQ

This is a half-baked sketch of compiling HTM expressions into a list of instructions that can be interpreted in runtime with a compact function. The instructions hopefully compress well and the evaluation function should be not-slow. Maybe.

  • evaluate.js contains the evaluation function (evaluate(...)) as well as an example of a compiled HTM call.

  • babel.js contains a Babel plugin that transforms HTM calls into the stack language. The build.mjs file it imports is HTM's src/build.mjs.

This tweet demonstrates that a minified evaluate() fits in one tweet: https://twitter.com/jviide/status/1257755526722662405 🙂

import { build, treeify } from '../../src/build.mjs';
const SEPARATOR = '|';
const APPEND = 0;
const SET = 1;
const PUSH = 2;
const ASSIGN = 3;
const EVAL = 4;
const ARG = 5;
const TRUE = 6;
const NULL = 7;
const OBJECT = 8;
const EXPR = 9;
/**
* @param {Babel} babel
* @param {object} options
* @param {string} [options.tag=html] The tagged template "tag" function name to process.
*/
export default function htmBabelPlugin({ types: t }, options = {}) {
function patternStringToRegExp(str) {
const parts = str.split('/').slice(1);
const end = parts.pop() || '';
return new RegExp(parts.join('/'), end);
}
function transform(node, ops, strings, values) {
function put(value) {
if (t.isNode(value)) {
values.push(value);
return ARG;
}
else if (value === true) {
return TRUE;
}
values.push(t.stringLiteral(value));
return ARG;
}
function assign(value) {
values.push(value);
return ASSIGN;
}
if (t.isNode(node) || typeof node === 'string') {
ops.push(put(node));
return;
}
const { tag, props, children } = node;
put(tag);
if (props.length === 0) {
ops.push(NULL);
}
else if (props.length === 1 && t.isNode(props[0])) {
values.push(props[0]);
ops.push(EXPR);
}
else {
ops.push(OBJECT);
props.forEach((obj, index) => {
if (t.isNode(obj)) {
ops.push(assign(obj));
}
else {
Object.entries(obj).forEach(([key, values]) => {
if (values.length === 0) {
return;
}
values.forEach((value, index) => {
ops.push(put(value));
if (index > 0) {
ops.push(APPEND);
}
});
ops.push(put(key), SET);
});
}
});
}
children.forEach(child => {
transform(child, ops, strings, values);
ops.push(PUSH);
});
ops.push(EVAL);
}
// The tagged template tag function name we're looking for.
// This is static because it's generally assigned via htm.bind(h),
// which could be imported from elsewhere, making tracking impossible.
const htmlName = options.tag || 'html';
return {
name: 'htm',
visitor: {
TaggedTemplateExpression: {
exit(path) {
const tag = path.node.tag.name;
if (htmlName[0]==='/' ? patternStringToRegExp(htmlName).test(tag) : tag === htmlName) {
const quasis = path.node.quasi.quasis.map(e => e.value.raw);
const expr = path.node.quasi.expressions;
let tree = treeify(build(quasis), expr);
let ops = [];
let strings = [];
let values = [];
if (Array.isArray(tree)) {
ops.push(TRUE);
tree.forEach(root => {
transform(root, ops, strings, values);
ops.push(PUSH);
});
}
else if (tree) {
transform(tree, ops, strings, values);
}
path.replaceWith(
t.callExpression(
path.node.tag,
[
t.stringLiteral([ops.join(''), ...strings].join(SEPARATOR)),
...values
]
)
);
path.skip();
}
}
}
}
};
}
function evaluate(ops) {
const args = arguments;
const stack = [];
for (let opIdx = 0, argIdx = 0, sp = -1, op; (op = ops[opIdx++]);) {
if (!op--) {
stack[sp - 1] += '' + stack[sp--];
}
else if (!op--) {
stack[sp - 2][1][stack[sp--]] = stack[sp--];
}
else if (!op--) {
stack[sp - 1].push(stack[sp--]);
}
else if (!op--) {
Object.assign(stack[sp][1], args[++argIdx]);
}
else if (!op--) {
stack[sp] = this.apply(0, stack[sp]);
}
else {
stack[++sp] = op-- ? op-- ? [args[++argIdx], op-- ? op ? args[++argIdx] : {} : null] : opIdx > 1 || [] : args[++argIdx];
}
}
return stack[0];
}
const html = evaluate.bind((type, props, ...children) => ({ type, props, children }));
html("8551651952424","div","hello","class","a","h1",{id:'hello'},"Hello | World!");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment