From 3a5d6a114bd90a2062701f33c912963334fa2d2b Mon Sep 17 00:00:00 2001 From: Mike Ralphson Date: Mon, 19 Jul 2021 15:35:54 +0100 Subject: [PATCH] perf: switch to tempura for templating --- .eslintrc.json | 10 ++- src/_includes/card.html | 59 ++++++------- src/_layouts/apis-page.liquid | 2 - src/assets/javascript/apis.js | 13 ++- src/assets/javascript/main.js | 2 - src/assets/javascript/tempura.js | 138 +++++++++++++++++++++++++++++++ src/index.md | 10 ++- 7 files changed, 186 insertions(+), 48 deletions(-) create mode 100644 src/assets/javascript/tempura.js diff --git a/.eslintrc.json b/.eslintrc.json index f588728..f246ece 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -6,7 +6,8 @@ "jquery": true }, "parserOptions": { - "ecmaVersion": 2017 + "ecmaVersion": 2017, + "sourceType": "module" }, "extends": "eslint:recommended", "rules": { @@ -37,7 +38,7 @@ "callback-return": "off", "camelcase": "off", "class-methods-use-this": "error", - "comma-dangle": "error", + "comma-dangle": "warn", "comma-spacing": "off", "comma-style": [ "error", @@ -131,6 +132,7 @@ "no-extra-parens": "off", "no-floating-decimal": "error", "no-global-assign": "error", + "no-cond-assign": "warn", "no-implicit-globals": "error", "no-implied-eval": "error", "no-inline-comments": "off", @@ -151,7 +153,7 @@ "no-multi-spaces": "off", "no-multi-str": "error", "no-multiple-empty-lines": "error", - "no-negated-condition": "error", + "no-negated-condition": "warn", "no-nested-ternary": "off", "no-new": "error", "no-new-func": "error", @@ -252,7 +254,7 @@ "spaced-comment": "off", "strict": "error", "symbol-description": "error", - "template-curly-spacing": "error", + "template-curly-spacing": "warn", "unicode-bom": [ "error", "never" diff --git a/src/_includes/card.html b/src/_includes/card.html index f2dcbd8..793c7c2 100644 --- a/src/_includes/card.html +++ b/src/_includes/card.html @@ -1,56 +1,57 @@ - - - diff --git a/src/assets/javascript/apis.js b/src/assets/javascript/apis.js index f969df5..9dab00f 100644 --- a/src/assets/javascript/apis.js +++ b/src/assets/javascript/apis.js @@ -1,4 +1,4 @@ -'use strict'; +import * as tempura from "./tempura.js"; const dummy = { loading: { @@ -128,16 +128,15 @@ CardModel.prototype.fromAPIs = function(name, apis) { return this; }; -if (window.$) { - $(document).ready(function () { - var cardTemplateSrc = document.querySelector('script[type="text/dot-template"]').innerText; - var cardTemplate = window.doT.compile(cardTemplateSrc); +export function loadAPIs() { + var cardTemplateSrc = document.querySelector('script[type="text/tempura"]').innerText; + var cardTemplate = tempura.compile(cardTemplateSrc); var updateCards = function(data) { var fragment = $(document.createDocumentFragment()); $.each(data, function (name, apis) { var model = new CardModel().fromAPIs(name, apis); - var view = cardTemplate(model); + var view = cardTemplate({it:model}); fragment.append($(view)); }); @@ -232,5 +231,5 @@ if (window.$) { $('#search-input').focus(); }); - }); } + diff --git a/src/assets/javascript/main.js b/src/assets/javascript/main.js index a53e87a..ab5d4d7 100644 --- a/src/assets/javascript/main.js +++ b/src/assets/javascript/main.js @@ -1,5 +1,3 @@ -'use strict'; - function domReady(cb) { document.addEventListener("DOMContentLoaded", cb, false); } diff --git a/src/assets/javascript/tempura.js b/src/assets/javascript/tempura.js new file mode 100644 index 0000000..93dcd79 --- /dev/null +++ b/src/assets/javascript/tempura.js @@ -0,0 +1,138 @@ +const ESCAPE = /[&"<]/g, CHARS = { + '"': '"', + '&': '&', + '<': '<', +}; + +const ENDLINES = /[\r\n]+$/g; +const CURLY = /{{{?\s*([\s\S]*?)\s*}}}?/g; +const ARGS = /([a-zA-Z$_][^\s=]*)\s*=\s*((["`'])(?:(?=(\\?))\4.)*?\3|{[^}]*}|\[[^\]]*]|\S+)/g; + +// $$1 = escape() +// $$2 = extra blocks +// $$3 = template values +function gen(input, options) { + options = options || {}; + + let char, num, action, tmp; + let last = CURLY.lastIndex = 0; + let wip='', txt='', match, inner; + + let extra=options.blocks||{}, stack=[]; + let initials = new Set(options.props||[]); + + function close() { + if (wip.length > 0) { + txt += (txt ? 'x+=' : '=') + '`' + wip + '`;'; + } else if (txt.length === 0) { + txt = '="";' + } + wip = ''; + } + + while (match = CURLY.exec(input)) { + wip += input.substring(last, match.index).replace(ENDLINES, ''); + last = match.index + match[0].length; + + inner = match[1].trim(); + char = inner.charAt(0); + + if (char === '!') { + // comment, continue + } else if (char === '#') { + close(); + [, action, inner] = /^#\s*(\w[\w\d]+)\s*([^]*)/.exec(inner); + + if (action === 'expect') { + inner.split(/[\n\r\s\t]*,[\n\r\s\t]*/g).forEach(key => { + initials.add(key); + }); + } else if (action === 'var') { + num = inner.indexOf('='); + tmp = inner.substring(0, num++).trim(); + inner = inner.substring(num).trim().replace(/[;]$/, ''); + txt += `var ${tmp}=${inner};`; + } else if (action === 'each') { + num = inner.indexOf(' as '); + stack.push(action); + if (!~num) { + txt += `for(var i=0,$$a=${inner};i<$$a.length;i++){`; + } else { + tmp = inner.substring(0, num).trim(); + inner = inner.substring(num + 4).trim(); + let [item, idx='i'] = inner.replace(/[()\s]/g, '').split(','); // (item, idx?) + txt += `for(var ${idx}=0,${item},$$a=${tmp};${idx}<$$a.length;${idx}++){${item}=$$a[${idx}];`; + } + } else if (action === 'if') { + txt += `if(${inner}){`; + stack.push(action); + } else if (action === 'elif') { + txt += `}else if(${inner}){`; + } else if (action === 'else') { + txt += `}else{`; + } else if (action in extra) { + if (inner) { + tmp = []; + // parse arguments, `defer=true` -> `{ defer: true }` + while (match = ARGS.exec(inner)) tmp.push(match[1] + ':' + match[2]); + inner = tmp.length ? '{' + tmp.join() + '}' : ''; + } + inner = inner || '{}'; + tmp = options.async ? 'await ' : ''; + wip += '${' + tmp + '$$2.' + action + '(' + inner + ',$$2)}'; + } else { + throw new Error(`Unknown "${action}" block`); + } + } else if (char === '/') { + action = inner.substring(1); + inner = stack.pop(); + close(); + if (action === inner) txt += '}'; + else throw new Error(`Expected to close "${inner}" block; closed "${action}" instead`); + } else if (match[0].charAt(2) === '{') { + wip += '${' + inner + '}'; // {{{ raw }}} + } else { + wip += '${$$1(' + inner + ')}'; + } + } + + if (stack.length > 0) { + throw new Error(`Unterminated "${stack.pop()}" block`); + } + + if (last < input.length) { + wip += input.substring(last).replace(ENDLINES, ''); + } + + close(); + + tmp = initials.size ? `{${ [...initials].join() }}=$$3,x` : ' x'; + return `var${tmp + txt}return x`; +} + +export function esc(value) { + if (typeof value !== 'string') return value; + let last=ESCAPE.lastIndex=0, tmp=0, out=''; + while (ESCAPE.test(value)) { + tmp = ESCAPE.lastIndex - 1; + out += value.substring(last, tmp) + CHARS[value[tmp]]; + last = tmp + 1; + } + return out + value.substring(last); +} + +export function compile(input, options={}) { + return new (options.async ? (async()=>{}).constructor : Function)( + '$$1', '$$2', '$$3', gen(input, options) + ).bind(0, options.escape || esc, options.blocks); +} + +export function transform(input, options={}) { + return ( + options.format === 'cjs' + ? 'var $$1=require("tempura").esc;module.exports=' + : 'import{esc as $$1}from"tempura";export default ' + ) + ( + options.async ? 'async ' : '' + ) + 'function($$3,$$2){'+gen(input, options)+'}'; +} diff --git a/src/index.md b/src/index.md index 7aead80..c65d784 100644 --- a/src/index.md +++ b/src/index.md @@ -28,18 +28,20 @@ support: true {% include 'card.html' %} -