Skip to content

Commit

Permalink
Add support for SVG
Browse files Browse the repository at this point in the history
Related to wooorm/property-information#6.
Related to GH-5.
  • Loading branch information
wooorm committed Jul 17, 2018
1 parent baac1a8 commit 665a075
Show file tree
Hide file tree
Showing 8 changed files with 521 additions and 144 deletions.
47 changes: 47 additions & 0 deletions lib/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict'

// Characters.
var NULL = '\0'
var AMP = '&'
var SP = ' '
var TB = '\t'
var GR = '`'
var DQ = '"'
var SQ = "'"
var EQ = '='
var LT = '<'
var GT = '>'
var SO = '/'
var LF = '\n'
var CR = '\r'
var FF = '\f'

var whitespace = [SP, TB, LF, CR, FF]
// https://html.spec.whatwg.org/#attribute-name-state
var name = whitespace.concat(AMP, SO, GT, EQ)
// https://html.spec.whatwg.org/#attribute-value-(unquoted)-state
var unquoted = whitespace.concat(AMP, GT)
var unquotedSafe = unquoted.concat(NULL, DQ, SQ, LT, EQ, GR)
// https://html.spec.whatwg.org/#attribute-value-(single-quoted)-state
var singleQuoted = [AMP, SQ]
// https://html.spec.whatwg.org/#attribute-value-(double-quoted)-state
var doubleQuoted = [AMP, DQ]

// Maps of subsets. Each value is a matrix of tuples.
// The first value causes parse errors, the second is valid.
// Of both values, the first value is unsafe, and the second is safe.
module.exports = {
name: [
[name, name.concat(DQ, SQ, GR)],
[name.concat(NULL, DQ, SQ, LT), name.concat(NULL, DQ, SQ, LT, GR)]
],
unquoted: [[unquoted, unquotedSafe], [unquotedSafe, unquotedSafe]],
single: [
[singleQuoted, singleQuoted.concat(DQ, GR)],
[singleQuoted.concat(NULL), singleQuoted.concat(NULL, DQ, GR)]
],
double: [
[doubleQuoted, doubleQuoted.concat(SQ, GR)],
[doubleQuoted.concat(NULL), doubleQuoted.concat(NULL, SQ, GR)]
]
}
109 changes: 70 additions & 39 deletions lib/element.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
'use strict'

var xtend = require('xtend')
var svg = require('property-information/svg')
var find = require('property-information/find')
var spaces = require('space-separated-tokens').stringify
var commas = require('comma-separated-tokens').stringify
var information = require('property-information')
var entities = require('stringify-entities')
var kebab = require('kebab-case')
var ccount = require('ccount')
var all = require('./all')
var constants = require('./constants')

module.exports = element

/* Constants. */
var DATA = 'data'
var EMPTY = ''

/* Characters. */
Expand All @@ -26,12 +26,37 @@ var SO = '/'

