UNPKG

uhtml-ssr

Version:

uhtml for Service Worker, Web Worker, NodeJS, and other SSR cases

343 lines (313 loc) 10.7 kB
self.uhtml = (function (exports) { 'use strict'; class WeakMapSet extends WeakMap { set(key, value) { super.set(key, value); return value; } } /*! (c) Andrea Giammarchi - ISC */ const empty = /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i; const elements = /<([a-z]+[a-z0-9:._-]*)([^>]*?)(\/?)>/g; const attributes = /([^\s\\>"'=]+)\s*=\s*(['"]?)\x01/g; const holes = /[\x01\x02]/g; // \x01 Node.ELEMENT_NODE // \x02 Node.ATTRIBUTE_NODE /** * Given a template, find holes as both nodes and attributes and * return a string with holes as either comment nodes or named attributes. * @param {string[]} template a template literal tag array * @param {string} prefix prefix to use per each comment/attribute * @param {boolean} svg enforces self-closing tags * @returns {string} X/HTML with prefixed comments or attributes */ var instrument = (template, prefix, svg) => { let i = 0; return template .join('\x01') .trim() .replace( elements, (_, name, attrs, selfClosing) => { let ml = name + attrs.replace(attributes, '\x02=$2$1').trimEnd(); if (selfClosing.length) ml += (svg || empty.test(name)) ? ' /' : ('></' + name); return '<' + ml + '>'; } ) .replace( holes, hole => hole === '\x01' ? ('<!--' + prefix + i++ + '-->') : (prefix + i++) ); }; /** * Copyright (C) 2017-present by Andrea Giammarchi - @WebReflection * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ const {replace} = ''; const ca = /[&<>'"]/g; const esca = { '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }; const pe = m => esca[m]; /** * Safely escape HTML entities such as `&`, `<`, `>`, `"`, and `'`. * @param {string} es the input to safely escape * @returns {string} the escaped input, and it **throws** an error if * the input type is unexpected, except for boolean and numbers, * converted as string. */ const escape = es => replace.call(es, ca, pe); var uhyphen = camel => camel.replace(/(([A-Z0-9])([A-Z0-9][a-z]))|(([a-z])([A-Z]))/g, '$2$5-$3$6') .toLowerCase(); const ref = node => { let oldValue; return value => { if (oldValue !== value) { oldValue = value; if (typeof value === 'function') value(node); else value.current = node; } }; }; const {isArray} = Array; const {toString} = Function; const {keys} = Object; const passRef = ref(null); const prefix = 'isµ' + Date.now(); const rename = /([^\s>]+)[\s\S]*$/; const interpolation = new RegExp( `(<!--${prefix}(\\d+)-->|\\s*${prefix}(\\d+)=([^\\s>]))`, 'g' ); const attribute = (name, quote, value) => ` ${name}=${quote}${escape(value)}${quote}`; const getValue = value => { switch (typeof value) { case 'string': return escape(value); case 'boolean': case 'number': return String(value); case 'object': switch (true) { case isArray(value): return value.map(getValue).join(''); case value instanceof Hole: return value.toString(); } break; case 'function': return getValue(value()); } return value == null ? '' : escape(String(value)); }; // flag for foreign checks (slower path, fast by default) let useForeign = false; class Foreign { constructor(handler, value) { this._ = (...args) => handler(...args, value); } } const foreign = (handler, value) => { useForeign = true; return new Foreign(handler, value); }; class Hole extends String {} const parse = (template, expectedLength, svg) => { const html = instrument(template, prefix, svg); const updates = []; let i = 0; let match = null; while (match = interpolation.exec(html)) { const pre = html.slice(i, match.index); i = match.index + match[0].length; if (match[2]) updates.push(value => (pre + getValue(value))); else { let name = ''; let quote = match[4]; switch (quote) { case '"': case "'": const next = html.indexOf(quote, i); name = html.slice(i, next); i = next + 1; break; default: name = html.slice(--i).replace(rename, '$1'); i += name.length; quote = '"'; break; } switch (true) { case name === 'aria': updates.push(value => (pre + keys(value).map(aria, value).join(''))); break; case name === 'ref': updates.push(value => { passRef(value); return pre; }); break; // setters as boolean attributes (.disabled .contentEditable) case name[0] === '?': const boolean = name.slice(1).toLowerCase(); updates.push(value => { let result = pre; if (value) result += ` ${boolean}`; return result; }); break; case name[0] === '.': const lower = name.slice(1).toLowerCase(); updates.push(lower === 'dataset' ? (value => ( pre + keys(value) .filter(key => value[key] != null) .map(data, value) .join('') )) : (value => { let result = pre; // null, undefined, and false are not shown at all if (value != null && value !== false) { // true means boolean attribute, just show the name if (value === true) result += ` ${lower}`; // in all other cases, just escape it in quotes else result += attribute(lower, quote, value); } return result; }) ); break; case name[0] === '@': name = 'on' + name.slice(1); case name[0] === 'o' && name[1] === 'n': updates.push(value => { let result = pre; // allow handleEvent based objects that // follow the `onMethod` convention // allow listeners only if passed as string, // as functions with a special toString method, // as objects with handleEvents and a method switch (typeof value) { case 'object': if (!(name in value)) break; value = value[name]; if (typeof value !== 'function') break; case 'function': if (value.toString === toString) break; case 'string': result += attribute(name, quote, value); break; } return result; }); break; default: updates.push(value => { let result = pre; if (value != null) { if (useForeign && value instanceof Foreign) { value = value._(null, name); if (value == null) return result; } result += attribute(name, quote, value); } return result; }); break; } } } const {length} = updates; if (length !== expectedLength) throw new Error(`invalid template ${template}`); if (length) { const last = updates[length - 1]; const chunk = html.slice(i); updates[length - 1] = value => (last(value) + chunk); } else updates.push(() => html); return updates; }; // declarations function aria(key) { const value = escape(this[key]); return key === 'role' ? ` role="${value}"` : ` aria-${key.toLowerCase()}="${value}"`; } function data(key) { return ` data-${uhyphen(key)}="${escape(this[key])}"`; } const cache = new WeakMapSet; const uhtmlParity = svg => { const fn = (template, ...values) => { const {length} = values; const updates = cache.get(template) || cache.set(template, parse(template, length, svg)); return new Hole( length ? values.map(update, updates).join('') : updates[0]() ); }; // both `.node` and `.for` are for feature parity with uhtml // but don't do anything different from regular function call fn.node = fn; fn.for = () => fn; return fn; }; const html = uhtmlParity(false); const svg = uhtmlParity(true); const render = (where, what) => { const content = (typeof what === 'function' ? what() : what).toString(); return typeof where === 'function' ? where(content) : (where.write(content), where); }; function update(value, i) { return this[i](value); } exports.Hole = Hole; exports.foreign = foreign; exports.html = html; exports.render = render; exports.svg = svg; return exports; })({});