diff --git a/src/xhtm.js b/src/xhtm.js new file mode 100644 index 0000000..b947aa0 --- /dev/null +++ b/src/xhtm.js @@ -0,0 +1,114 @@ +// source: https://github.com/dy/xhtm | MIT + +const FIELD = '\ue000', QUOTES = '\ue001' + +export default function htm (statics) { + let h = this, prev = 0, current = [null], field = 0, args, name, value, quotes = [], quote = 0, last, level = 0, pre = false + + const evaluate = (str, parts = [], raw) => { + let i = 0 + str = (!raw && str === QUOTES ? + quotes[quote++].slice(1,-1) : + str.replace(/\ue001/g, m => quotes[quote++])) + + if (!str) return str + str.replace(/\ue000/g, (match, idx) => { + if (idx) parts.push(str.slice(i, idx)) + i = idx + 1 + return parts.push(arguments[++field]) + }) + if (i < str.length) parts.push(str.slice(i)) + return parts.length > 1 ? parts : parts[0] + } + + // close level + const up = () => { + // console.log('-level', current); + [current, last, ...args] = current + current.push(h(last, ...args)) + if (pre === level--) pre = false // reset
+ } + + let str = statics + .join(FIELD) + .replace(//g, '') + .replace(//g, '') + .replace(/('|")[^\1]*?\1/g, match => (quotes.push(match), QUOTES)) + + // ...>text<... sequence + str.replace(/(?:^|>)((?:[^<]|<[^\w\ue000\/?!>])*)(?:$|<)/g, (match, text, idx, str) => { + let tag, close + + if (idx) { + str.slice(prev, idx) + //→ + .replace(/(\S)\/$/, '$1 /') + .split(/\s+/) + .map((part, i) => { + // , > .../> + if (part[0] === '/') { + part = part.slice(1) + // ignore duplicate empty closers + if (EMPTY[part]) return + // ignore pairing self-closing tags + close = tag || part || 1 + // skip + } + // abc def,
x + if (typeof tag === 'string') { tag = tag.toLowerCase(); while (CLOSE[current[1]+tag]) up() } + current = [current, tag, null] + level++ + if (!pre && PRE[tag]) pre = level + // console.log('+level', tag) + if (EMPTY[tag]) close = tag + } + // attr=... + else if (part) { + let props = current[2] || (current[2] = {}) + if (part.slice(0, 3) === '...') { + Object.assign(props, arguments[++field]) + } + else { + [name, value] = part.split('='); + Array.isArray(value = props[evaluate(name)] = value ? evaluate(value) : true) && + // if prop value is array - make sure it serializes as string without csv + (value.toString = value.join.bind(value, '')) + } + } + }) + } + + if (close) { + if (!current[0]) err(`Wrong close tag \`${close}\``) + up() + // if last child is optionally closable - close it too + while (last !== close && CLOSE[last]) up() + } + prev = idx + match.length + + // fix text indentation + if (!pre) text = text.replace(/\s*\n\s*/g,'').replace(/\s+/g, ' ') + + if (text) evaluate((last = 0, text), current, true) + }) + + if (current[0] && CLOSE[current[1]]) up() + + if (level) err(`Unclosed \`${current[1]}\`.`) + + return current.length < 3 ? current[1] : (current.shift(), current) +} + +const err = (msg) => { throw SyntaxError(msg) } + +// self-closing elements +const EMPTY = htm.empty = {} + +// optional closing elements +const CLOSE = htm.close = {} + +// preformatted text elements +const PRE = htm.pre = {} \ No newline at end of file