From 665a07516db53992287e77f0e83b90500c069448 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 17 Jul 2018 12:03:09 -0400 Subject: [PATCH] Add support for SVG Related to wooorm/property-information#6. Related to GH-5. --- lib/constants.js | 47 +++++++ lib/element.js | 109 ++++++++++------ lib/index.js | 90 ++++--------- package.json | 5 +- readme.md | 36 ++++- test/attribute.js | 50 +++---- test/index.js | 1 + test/svg.js | 327 ++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 521 insertions(+), 144 deletions(-) create mode 100644 lib/constants.js create mode 100644 test/svg.js diff --git a/lib/constants.js b/lib/constants.js new file mode 100644 index 0000000..4121276 --- /dev/null +++ b/lib/constants.js @@ -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)] + ] +} diff --git a/lib/element.js b/lib/element.js index ba78710..6652d0b 100644 --- a/lib/element.js +++ b/lib/element.js @@ -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. */ @@ -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 @@ -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 } @@ -60,6 +85,8 @@ function element(ctx, node, index, parent) { value += LT + SO + name + GT } + ctx.schema = parentSchema + return value } @@ -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) { @@ -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 @@ -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 } diff --git a/lib/index.js b/lib/index.js index c4bf259..b1f21dd 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,5 +1,7 @@ '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') @@ -7,58 +9,15 @@ 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( @@ -66,25 +25,26 @@ function toHTML(node, options) { ) } - 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 + ) } diff --git a/package.json b/package.json index 43ac409..33127b5 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/readme.md b/readme.md index e93626e..deed8d1 100644 --- a/readme.md +++ b/readme.md @@ -34,9 +34,17 @@ Yields: ## API -### `toHTML(node[, options])` +### `toHTML(tree[, options])` -Stringify the given [HAST node][hast]. +Stringify the given [HAST tree][hast]. + +###### `options.space` + +Whether the root of the given tree is in the `'html'` or `'svg'` space (enum, +`'svg'` or `'html'`, default: `'html'`). + +If an `svg` element is found in the HTML space, `toHTML` automatically switches +to the SVG space when entering the element, and switches back when leaving. ###### `options.entities` @@ -50,6 +58,8 @@ Configuration for [`stringify-entities`][stringify-entities] Tag-names of elements to stringify without closing tag (`Array.`, default: [`html-void-elements`][html-void-elements]). +Not used in the SVG space. + ###### `options.quote` Preferred quote to use (`'"'` or `'\''`, default: `'"'`). @@ -64,6 +74,8 @@ Use the other quote if that results in less bytes (`boolean`, default: Leave attributes unquoted if that results in less bytes (`boolean`, default: `false`). +Not used in the SVG space. + ###### `options.omitOptionalTags` Omit optional opening and closing tags (`boolean`, default: `false`). @@ -71,22 +83,36 @@ For example, in `
  1. one
  2. two
