'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 }