Skip to content

Commit 665a075

Browse files
committed
Add support for SVG
Related to wooorm/property-information#6. Related to GH-5.
1 parent baac1a8 commit 665a075

File tree

8 files changed

+521
-144
lines changed

8 files changed

+521
-144
lines changed

lib/constants.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict'
2+
3+
// Characters.
4+
var NULL = '\0'
5+
var AMP = '&'
6+
var SP = ' '
7+
var TB = '\t'
8+
var GR = '`'
9+
var DQ = '"'
10+
var SQ = "'"
11+
var EQ = '='
12+
var LT = '<'
13+
var GT = '>'
14+
var SO = '/'
15+
var LF = '\n'
16+
var CR = '\r'
17+
var FF = '\f'
18+
19+
var whitespace = [SP, TB, LF, CR, FF]
20+
// https://html.spec.whatwg.org/#attribute-name-state
21+
var name = whitespace.concat(AMP, SO, GT, EQ)
22+
// https://html.spec.whatwg.org/#attribute-value-(unquoted)-state
23+
var unquoted = whitespace.concat(AMP, GT)
24+
var unquotedSafe = unquoted.concat(NULL, DQ, SQ, LT, EQ, GR)
25+
// https://html.spec.whatwg.org/#attribute-value-(single-quoted)-state
26+
var singleQuoted = [AMP, SQ]
27+
// https://html.spec.whatwg.org/#attribute-value-(double-quoted)-state
28+
var doubleQuoted = [AMP, DQ]
29+
30+
// Maps of subsets. Each value is a matrix of tuples.
31+
// The first value causes parse errors, the second is valid.
32+
// Of both values, the first value is unsafe, and the second is safe.
33+
module.exports = {
34+
name: [
35+
[name, name.concat(DQ, SQ, GR)],
36+
[name.concat(NULL, DQ, SQ, LT), name.concat(NULL, DQ, SQ, LT, GR)]
37+
],
38+
unquoted: [[unquoted, unquotedSafe], [unquotedSafe, unquotedSafe]],
39+
single: [
40+
[singleQuoted, singleQuoted.concat(DQ, GR)],
41+
[singleQuoted.concat(NULL), singleQuoted.concat(NULL, DQ, GR)]
42+
],
43+
double: [
44+
[doubleQuoted, doubleQuoted.concat(SQ, GR)],
45+
[doubleQuoted.concat(NULL), doubleQuoted.concat(NULL, SQ, GR)]
46+
]
47+
}

lib/element.js

Lines changed: 70 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
'use strict'
22

33
var xtend = require('xtend')
4+
var svg = require('property-information/svg')
5+
var find = require('property-information/find')
46
var spaces = require('space-separated-tokens').stringify
57
var commas = require('comma-separated-tokens').stringify
6-
var information = require('property-information')
78
var entities = require('stringify-entities')
8-
var kebab = require('kebab-case')
99
var ccount = require('ccount')
1010
var all = require('./all')
11+
var constants = require('./constants')
1112

1213
module.exports = element
1314

1415
/* Constants. */
15-
var DATA = 'data'
1616
var EMPTY = ''
1717

1818
/* Characters. */
@@ -26,12 +26,37 @@ var SO = '/'
2626

