Skip to content

Commit

Permalink
handle optional closing tags for self-closing tags. fixes #41
Browse files Browse the repository at this point in the history
  • Loading branch information
mreinstein committed Feb 11, 2021
1 parent c397ca9 commit d86f4db
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 64 deletions.
188 changes: 124 additions & 64 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
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
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 = {}
module.exports = function (h, 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])
Expand All @@ -43,120 +47,151 @@ 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 = [ ]

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
} else if (c === '>' && !quot(state) && state !== COMMENT) {
Expand All @@ -167,14 +202,16 @@ module.exports = function (h, opts) {
} else if (state === ATTR_VALUE && reg.length) {
res.push([ATTR_VALUE,reg])
}
res.push([CLOSE])
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) {
Expand All @@ -185,11 +222,13 @@ 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 <br/>
// self closing tag without a space <br/>
isSelfClosing = true

} else if (state === OPEN && /\s/.test(c)) {
if (reg.length) {
if (reg.length)
res.push([OPEN, reg])
}

reg = ''
state = ATTR
} else if (state === OPEN) {
Expand All @@ -198,7 +237,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])
Expand All @@ -208,6 +248,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 === '=') {
Expand All @@ -218,7 +262,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 === "'") {
Expand Down Expand Up @@ -276,6 +324,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',
Expand All @@ -292,4 +350,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) }
*/
25 changes: 25 additions & 0 deletions test/svg.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<div>
<h3>test</h3>
<svg width="150" height="100" viewBox="0 0 3 2">
<use id="test"></use>
<rect width="1" height="2" x="0" fill="#008d46"></rect>
<use id="test"></use>
<rect width="1" height="2" x="0" fill="#008d46"></rect>
<use id="test"></use>
</svg>
</div>`
var tree = hx`<div>
<h3>test</h3>
<svg width="150" height="100" viewBox="0 0 3 2">
<use id="test"></use>
<rect width="1" height="2" x="0" fill="#008d46"></rect>
<use id="test"/>
<rect width="1" height="2" x="0" fill="#008d46"/>
<use id="test"></use>
</svg>
</div>`
t.equal(vdom.create(tree).toString(), expected)
t.end()
})

0 comments on commit d86f4db

Please sign in to comment.