`, both `` closing tags can be omitted. The first because it’s followed by another `li`, the last because it’s followed by nothing. +Not used in the SVG space. + ###### `options.collapseEmptyAttributes` Collapse empty attributes: `class=""` is stringified as `class` instead (`boolean`, default: `false`). **Note**: boolean attributes, such as `hidden`, are always collapsed. +Not used in the SVG space. + ###### `options.closeSelfClosing` Close self-closing nodes with an extra slash (`/`): `` instead of `` (`boolean`, default: `false`). +Not used in the SVG space. + +###### `options.closeEmptyElements` + +Close SVG elements without any content with slash (`/`) on the opening tag +instead of an end tag: `` instead of `` (`boolean`, +default: `false`). + +Not used in the HTML space. + ###### `options.tightSelfClosing` Do not use an extra space when closing self-closing elements: `` instead of `` (`boolean`, default: `false`). **Note**: Only used -if `closeSelfClosing: true`. +if `closeSelfClosing: true` or `closeEmptyElements: true`. ###### `options.tightCommaSeparatedLists` @@ -101,6 +127,8 @@ Join attributes together, without white-space, if possible: instead to save bytes (`boolean`, default: `false`). **Note**: creates invalid (but working) markup. +Not used in the SVG space. + ###### `options.tightDoctype` Drop unneeded spaces in doctypes: `` instead of `` @@ -113,6 +141,8 @@ Do not encode characters which trigger parse errors (even though they work), to save bytes (`boolean`, default: `false`). **Note**: creates invalid (but working) markup. +Not used in the SVG space. + ###### `options.allowDangerousCharacters` Do not encode some characters which cause XSS vulnerabilities in older diff --git a/test/attribute.js b/test/attribute.js index c58cfab..80dc827 100644 --- a/test/attribute.js +++ b/test/attribute.js @@ -71,8 +71,8 @@ test('`element` attributes', function(t) { ) t.deepEqual( - to(h('span', {dataUnknown: ['alpha', 'bravo']})), - '', + to(h('span', {unknown: ['alpha', 'bravo']})), + '', 'should stringify unknown lists as space-separated' ) @@ -101,15 +101,15 @@ test('`element` attributes', function(t) { ) t.deepEqual( - to(h('i', {dataUnknown: false}, 'bravo')), - 'bravo', - 'should stringify unknown attributes set to `false`' + to(h('i', {unknown: false}, 'bravo')), + 'bravo', + 'should ignore unknown attributes set to `false`' ) t.deepEqual( - to(h('i', {dataUnknown: true}, 'bravo')), - 'bravo', - 'should stringify unknown attributes set to `true`' + to(h('i', {unknown: true}, 'bravo')), + 'bravo', + 'should stringify unknown attributes set to `true` as booleans' ) t.deepEqual( @@ -168,14 +168,14 @@ test('`element` attributes', function(t) { t.deepEqual( to(h('i', {id: true}, 'bravo')), - 'bravo', + 'bravo', 'should stringify other non-string attributes' ) t.deepEqual( to(h('img', {alt: ''}), {quote: "'"}), "", - 'should quote attribute values with single quotes is `quote: "\'"`' + 'should quote attribute values with single quotes if `quote: "\'"`' ) t.throws( @@ -189,7 +189,7 @@ test('`element` attributes', function(t) { t.deepEqual( to(h('img', {alt: ''}), {quote: '"'}), '', - "should quote attribute values with single quotes is `quote: '\"'`" + "should quote attribute values with double quotes if `quote: '\"'`" ) t.deepEqual( @@ -252,36 +252,18 @@ test('`element` attributes', function(t) { t.deepEqual( to(h('i', {title: "3'5"}), {allowDangerousCharacters: true}), '', - 'should not encode characters which cause XSS issues in older browsers, in `allowParseErrors` mode' + 'should not encode characters which cause XSS issues in older browsers, in `allowDangerousCharacters` mode' ) t.deepEqual( - to( - u( - 'element', - { - tagName: 'i', - properties: {id: null} - }, - [u('text', 'bravo')] - ) - ), - 'bravo', + to(u('element', {tagName: 'i', properties: {id: null}}, [])), + '', 'should ignore attributes set to `null`' ) t.deepEqual( - to( - u( - 'element', - { - tagName: 'i', - properties: {id: undefined} - }, - [u('text', 'bravo')] - ) - ), - 'bravo', + to(u('element', {tagName: 'i', properties: {id: undefined}}, [])), + '', 'should ignore attributes set to `undefined`' ) diff --git a/test/index.js b/test/index.js index 9ba8a1e..392c210 100644 --- a/test/index.js +++ b/test/index.js @@ -13,3 +13,4 @@ require('./attribute') require('./omission') require('./omission-opening') require('./omission-closing') +require('./svg') diff --git a/test/svg.js b/test/svg.js new file mode 100644 index 0000000..f49dbbe --- /dev/null +++ b/test/svg.js @@ -0,0 +1,327 @@ +'use strict' + +var test = require('tape') +var u = require('unist-builder') +var s = require('hastscript/svg') +var h = require('hastscript') +var to = require('..') + +test('svg', function(t) { + t.deepEqual( + to(s('path'), {space: 'svg'}), + '', + 'should stringify `element`s' + ) + + t.deepEqual( + to(s('foo'), {space: 'svg'}), + '', + 'should stringify unknown `element`s' + ) + + t.deepEqual( + to(s('g', s('circle')), {space: 'svg'}), + '', + 'should stringify `element`s with content' + ) + + t.deepEqual( + to(s('circle'), {space: 'svg', closeEmptyElements: true}), + '', + 'should stringify with ` /` in `closeEmptyElements` mode' + ) + + t.deepEqual( + to(s('circle'), { + space: 'svg', + closeEmptyElements: true, + tightSelfClosing: true + }), + '', + 'should stringify voids with `/` in `closeEmptyElements` and `tightSelfClosing` mode' + ) + + t.deepEqual( + to(s('text', {dataFoo: 'alpha'}, 'bravo')), + 'bravo', + 'should stringify properties' + ) + + t.deepEqual( + to(s('text', {className: ['alpha']}, 'bravo'), {space: 'svg'}), + 'bravo', + 'should stringify special properties' + ) + + t.deepEqual( + to(s('circle', {title: ''}), {space: 'svg', collapseEmptyAttributes: true}), + '', + 'should *not* collapse empty string attributes in `collapseEmptyAttributes` mode' + ) + + t.deepEqual( + to(s('text', {className: ['a', 'b'], title: 'c d'}, 'bravo'), { + space: 'svg' + }), + 'bravo', + 'should stringify multiple properties' + ) + + t.deepEqual( + to(s('text', {className: ['a', 'b'], title: 'c d'}, 'bravo'), { + space: 'svg', + tightAttributes: true + }), + 'bravo', + 'should *not* stringify multiple properties tightly in `tightAttributes` mode' + ) + + t.deepEqual( + to(s('text', {className: ['alpha', 'charlie']}, 'bravo'), {space: 'svg'}), + 'bravo', + 'should stringify space-separated attributes' + ) + + t.deepEqual( + to(s('glyph', {glyphName: ['foo', 'bar']}), {space: 'svg'}), + '', + 'should stringify comma-separated attributes' + ) + + t.deepEqual( + to(s('glyph', {glyphName: ['foo', 'bar']}), { + tightCommaSeparatedLists: true, + space: 'svg' + }), + '', + 'should stringify comma-separated attributes tighly in `tightCommaSeparatedLists` mode' + ) + + t.deepEqual( + to(s('circle', {unknown: ['alpha', 'bravo']}), {space: true}), + '', + 'should stringify unknown lists as space-separated' + ) + + t.deepEqual( + to(s('a', {download: true}, 'bravo'), {space: 'svg'}), + 'bravo', + 'should stringify known boolean attributes set to `true`' + ) + + t.deepEqual( + to(s('a', {download: false}, 'bravo'), {space: 'svg'}), + 'bravo', + 'should ignore known boolean attributes set to `false`' + ) + + t.deepEqual( + to(s('a', {download: 1}, 'bravo'), {space: 'svg'}), + 'bravo', + 'should stringify truthy known boolean attributes' + ) + + t.deepEqual( + to(s('a', {download: 0}, 'bravo'), {space: 'svg'}), + 'bravo', + 'should ignore falsey known boolean attributes' + ) + + t.deepEqual( + to(s('a', {unknown: false}, 'bravo'), {space: 'svg'}), + 'bravo', + 'should ignore unknown attributes set to `false`' + ) + + t.deepEqual( + to(s('a', {unknown: true}, 'bravo'), {space: 'svg'}), + 'bravo', + 'should stringify unknown attributes set to `true`' + ) + + t.deepEqual( + to(s('path', {strokeOpacity: 0.7}), {space: 'svg'}), + '', + 'should stringify positive known numeric attributes' + ) + + t.deepEqual( + to(s('path', {strokeMiterLimit: -1}), {space: 'svg'}), + '', + 'should stringify negative known numeric attributes' + ) + + t.deepEqual( + to(s('path', {strokeOpacity: 0}), {space: 'svg'}), + '', + 'should stringify known numeric attributes set to `0`' + ) + + t.deepEqual( + to(s('path', {strokeOpacity: NaN}), {space: 'svg'}), + '', + 'should ignore known numeric attributes set to `NaN`' + ) + + t.deepEqual( + to(s('path', {strokeOpacity: {toString: () => 'yup'}}), {space: 'svg'}), + '', + 'should stringify known numeric attributes set to non-numeric values' + ) + + t.deepEqual( + to(s('svg', {viewBox: '0 0 10 10'}), {space: 'svg'}), + '', + 'should stringify other attributes' + ) + + t.deepEqual( + to(s('svg', {viewBox: ''}), {space: 'svg'}), + '', + 'should stringify other falsey attributes' + ) + + t.deepEqual( + to(s('i', {id: true}, 'bravo'), {space: 'svg'}), + 'bravo', + 'should stringify other non-string attributes' + ) + + t.deepEqual( + to(s('svg', {viewBox: '0 0 10 10'}), {space: 'svg', quote: "'"}), + "", + 'should quote attribute values with single quotes if `quote: "\'"`' + ) + + t.deepEqual( + to(s('svg', {viewBox: '0 0 10 10'}), {space: 'svg', quote: '"'}), + '', + "should quote attribute values with double quotes if `quote: '\"'`" + ) + + t.deepEqual( + to(s('circle', {title: '"some \' stuff"'}), { + space: 'svg', + quote: '"', + quoteSmart: true + }), + "", + 'should quote smartly if the other quote is less prominent (#1)' + ) + + t.deepEqual( + to(s('circle', {title: "'some \" stuff'"}), { + space: 'svg', + quote: '"', + quoteSmart: true + }), + '', + 'should quote smartly if the other quote is less prominent (#2)' + ) + + t.deepEqual( + to(s('circle', {cx: 2}), {space: 'svg', preferUnquoted: true}), + '', + 'should *not* omit quotes in `preferUnquoted`' + ) + + t.deepEqual( + to(s('circle', {'3<5\0': 'alpha'}), {space: 'svg'}), + '', + 'should encode entities in attribute names' + ) + + t.deepEqual( + to(s('circle', {title: '3<5\0'}), {space: 'svg'}), + '', + 'should encode entities in attribute values' + ) + + t.deepEqual( + to(s('circle', {'3=5\0': 'alpha'}), {space: 'svg', allowParseErrors: true}), + '', + '*should* encode characters in attribute names which cause parse errors, work, even though `allowParseErrors` mode is on' + ) + + t.deepEqual( + to(s('circle', {title: '3"5\0'}), {space: 'svg', allowParseErrors: true}), + '', + '*should* encode characters in attribute values which cause parse errors, work, even though `allowParseErrors` mode is on' + ) + + t.deepEqual( + to(s('circle', {title: "3'5"}), { + space: 'svg', + allowDangerousCharacters: true + }), + '', + 'should not encode characters which cause XSS issues in older browsers, in `allowDangerousCharacters` mode' + ) + + t.deepEqual( + to(u('element', {tagName: 'circle', properties: {id: null}}, [])), + '', + 'should ignore attributes set to `null`' + ) + + t.deepEqual( + to(u('element', {tagName: 'circle', properties: {id: undefined}}, [])), + '', + 'should ignore attributes set to `undefined`' + ) + + t.deepEqual( + to( + s( + 'svg', + { + xlmns: 'http://www.w3.org/2000/svg', + xmlnsXLink: 'http://www.w3.org/1999/xlink', + width: 500, + height: 500, + viewBox: [0, 0, 500, 500] + }, + [ + s('title', 'SVG `` element'), + s('circle', {cx: 120, cy: 120, r: 100}) + ] + ), + {space: 'svg'} + ), + [ + '', + 'SVG `<circle>` element', + '', + '' + ].join(''), + 'should stringify an SVG tree' + ) + + t.deepEqual( + to( + u('root', [ + u('doctype', {name: 'html'}), + h('head', h('title', 'The SVG `` element')), + h('body', [ + s( + 'svg', + {xlmns: 'http://www.w3.org/2000/svg', viewbox: [0, 0, 500, 500]}, + s('circle', {cx: 120, cy: 120, r: 100}) + ) + ]) + ]) + ), + [ + '', + 'The SVG `<circle>` element', + '', + '', + '', + '', + '' + ].join(''), + 'should stringify an HTML tree with embedded HTML' + ) + + t.end() +})