Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handle optional closing tags for self-closing tags. fixes #41 #81

Merged
merged 11 commits into from
Oct 30, 2023
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"
}
Loading