Skip to content

Commit

Permalink
Merge pull request #81 from mreinstein/master
Browse files Browse the repository at this point in the history
handle optional closing tags for self-closing tags. fixes #41
  • Loading branch information
mreinstein committed Oct 30, 2023
2 parents c397ca9 + b748594 commit 16690b2
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 71 deletions.
215 changes: 151 additions & 64 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
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 = {}
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])
Expand All @@ -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 <style> tags shouldn't close
// e.g., <style> ul > .test { color: blue }</style>
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) {
Expand All @@ -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 <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])
}

if (reg === 'style')
isInStyleTag = true
else if (reg === '/style')
isInStyleTag = false

reg = ''
state = ATTR
} else if (state === OPEN) {
Expand All @@ -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])
Expand All @@ -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 === '=') {
Expand All @@ -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 === "'") {
Expand Down Expand Up @@ -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',
Expand All @@ -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) }
*/
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -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": {
Expand All @@ -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"
}

0 comments on commit 16690b2

Please sign in to comment.