236 lines
5.3 KiB
JavaScript
236 lines
5.3 KiB
JavaScript
'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 entities = require('stringify-entities')
|
||
var ccount = require('ccount')
|
||
var all = require('./all')
|
||
var constants = require('./constants')
|
||
|
||
module.exports = element
|
||
|
||
/* Constants. */
|
||
var EMPTY = ''
|
||
|
||
/* Characters. */
|
||
var SPACE = ' '
|
||
var DQ = '"'
|
||
var SQ = "'"
|
||
var EQ = '='
|
||
var LT = '<'
|
||
var GT = '>'
|
||
var SO = '/'
|
||
|
||
/* Stringify an element `node`. */
|
||
function element(ctx, node, index, parent) {
|
||
var parentSchema = ctx.schema
|
||
var name = node.tagName
|
||
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
|
||
* enables for example `menuitem`s, which are
|
||
* void in W3C HTML but not void in WHATWG HTML, to
|
||
* be stringified properly. */
|
||
selfClosing = content ? false : selfClosing
|
||
|
||
if (attrs || !omit || !omit.opening(node, index, parent)) {
|
||
value = LT + name + (attrs ? SPACE + attrs : EMPTY)
|
||
|
||
if (selfClosing && close) {
|
||
if (!ctx.tightClose || attrs.charAt(attrs.length - 1) === SO) {
|
||
value += SPACE
|
||
}
|
||
|
||
value += SO
|
||
}
|
||
|
||
value += GT
|
||
}
|
||
|
||
value += content
|
||
|
||
if (!selfClosing && (!omit || !omit.closing(node, index, parent))) {
|
||
value += LT + SO + name + GT
|
||
}
|
||
|
||
ctx.schema = parentSchema
|
||
|
||
return value
|
||
}
|
||
|
||
/* Stringify all attributes. */
|
||
function attributes(ctx, props) {
|
||
var values = []
|
||
var key
|
||
var value
|
||
var result
|
||
var length
|
||
var index
|
||
var last
|
||
|
||
for (key in props) {
|
||
value = props[key]
|
||
|
||
if (value == null) {
|
||
continue
|
||
}
|
||
|
||
result = attribute(ctx, key, value)
|
||
|
||
if (result) {
|
||
values.push(result)
|
||
}
|
||
}
|
||
|
||
length = values.length
|
||
index = -1
|
||
|
||
while (++index < length) {
|
||
result = values[index]
|
||
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) {
|
||
values[index] = result + SPACE
|
||
}
|
||
}
|
||
|
||
return values.join(EMPTY)
|
||
}
|
||
|
||
/* Stringify one attribute. */
|
||
function attribute(ctx, key, value) {
|
||
var schema = ctx.schema
|
||
var space = schema.space
|
||
var info = find(schema, key)
|
||
var name = info.attribute
|
||
|
||
if (info.overloadedBoolean && (value === name || value === '')) {
|
||
value = true
|
||
} else if (
|
||
info.boolean ||
|
||
(info.overloadedBoolean && typeof value !== 'string')
|
||
) {
|
||
value = Boolean(value)
|
||
}
|
||
|
||
if (
|
||
value == null ||
|
||
value === false ||
|
||
(typeof value === 'number' && isNaN(value))
|
||
) {
|
||
return EMPTY
|
||
}
|
||
|
||
name = attributeName(ctx, name)
|
||
|
||
if (value === true) {
|
||
if (space === 'html') {
|
||
return name
|
||
}
|
||
|
||
value = name
|
||
}
|
||
|
||
return name + attributeValue(ctx, key, value, info)
|
||
}
|
||
|
||
/* Stringify the attribute name. */
|
||
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]
|
||
|
||
return entities(name, xtend(ctx.entities, {subset: subset}))
|
||
}
|
||
|
||
/* Stringify the attribute value. */
|
||
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
|
||
* given here just to keep the code cleaner. */
|
||
value = (info.commaSeparated ? commas : spaces)(value, {
|
||
padLeft: !ctx.tightLists
|
||
})
|
||
}
|
||
|
||
value = String(value)
|
||
|
||
if (space !== 'html' || value || !ctx.collapseEmpty) {
|
||
unquoted = value
|
||
|
||
/* Check unquoted value. */
|
||
if (space === 'html' && ctx.unquoted) {
|
||
subset = constants.unquoted[ctx.valid][ctx.safe]
|
||
unquoted = entities(
|
||
value,
|
||
xtend(options, {subset: subset, attribute: true})
|
||
)
|
||
}
|
||
|
||
/* If `value` contains entities when unquoted... */
|
||
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
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
/* Don’t add a `=` for unquoted empties. */
|
||
value = value ? EQ + value : value
|
||
}
|
||
|
||
return value
|
||
}
|