Skip to content

Commit 7528714

Browse files
committed
add proper support for void elements even if not closed.
add support for self-closing void custom elements/components. add a rudimentary speed test just for getting rough impression of impact of changes while developing. add support for multiple root elements
1 parent 8e6ba4f commit 7528714

File tree

7 files changed

+209
-56
lines changed

7 files changed

+209
-56
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ console.log(ast);
7373
7474
// whether this is a self-closing tag
7575
// such as <img/>
76-
selfClosing: false,
76+
voidElement: false,
7777
7878
// an array of child nodes
7979
// we see the same structure
@@ -83,7 +83,7 @@ console.log(ast);
8383
type: 'tag',
8484
name: 'p',
8585
attrs: {},
86-
selfClosing: false,
86+
voidElement: false,
8787
children: [
8888
// this is a text node
8989
// it also has a `type`
@@ -110,7 +110,7 @@ properties:
110110
- `type` - will always be `tag` for this type of node
111111
- `name` - tag name, such as 'div'
112112
- `attrs` - an object of key/value pairs. If an attribute has multiple space-separated items such as classes, they'll still be in a single string, for example: `class: "class1 class2"`
113-
- `selfClosing` - `true` or `false`. Whether this tag has a self-closing slash such as: `<img/>`, or `<input/>`
113+
- `voidElement` - `true` or `false`. Whether this tag is a known void element as defined by [spec](http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements).
114114
- `children` - array of child nodes. Note that any continuous string of text is a text node child, see below.
115115

116116
### 2. text
@@ -131,7 +131,7 @@ properties:
131131
- `type` - will always be `component` for this type of node
132132
- `name` - tag name, such as 'div'
133133
- `attrs` - an object of key/value pairs. If an attribute has multiple space-separated items such as classes, they'll still be in a single string, for example: `class: "class1 class2"`
134-
- `selfClosing` - `true` or `false`. Whether this tag has a self-closing slash such as: `<img/>`, or `<input/>`
134+
- `voidElement` - `true` or `false`. Whether this tag is a known void element as defined by [spec](http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements).
135135
- `children` - it will still have a `children` array, but it will always be empty.
136136

137137

lib/parse-tag.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,45 @@
11
var attrRE = /([\w-]+)|['"]{1}([^'"]*)['"]{1}/g;
22

3+
// create optimized lookup object for
4+
// void elements as listed here:
5+
// http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements
6+
var lookup = (Object.create) ? Object.create(null) : {};
7+
lookup.area = true;
8+
lookup.base = true;
9+
lookup.br = true;
10+
lookup.col = true;
11+
lookup.embed = true;
12+
lookup.hr = true;
13+
lookup.img = true;
14+
lookup.input = true;
15+
lookup.keygen = true;
16+
lookup.link = true;
17+
lookup.menuitem = true;
18+
lookup.meta = true;
19+
lookup.param = true;
20+
lookup.source = true;
21+
lookup.track = true;
22+
lookup.wbr = true;
323

424
module.exports = function (tag) {
525
var i = 0;
626
var key;
727
var res = {
8-
selfClosing: tag.slice(-2, -1) === '/',
9-
attrs: {},
1028
type: 'tag',
11-
children: [],
12-
name: ''
29+
name: '',
30+
voidElement: false,
31+
attrs: {},
32+
children: []
1333
};
1434

1535
tag.replace(attrRE, function (match) {
1636
if (i % 2) {
1737
key = match;
1838
} else {
1939
if (i === 0) {
40+
if (lookup[match] || tag.charAt(tag.length - 2) === '/') {
41+
res.voidElement = true;
42+
}
2043
res.name = match;
2144
} else {
2245
res.attrs[key] = match.replace(/['"]/g, '');

lib/parse.js

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
var tagRE = /<(?:"[^"]*"['"]*|'[^']*'['"]*|[^'">])+>/g;
22
var parseTag = require('./parse-tag');
3-
3+
// re-used obj for quick lookups of components
4+
var empty = Object.create ? Object.create(null) : {};
45

56
module.exports = function parse(html, options) {
6-
options = options || {};
7-
options.components = options.components || {};
8-
var result;
7+
options || (options = {});
8+
options.components || (options.components = empty);
9+
var result = [];
910
var current;
10-
var previous;
1111
var level = -1;
1212
var arr = [];
1313
var byTag = {};
@@ -26,8 +26,6 @@ module.exports = function parse(html, options) {
2626
var nextChar = html.charAt(start);
2727
var parent;
2828

29-
previous = current;
30-
3129
if (isOpen) {
3230
level++;
3331

@@ -37,7 +35,7 @@ module.exports = function parse(html, options) {
3735
inComponent = true;
3836
}
3937

40-
if (!inComponent && nextChar !== '<') {
38+
if (!inComponent && nextChar && nextChar !== '<') {
4139
current.children.push({
4240
type: 'text',
4341
content: html.slice(start, html.indexOf('<', start))
@@ -46,10 +44,8 @@ module.exports = function parse(html, options) {
4644

4745
byTag[current.tagName] = current;
4846

49-
// this is our base if we don't already have one
50-
if (!previous) {
51-
result = current;
52-
}
47+
// if we're at root, push new base node
48+
if (level === 0) result.push(current);
5349

5450
parent = arr[level - 1];
5551

@@ -60,7 +56,7 @@ module.exports = function parse(html, options) {
6056
arr[level] = current;
6157
}
6258

63-
if (!isOpen || current.selfClosing) {
59+
if (!isOpen || current.voidElement) {
6460
level--;
6561
if (!inComponent && nextChar !== '<' && nextChar) {
6662
// trailing text node

lib/stringify.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@ function stringify(buff, doc) {
1414
case 'text':
1515
return buff + doc.content;
1616
case 'tag':
17-
buff += '<' + doc.name + (doc.attrs ? attrString(doc.attrs) : '') + (doc.selfClosing ? '/>' : '>');
18-
if (doc.selfClosing) {
17+
buff += '<' + doc.name + (doc.attrs ? attrString(doc.attrs) : '') + (doc.voidElement ? '/>' : '>');
18+
if (doc.voidElement) {
1919
return buff;
2020
}
2121
return buff + doc.children.reduce(stringify, '') + '</' + doc.name + '>';
2222
}
2323
}
2424

2525
module.exports = function (doc) {
26-
return stringify('', doc);
26+
return doc.reduce(function (token, rootEl) {
27+
return token + stringify('', rootEl);
28+
}, '');
2729
};

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,8 @@
2626
},
2727
"scripts": {
2828
"test": "node test/index.js | tap-spec"
29+
},
30+
"dependencies": {
31+
"void-elements": "^1.0.0"
2932
}
3033
}

test/parse-tag.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ test('parseTag', function (t) {
1414
quote: 'me '
1515
},
1616
name: 'div',
17-
selfClosing: false,
17+
voidElement: false,
1818
children: []
1919
});
2020

@@ -26,7 +26,7 @@ test('parseTag', function (t) {
2626
class: 'single quoted thing'
2727
},
2828
name: 'something-custom',
29-
selfClosing: false,
29+
voidElement: false,
3030
children: []
3131
});
3232

@@ -36,7 +36,7 @@ test('parseTag', function (t) {
3636
type: 'tag',
3737
attrs: {},
3838
name: 'p',
39-
selfClosing: false,
39+
voidElement: false,
4040
children: []
4141
});
4242

@@ -49,7 +49,7 @@ test('parseTag', function (t) {
4949
alt: 'sweet picture'
5050
},
5151
name: 'img',
52-
selfClosing: true,
52+
voidElement: true,
5353
children: []
5454
});
5555

0 commit comments

Comments
 (0)