2727
/* Stringify an element `node`. */
2828
function element(ctx, node, index, parent) {
29+
var parentSchema = ctx.schema
2930
var name = node.tagName
30-
var content = all(ctx, name === 'template' ? node.content : node)
31-
var selfClosing = ctx.voids.indexOf(name.toLowerCase()) !== -1
32-
var attrs = attributes(ctx, node.properties)
33-
var omit = ctx.omit
3431
var value = ''
32+
var selfClosing
33+
var close
34+
var omit
35+
var root = node
36+
var content
37+
var attrs
38+
39+
if (parentSchema.space === 'html' && name === 'svg') {
40+
ctx.schema = svg
41+
}
42+
43+
attrs = attributes(ctx, node.properties)
44+
45+
if (ctx.schema.space === 'svg') {
46+
omit = false
47+
close = true
48+
selfClosing = ctx.closeEmpty
49+
} else {
50+
omit = ctx.omit
51+
close = ctx.close
52+
selfClosing = ctx.voids.indexOf(name.toLowerCase()) !== -1
53+
54+
if (name === 'template') {
55+
root = node.content
56+
}
57+
}
58+
59+
content = all(ctx, root)
3560

3661
/* If the node is categorised as void, but it has
3762
* children, remove the categorisation. This
@@ -43,7 +68,7 @@ function element(ctx, node, index, parent) {
4368
if (attrs || !omit || !omit.opening(node, index, parent)) {
4469
value = LT + name + (attrs ? SPACE + attrs : EMPTY)
4570

46-
if (selfClosing && ctx.close) {
71+
if (selfClosing && close) {
4772
if (!ctx.tightClose || attrs.charAt(attrs.length - 1) === SO) {
4873
value += SPACE
4974
}
@@ -60,6 +85,8 @@ function element(ctx, node, index, parent) {
6085
value += LT + SO + name + GT
6186
}
6287

88+
ctx.schema = parentSchema
89+
6390
return value
6491
}
6592

@@ -92,7 +119,11 @@ function attributes(ctx, props) {
92119

93120
while (++index < length) {
94121
result = values[index]
95-
last = ctx.tight && result.charAt(result.length - 1)
122+
last = null
123+
124+
if (ctx.schema.space === 'html' && ctx.tight) {
125+
last = result.charAt(result.length - 1)
126+
}
96127

97128
/* In tight mode, don’t add a space after quoted attributes. */
98129
if (index !== length - 1 && last !== DQ && last !== SQ) {
@@ -105,49 +136,50 @@ function attributes(ctx, props) {
105136

106137
/* Stringify one attribute. */
107138
function attribute(ctx, key, value) {
108-
var info = information(key) || {}
139+
var schema = ctx.schema
140+
var space = schema.space
141+
var info = find(schema, key)
109142
var name
110143

111144
if (
112145
value == null ||
146+
value === false ||
113147
(typeof value === 'number' && isNaN(value)) ||
114-
(!value && info.boolean) ||
115-
(value === false && info.overloadedBoolean)
148+
(!value && info.boolean)
116149
) {
117150
return EMPTY
118151
}
119152

120-
name = attributeName(ctx, key)
153+
name = attributeName(ctx, info.attribute)
121154

122-
if ((value && info.boolean) || (value === true && info.overloadedBoolean)) {
155+
if (value === true || (value && info.boolean)) {
156+
value = name
157+
}
158+
159+
if (space === 'html' && value === name) {
123160
return name
124161
}
125162

126-
return name + attributeValue(ctx, key, value)
163+
return name + attributeValue(ctx, key, value, info)
127164
}
128165

129166
/* Stringify the attribute name. */
130-
function attributeName(ctx, key) {
131-
var info = information(key) || {}
132-
var name = info.name || kebab(key)
167+
function attributeName(ctx, name) {
168+
// Always encode without parse errors in non-HTML.
169+
var valid = ctx.schema.space === 'html' ? ctx.valid : 1
170+
var subset = constants.name[valid][ctx.safe]
133171

134-
if (
135-
name.slice(0, DATA.length) === DATA &&
136-
/\d/.test(name.charAt(DATA.length))
137-
) {
138-
name = DATA + '-' + name.slice(4)
139-
}
140-
141-
return entities(name, xtend(ctx.entities, {subset: ctx.NAME}))
172+
return entities(name, xtend(ctx.entities, {subset: subset}))
142173
}
143174

144175
/* Stringify the attribute value. */
145-
function attributeValue(ctx, key, value) {
146-
var info = information(key) || {}
176+
function attributeValue(ctx, key, value, info) {
147177
var options = ctx.entities
148178
var quote = ctx.quote
149179
var alternative = ctx.alternative
180+
var space = ctx.schema.space
150181
var unquoted
182+
var subset
151183

152184
if (typeof value === 'object' && 'length' in value) {
153185
/* `spaces` doesn’t accept a second argument, but it’s
@@ -159,31 +191,30 @@ function attributeValue(ctx, key, value) {
159191

160192
value = String(value)
161193

162-
if (value || !ctx.collapseEmpty) {
194+
if (space !== 'html' || value || !ctx.collapseEmpty) {
163195
unquoted = value
164196

165197
/* Check unquoted value. */
166-
if (ctx.unquoted) {
198+
if (space === 'html' && ctx.unquoted) {
199+
subset = constants.unquoted[ctx.valid][ctx.safe]
167200
unquoted = entities(
168201
value,
169-
xtend(options, {subset: ctx.UNQUOTED, attribute: true})
202+
xtend(options, {subset: subset, attribute: true})
170203
)
171204
}
172205

173206
/* If `value` contains entities when unquoted... */
174-
if (!ctx.unquoted || unquoted !== value) {
207+
if (space !== 'html' || !ctx.unquoted || unquoted !== value) {
175208
/* If the alternative is less common than `quote`, switch. */
176209
if (alternative && ccount(value, quote) > ccount(value, alternative)) {
177210
quote = alternative
178211
}
179212

180-
value = entities(
181-
value,
182-
xtend(options, {
183-
subset: quote === SQ ? ctx.SINGLE_QUOTED : ctx.DOUBLE_QUOTED,
184-
attribute: true
185-
})
186-
)
213+
subset = quote === SQ ? constants.single : constants.double
214+
// Always encode without parse errors in non-HTML.
215+
subset = subset[space === 'html' ? ctx.valid : 1][ctx.safe]
216+
217+
value = entities(value, xtend(options, {subset: subset, attribute: true}))
187218

188219
value = quote + value + quote
189220
}

lib/index.js

Lines changed: 25 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,50 @@
11
'use strict'
22

3+
var html = require('property-information/html')
4+
var svg = require('property-information/svg')
35
var voids = require('html-void-elements')
46
var omission = require('./omission')
57
var one = require('./one')
68

79
module.exports = toHTML
810

911
/* Characters. */
10-
var NULL = '\0'
11-
var AMP = '&'
12-
var SPACE = ' '
13-
var TAB = '\t'
14-
var GR = '`'
1512
var DQ = '"'
1613
var SQ = "'"
17-
var EQ = '='
18-
var LT = '<'
19-
var GT = '>'
20-
var SO = '/'
21-
var LF = '\n'
22-
var CR = '\r'
23-
var FF = '\f'
24-
25-
/* https://html.spec.whatwg.org/#attribute-name-state */
26-
var NAME = [AMP, SPACE, TAB, LF, CR, FF, SO, GT, EQ]
27-
var CLEAN_NAME = NAME.concat(NULL, DQ, SQ, LT)
28-
29-
/* In safe mode, all attribute values contain DQ (`"`),
30-
* SQ (`'`), and GR (`` ` ``), as those can create XSS
31-
* issues in older browsers:
32-
* - https://html5sec.org/#59
33-
* - https://html5sec.org/#102
34-
* - https://html5sec.org/#108 */
35-
var QUOTES = [DQ, SQ, GR]
36-
37-
/* https://html.spec.whatwg.org/#attribute-value-(unquoted)-state */
38-
var UQ_VALUE = [AMP, SPACE, TAB, LF, CR, FF, GT]
39-
var UQ_VALUE_CLEAN = UQ_VALUE.concat(NULL, DQ, SQ, LT, EQ, GR)
40-
41-
/* https://html.spec.whatwg.org/#attribute-value-(single-quoted)-state */
42-
var SQ_VALUE = [AMP, SQ]
43-
var SQ_VALUE_CLEAN = SQ_VALUE.concat(NULL)
44-
45-
/* https://html.spec.whatwg.org/#attribute-value-(double-quoted)-state */
46-
var DQ_VALUE = [AMP, DQ]
47-
var DQ_VALUE_CLEAN = DQ_VALUE.concat(NULL)
4814

4915
/* Stringify the given HAST node. */
5016
function toHTML(node, options) {
5117
var settings = options || {}
5218
var quote = settings.quote || DQ
53-
var smart = settings.quoteSmart
54-
var errors = settings.allowParseErrors
55-
var characters = settings.allowDangerousCharacters
5619
var alternative = quote === DQ ? SQ : DQ
57-
var name = errors ? NAME : CLEAN_NAME
58-
var unquoted = errors ? UQ_VALUE : UQ_VALUE_CLEAN
59-
var singleQuoted = errors ? SQ_VALUE : SQ_VALUE_CLEAN
60-
var doubleQuoted = errors ? DQ_VALUE : DQ_VALUE_CLEAN
61-
var config
20+
var smart = settings.quoteSmart
6221

6322
if (quote !== DQ && quote !== SQ) {
6423
throw new Error(
6524
'Invalid quote `' + quote + '`, expected `' + SQ + '` or `' + DQ + '`'
6625
)
6726
}
6827

69-
config = {
70-
NAME: name.concat(characters ? [] : QUOTES),
71-
UNQUOTED: unquoted.concat(characters ? [] : QUOTES),
72-
DOUBLE_QUOTED: doubleQuoted.concat(characters ? [] : QUOTES),
73-
SINGLE_QUOTED: singleQuoted.concat(characters ? [] : QUOTES),
74-
omit: settings.omitOptionalTags && omission,
75-
quote: quote,
76-
alternative: smart ? alternative : null,
77-
unquoted: Boolean(settings.preferUnquoted),
78-
tight: settings.tightAttributes,
79-
tightDoctype: Boolean(settings.tightDoctype),
80-
tightLists: settings.tightCommaSeparatedLists,
81-
tightClose: settings.tightSelfClosing,
82-
collapseEmpty: settings.collapseEmptyAttributes,
83-
dangerous: settings.allowDangerousHTML,
84-
voids: settings.voids || voids.concat(),
85-
entities: settings.entities || {},
86-
close: settings.closeSelfClosing
87-
}
88-
89-
return one(config, node)
28+
return one(
29+
{
30+
valid: settings.allowParseErrors ? 0 : 1,
31+
safe: settings.allowDangerousCharacters ? 0 : 1,
32+
schema: settings.space === 'svg' ? svg : html,
33+
omit: settings.omitOptionalTags && omission,
34+
quote: quote,
35+
alternative: smart ? alternative : null,
36+
unquoted: Boolean(settings.preferUnquoted),
37+
tight: settings.tightAttributes,
38+
tightDoctype: Boolean(settings.tightDoctype),
39+
tightLists: settings.tightCommaSeparatedLists,
40+
tightClose: settings.tightSelfClosing,
41+
collapseEmpty: settings.collapseEmptyAttributes,
42+
dangerous: settings.allowDangerousHTML,
43+
voids: settings.voids || voids.concat(),
44+
entities: settings.entities || {},
45+
close: settings.closeSelfClosing,
46+
closeEmpty: settings.closeEmptyElements
47+
},
48+
node
49+
)
9050
}

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@
2424
"hast-util-is-element": "^1.0.0",
2525
"hast-util-whitespace": "^1.0.0",
2626
"html-void-elements": "^1.0.0",
27-
"kebab-case": "^1.0.0",
28-
"property-information": "^3.1.0",
27+
"property-information": "^4.0.0",
2928
"space-separated-tokens": "^1.0.0",
3029
"stringify-entities": "^1.0.1",
3130
"unist-util-is": "^2.0.0",
@@ -35,7 +34,7 @@
3534
"browserify": "^16.0.0",
3635
"bundle-collapser": "^1.2.1",
3736
"esmangle": "^1.0.1",
38-
"hastscript": "^3.0.0",
37+
"hastscript": "^4.0.0",
3938
"nyc": "^12.0.0",
4039
"prettier": "^1.13.5",
4140
"remark-cli": "^5.0.0",

0 commit comments

Comments
 (0)