diff --git a/index.js b/index.js index de6fc07..95fc8ee 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ var attrToProp = require('hyperscript-attribute-to-property') + var VAR = 0, TEXT = 1, OPEN = 2, CLOSE = 3, ATTR = 4 var ATTR_KEY = 5, ATTR_KEY_W = 6 var ATTR_VALUE_W = 7, ATTR_VALUE = 8 @@ -7,21 +8,26 @@ var ATTR_VALUE_SQ = 9, ATTR_VALUE_DQ = 10 var ATTR_EQ = 11, ATTR_BREAK = 12 var COMMENT = 13 + module.exports = function (h, opts) { - if (!opts) opts = {} + if (!opts) opts = { } + var concat = opts.concat || function (a, b) { return String(a) + String(b) } - if (opts.attrToProp !== false) { + + if (opts.attrToProp !== false) h = attrToProp(h) - } + return function (strings) { - var state = TEXT, reg = '' + + var state = TEXT, reg = '', isSelfClosing = false var arglen = arguments.length var parts = [] for (var i = 0; i < strings.length; i++) { + if (i < arglen - 1) { var arg = arguments[i+1] var p = parse(strings[i]) @@ -43,138 +49,191 @@ module.exports = function (h, opts) { p.push([ VAR, xstate, arg ]) } parts.push.apply(parts, p) - } else parts.push.apply(parts, parse(strings[i])) + } else { + parts.push.apply(parts, parse(strings[i])) + } } - var tree = [null,{},[]] - var stack = [[tree,-1]] + var tree = [ null, {}, [] ] + var stack = [ [ tree, -1 ] ] + for (var i = 0; i < parts.length; i++) { var cur = stack[stack.length-1][0] - var p = parts[i], s = p[0] - if (s === OPEN && /^\//.test(p[1])) { + var p = parts[i], state = p[0] + + if (state === OPEN && /^\//.test(p[1])) { var ix = stack[stack.length-1][1] if (stack.length > 1) { stack.pop() - stack[stack.length-1][0][2][ix] = h( - cur[0], cur[1], cur[2].length ? cur[2] : undefined - ) + stack[stack.length-1][0][2][ix] = h(cur[0], cur[1], cur[2].length ? cur[2] : undefined) } - } else if (s === OPEN) { + + } else if (state === OPEN) { var c = [p[1],{},[]] cur[2].push(c) stack.push([c,cur[2].length-1]) - } else if (s === ATTR_KEY || (s === VAR && p[1] === ATTR_KEY)) { + + } else if (state === ATTR_KEY || (state === VAR && p[1] === ATTR_KEY)) { var key = '' var copyKey for (; i < parts.length; i++) { if (parts[i][0] === ATTR_KEY) { key = concat(key, parts[i][1]) + } else if (parts[i][0] === VAR && parts[i][1] === ATTR_KEY) { if (typeof parts[i][2] === 'object' && !key) { - for (copyKey in parts[i][2]) { - if (parts[i][2].hasOwnProperty(copyKey) && !cur[1][copyKey]) { + for (copyKey in parts[i][2]) + if (parts[i][2].hasOwnProperty(copyKey) && !cur[1][copyKey]) cur[1][copyKey] = parts[i][2][copyKey] - } - } + } else { key = concat(key, parts[i][2]) } - } else break + + } else { + break + } } - if (parts[i][0] === ATTR_EQ) i++ + + if (parts[i][0] === ATTR_EQ) + i++ + var j = i + for (; i < parts.length; i++) { if (parts[i][0] === ATTR_VALUE || parts[i][0] === ATTR_KEY) { - if (!cur[1][key]) cur[1][key] = strfn(parts[i][1]) - else parts[i][1]==="" || (cur[1][key] = concat(cur[1][key], parts[i][1])); - } else if (parts[i][0] === VAR - && (parts[i][1] === ATTR_VALUE || parts[i][1] === ATTR_KEY)) { - if (!cur[1][key]) cur[1][key] = strfn(parts[i][2]) - else parts[i][2]==="" || (cur[1][key] = concat(cur[1][key], parts[i][2])); + if (!cur[1][key]) + cur[1][key] = strfn(parts[i][1]) + else + parts[i][1]==="" || (cur[1][key] = concat(cur[1][key], parts[i][1])); + + } else if (parts[i][0] === VAR && (parts[i][1] === ATTR_VALUE || parts[i][1] === ATTR_KEY)) { + if (!cur[1][key]) + cur[1][key] = strfn(parts[i][2]) + else + parts[i][2]==="" || (cur[1][key] = concat(cur[1][key], parts[i][2])); + } else { - if (key.length && !cur[1][key] && i === j - && (parts[i][0] === CLOSE || parts[i][0] === ATTR_BREAK)) { + if (key.length && !cur[1][key] && i === j && (parts[i][0] === CLOSE || parts[i][0] === ATTR_BREAK)) { // https://html.spec.whatwg.org/multipage/infrastructure.html#boolean-attributes // empty string is falsy, not well behaved value in browser cur[1][key] = key.toLowerCase() } - if (parts[i][0] === CLOSE) { + + if (parts[i][0] === CLOSE) i-- - } + break } } - } else if (s === ATTR_KEY) { + + } else if (state === ATTR_KEY) { cur[1][p[1]] = true - } else if (s === VAR && p[1] === ATTR_KEY) { + + } else if (state === VAR && p[1] === ATTR_KEY) { cur[1][p[2]] = true - } else if (s === CLOSE) { - if (selfClosing(cur[0]) && stack.length) { + + } else if (state === CLOSE) { + + const isSelfClosing = p[1] || selfClosingVoid(cur[0]) + //if (selfClosing(cur[0]) && stack.length) { + if (isSelfClosing && stack.length) { var ix = stack[stack.length-1][1] stack.pop() - stack[stack.length-1][0][2][ix] = h( - cur[0], cur[1], cur[2].length ? cur[2] : undefined - ) + stack[stack.length-1][0][2][ix] = h(cur[0], cur[1], cur[2].length ? cur[2] : undefined) } - } else if (s === VAR && p[1] === TEXT) { - if (p[2] === undefined || p[2] === null) p[2] = '' - else if (!p[2]) p[2] = concat('', p[2]) - if (Array.isArray(p[2][0])) { + + } else if (state === VAR && p[1] === TEXT) { + if (p[2] === undefined || p[2] === null) + p[2] = '' + else if (!p[2]) + p[2] = concat('', p[2]) + + if (Array.isArray(p[2][0])) cur[2].push.apply(cur[2], p[2]) - } else { + else cur[2].push(p[2]) - } - } else if (s === TEXT) { + + } else if (state === TEXT) { cur[2].push(p[1]) - } else if (s === ATTR_EQ || s === ATTR_BREAK) { + + } else if (state === ATTR_EQ || state === ATTR_BREAK) { // no-op + } else { - throw new Error('unhandled: ' + s) + throw new Error('unhandled: ' + state) + } } - if (tree[2].length > 1 && /^\s*$/.test(tree[2][0])) { + if (tree[2].length > 1 && /^\s*$/.test(tree[2][0])) tree[2].shift() - } - if (tree[2].length > 2 - || (tree[2].length === 2 && /\S/.test(tree[2][1]))) { - if (opts.createFragment) return opts.createFragment(tree[2]) + if (tree[2].length > 2 || (tree[2].length === 2 && /\S/.test(tree[2][1]))) { + if (opts.createFragment) + return opts.createFragment(tree[2]) + throw new Error( 'multiple root elements must be wrapped in an enclosing tag' ) } - if (Array.isArray(tree[2][0]) && typeof tree[2][0][0] === 'string' - && Array.isArray(tree[2][0][2])) { + + if (Array.isArray(tree[2][0]) && typeof tree[2][0][0] === 'string' && Array.isArray(tree[2][0][2])) tree[2][0] = h(tree[2][0][0], tree[2][0][1], tree[2][0][2]) - } + return tree[2][0] function parse (str) { - var res = [] - if (state === ATTR_VALUE_W) state = ATTR + var res = [ ] + + var isInStyleTag = false + + if (state === ATTR_VALUE_W) + state = ATTR + for (var i = 0; i < str.length; i++) { var c = str.charAt(i) if (state === TEXT && c === '<') { - if (reg.length) res.push([TEXT, reg]) + if (reg.length) + res.push([TEXT, reg]) reg = '' state = OPEN + isInStyleTag = false + } else if (c === '>' && !quot(state) && state !== COMMENT) { + if (state === OPEN && reg.length) { res.push([OPEN,reg]) + + if (reg === 'style') + isInStyleTag = true + else if (reg === '/style') + isInStyleTag = false + } else if (state === ATTR_KEY) { res.push([ATTR_KEY,reg]) } else if (state === ATTR_VALUE && reg.length) { res.push([ATTR_VALUE,reg]) } - res.push([CLOSE]) - reg = '' + + if (state === TEXT && isInStyleTag) { + // the css descendant selector within + reg += c + } else { + res.push([CLOSE, isSelfClosing]) + isSelfClosing = false + reg = '' + } + state = TEXT + } else if (state === COMMENT && /-$/.test(reg) && c === '-') { if (opts.comments) { res.push([ATTR_VALUE,reg.substr(0, reg.length - 1)]) } reg = '' + isSelfClosing = true state = TEXT } else if (state === OPEN && /^!--$/.test(reg)) { if (opts.comments) { @@ -185,11 +244,18 @@ module.exports = function (h, opts) { } else if (state === TEXT || state === COMMENT) { reg += c } else if (state === OPEN && c === '/' && reg.length) { - // no-op, self closing tag without a space
+ // self closing tag without a space
+ isSelfClosing = true + } else if (state === OPEN && /\s/.test(c)) { - if (reg.length) { + if (reg.length) res.push([OPEN, reg]) - } + + if (reg === 'style') + isInStyleTag = true + else if (reg === '/style') + isInStyleTag = false + reg = '' state = ATTR } else if (state === OPEN) { @@ -198,7 +264,8 @@ module.exports = function (h, opts) { state = ATTR_KEY reg = c } else if (state === ATTR && /\s/.test(c)) { - if (reg.length) res.push([ATTR_KEY,reg]) + if (reg.length) + res.push([ATTR_KEY,reg]) res.push([ATTR_BREAK]) } else if (state === ATTR_KEY && /\s/.test(c)) { res.push([ATTR_KEY,reg]) @@ -208,6 +275,10 @@ module.exports = function (h, opts) { res.push([ATTR_KEY,reg],[ATTR_EQ]) reg = '' state = ATTR_VALUE_W + } else if (state === ATTR_KEY && c === '/') { + isSelfClosing = true + reg='' + state = ATTR } else if (state === ATTR_KEY) { reg += c } else if ((state === ATTR_KEY_W || state === ATTR) && c === '=') { @@ -218,7 +289,11 @@ module.exports = function (h, opts) { if (/[\w-]/.test(c)) { reg += c state = ATTR_KEY - } else state = ATTR + } else if (c === '/') { + isSelfClosing = true + } else { + state = ATTR + } } else if (state === ATTR_VALUE_W && c === '"') { state = ATTR_VALUE_DQ } else if (state === ATTR_VALUE_W && c === "'") { @@ -276,6 +351,16 @@ function quot (state) { return state === ATTR_VALUE_SQ || state === ATTR_VALUE_DQ } +//area, base, br, col, command, embed, hr, img, input, keygen, link, meta, param, source, track, wbr +var voidCloseRE = RegExp('^(' + [ + 'area', 'base', 'br', 'col', 'command', 'embed', + 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', + 'source', 'track', 'wbr' +].join('|') + ')(?:[\.#][a-zA-Z0-9\u007F-\uFFFF_:-]+)*$') + +function selfClosingVoid (tag) { return voidCloseRE.test(tag) } + +/* var closeRE = RegExp('^(' + [ 'area', 'base', 'basefont', 'bgsound', 'br', 'col', 'command', 'embed', 'frame', 'hr', 'img', 'input', 'isindex', 'keygen', 'link', 'meta', 'param', @@ -292,4 +377,6 @@ var closeRE = RegExp('^(' + [ 'path', 'polygon', 'polyline', 'rect', 'set', 'stop', 'tref', 'use', 'view', 'vkern' ].join('|') + ')(?:[\.#][a-zA-Z0-9\u007F-\uFFFF_:-]+)*$') + function selfClosing (tag) { return closeRE.test(tag) } +*/ diff --git a/package.json b/package.json index de50b87..509caeb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "hyperx", - "version": "2.5.4", + "name": "hyperx-tmp", + "version": "2.5.6", "description": "tagged template string virtual dom builder", "main": "index.js", "scripts": { @@ -21,8 +21,8 @@ "license": "BSD", "devDependencies": { "covert": "^1.1.0", - "hyperscript": "^1.4.7", - "tape": "^4.4.0", + "hyperscript": "^2.0.2", + "tape": "^5.4.0", "virtual-dom": "^2.1.1" }, "dependencies": { @@ -34,10 +34,10 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/substack/hyperx.git" + "url": "git+https://github.com/mreinstein/hyperx.git" }, "bugs": { - "url": "https://github.com/substack/hyperx/issues" + "url": "https://github.com/mreinstein/hyperx/issues" }, - "homepage": "https://github.com/substack/hyperx#readme" + "homepage": "https://github.com/mreinstein/hyperx#readme" } diff --git a/test/style.js b/test/style.js index 7eef5b5..14cd390 100644 --- a/test/style.js +++ b/test/style.js @@ -15,3 +15,42 @@ test('style', function (t) { ) t.end() }) + + +test('embedded style', function (t) { + var key = 'type' + var value = 'text' + var tree = hx`` + t.equal( + vdom.create(tree).toString(), + `` + ) + t.end() +}) + +test('embedded style with attributes', function (t) { + var key = 'type' + var value = 'text' + var tree = hx`` + t.equal( + vdom.create(tree).toString(), + `` + ) + t.end() +}) diff --git a/test/svg.js b/test/svg.js index 1ee0fa1..e7f515b 100644 --- a/test/svg.js +++ b/test/svg.js @@ -19,3 +19,28 @@ test('svg mixed with html', function (t) { t.equal(vdom.create(tree).toString(), expected) t.end() }) + +test('svg mixed with html and close / self-closing tags', function (t) { + var expected = `
+

test

+ + + + + + + +
` + var tree = hx`
+

test

+ + + + + + + +
` + t.equal(vdom.create(tree).toString(), expected) + t.end() +})