/* Stringify an element `node`. */
function element(ctx, node, index, parent) {
var parentSchema = ctx.schema
var name = node.tagName
var content = all(ctx, name === 'template' ? node.content : node)
var selfClosing = ctx.voids.indexOf(name.toLowerCase()) !== -1
var attrs = attributes(ctx, node.properties)
var omit = ctx.omit
var value = ''
var selfClosing
var close
var omit
var root = node
var content
var attrs

if (parentSchema.space === 'html' && name === 'svg') {
ctx.schema = svg
}

attrs = attributes(ctx, node.properties)

if (ctx.schema.space === 'svg') {
omit = false
close = true
selfClosing = ctx.closeEmpty
} else {
omit = ctx.omit
close = ctx.close
selfClosing = ctx.voids.indexOf(name.toLowerCase()) !== -1

if (name === 'template') {
root = node.content
}
}

content = all(ctx, root)

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

if (selfClosing && ctx.close) {
if (selfClosing && close) {
if (!ctx.tightClose || attrs.charAt(attrs.length - 1) === SO) {
value += SPACE
}
Expand All @@ -60,6 +85,8 @@ function element(ctx, node, index, parent) {
value += LT + SO + name + GT
}

ctx.schema = parentSchema

return value
}

Expand Down Expand Up @@ -92,7 +119,11 @@ function attributes(ctx, props) {

while (++index < length) {
result = values[index]
last = ctx.tight && result.charAt(result.length - 1)
last = null

if (ctx.schema.space === 'html' && ctx.tight) {
last = result.charAt(result.length - 1)
}

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

/* Stringify one attribute. */
function attribute(ctx, key, value) {
var info = information(key) || {}
var schema = ctx.schema
var space = schema.space
var info = find(schema, key)
var name

if (
value == null ||
value === false ||
(typeof value === 'number' && isNaN(value)) ||
(!value && info.boolean) ||
(value === false && info.overloadedBoolean)
(!value && info.boolean)
) {
return EMPTY
}

name = attributeName(ctx, key)
name = attributeName(ctx, info.attribute)

if ((value && info.boolean) || (value === true && info.overloadedBoolean)) {
if (value === true || (value && info.boolean)) {
value = name
}

if (space === 'html' && value === name) {
return name
}

return name + attributeValue(ctx, key, value)
return name + attributeValue(ctx, key, value, info)
}

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

if (
name.slice(0, DATA.length) === DATA &&
/\d/.test(name.charAt(DATA.length))
) {
name = DATA + '-' + name.slice(4)
}

return entities(name, xtend(ctx.entities, {subset: ctx.NAME}))
return entities(name, xtend(ctx.entities, {subset: subset}))
}

/* Stringify the attribute value. */
function attributeValue(ctx, key, value) {
var info = information(key) || {}
function attributeValue(ctx, key, value, info) {
var options = ctx.entities
var quote = ctx.quote
var alternative = ctx.alternative
var space = ctx.schema.space
var unquoted
var subset

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

value = String(value)

if (value || !ctx.collapseEmpty) {
if (space !== 'html' || value || !ctx.collapseEmpty) {
unquoted = value

/* Check unquoted value. */
if (ctx.unquoted) {
if (space === 'html' && ctx.unquoted) {
subset = constants.unquoted[ctx.valid][ctx.safe]
unquoted = entities(
value,
xtend(options, {subset: ctx.UNQUOTED, attribute: true})
xtend(options, {subset: subset, attribute: true})
)
}

/* If `value` contains entities when unquoted... */
if (!ctx.unquoted || unquoted !== value) {
if (space !== 'html' || !ctx.unquoted || unquoted !== value) {
/* If the alternative is less common than `quote`, switch. */
if (alternative && ccount(value, quote) > ccount(value, alternative)) {
quote = alternative
}

value = entities(
value,
xtend(options, {
subset: quote === SQ ? ctx.SINGLE_QUOTED : ctx.DOUBLE_QUOTED,
attribute: true
})
)
subset = quote === SQ ? constants.single : constants.double
// Always encode without parse errors in non-HTML.
subset = subset[space === 'html' ? ctx.valid : 1][ctx.safe]

value = entities(value, xtend(options, {subset: subset, attribute: true}))

value = quote + value + quote
}
Expand Down
90 changes: 25 additions & 65 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,90 +1,50 @@
'use strict'

var html = require('property-information/html')
var svg = require('property-information/svg')
var voids = require('html-void-elements')
var omission = require('./omission')
var one = require('./one')

module.exports = toHTML

/* Characters. */
var NULL = '\0'
var AMP = '&'
var SPACE = ' '
var TAB = '\t'
var GR = '`'
var DQ = '"'
var SQ = "'"
var EQ = '='
var LT = '<'
var GT = '>'
var SO = '/'
var LF = '\n'
var CR = '\r'
var FF = '\f'

/* https://html.spec.whatwg.org/#attribute-name-state */
var NAME = [AMP, SPACE, TAB, LF, CR, FF, SO, GT, EQ]
var CLEAN_NAME = NAME.concat(NULL, DQ, SQ, LT)

/* In safe mode, all attribute values contain DQ (`"`),
* SQ (`'`), and GR (`` ` ``), as those can create XSS
* issues in older browsers:
* - https://html5sec.org/#59
* - https://html5sec.org/#102
* - https://html5sec.org/#108 */
var QUOTES = [DQ, SQ, GR]

/* https://html.spec.whatwg.org/#attribute-value-(unquoted)-state */
var UQ_VALUE = [AMP, SPACE, TAB, LF, CR, FF, GT]
var UQ_VALUE_CLEAN = UQ_VALUE.concat(NULL, DQ, SQ, LT, EQ, GR)

/* https://html.spec.whatwg.org/#attribute-value-(single-quoted)-state */
var SQ_VALUE = [AMP, SQ]
var SQ_VALUE_CLEAN = SQ_VALUE.concat(NULL)

/* https://html.spec.whatwg.org/#attribute-value-(double-quoted)-state */
var DQ_VALUE = [AMP, DQ]
var DQ_VALUE_CLEAN = DQ_VALUE.concat(NULL)

/* Stringify the given HAST node. */
function toHTML(node, options) {
var settings = options || {}
var quote = settings.quote || DQ
var smart = settings.quoteSmart
var errors = settings.allowParseErrors
var characters = settings.allowDangerousCharacters
var alternative = quote === DQ ? SQ : DQ
var name = errors ? NAME : CLEAN_NAME
var unquoted = errors ? UQ_VALUE : UQ_VALUE_CLEAN
var singleQuoted = errors ? SQ_VALUE : SQ_VALUE_CLEAN
var doubleQuoted = errors ? DQ_VALUE : DQ_VALUE_CLEAN
var config
var smart = settings.quoteSmart

if (quote !== DQ && quote !== SQ) {
throw new Error(
'Invalid quote `' + quote + '`, expected `' + SQ + '` or `' + DQ + '`'
)
}

config = {
NAME: name.concat(characters ? [] : QUOTES),
UNQUOTED: unquoted.concat(characters ? [] : QUOTES),
DOUBLE_QUOTED: doubleQuoted.concat(characters ? [] : QUOTES),
SINGLE_QUOTED: singleQuoted.concat(characters ? [] : QUOTES),
omit: settings.omitOptionalTags && omission,
quote: quote,
alternative: smart ? alternative : null,
unquoted: Boolean(settings.preferUnquoted),
tight: settings.tightAttributes,
tightDoctype: Boolean(settings.tightDoctype),
tightLists: settings.tightCommaSeparatedLists,
tightClose: settings.tightSelfClosing,
collapseEmpty: settings.collapseEmptyAttributes,
dangerous: settings.allowDangerousHTML,
voids: settings.voids || voids.concat(),
entities: settings.entities || {},
close: settings.closeSelfClosing
}

return one(config, node)
return one(
{
valid: settings.allowParseErrors ? 0 : 1,
safe: settings.allowDangerousCharacters ? 0 : 1,
schema: settings.space === 'svg' ? svg : html,
omit: settings.omitOptionalTags && omission,
quote: quote,
alternative: smart ? alternative : null,
unquoted: Boolean(settings.preferUnquoted),
tight: settings.tightAttributes,
tightDoctype: Boolean(settings.tightDoctype),
tightLists: settings.tightCommaSeparatedLists,
tightClose: settings.tightSelfClosing,
collapseEmpty: settings.collapseEmptyAttributes,
dangerous: settings.allowDangerousHTML,
voids: settings.voids || voids.concat(),
entities: settings.entities || {},
close: settings.closeSelfClosing,
closeEmpty: settings.closeEmptyElements
},
node
)
}
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@
"hast-util-is-element": "^1.0.0",
"hast-util-whitespace": "^1.0.0",
"html-void-elements": "^1.0.0",
"kebab-case": "^1.0.0",
"property-information": "^3.1.0",
"property-information": "^4.0.0",
"space-separated-tokens": "^1.0.0",
"stringify-entities": "^1.0.1",
"unist-util-is": "^2.0.0",
Expand All @@ -35,7 +34,7 @@
"browserify": "^16.0.0",
"bundle-collapser": "^1.2.1",
"esmangle": "^1.0.1",
"hastscript": "^3.0.0",
"hastscript": "^4.0.0",
"nyc": "^12.0.0",
"prettier": "^1.13.5",
"remark-cli": "^5.0.0",
Expand Down
Loading

0 comments on commit 665a075

Please sign in to comment.