element.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. 'use strict'
  2. var xtend = require('xtend')
  3. var svg = require('property-information/svg')
  4. var find = require('property-information/find')
  5. var spaces = require('space-separated-tokens').stringify
  6. var commas = require('comma-separated-tokens').stringify
  7. var entities = require('stringify-entities')
  8. var ccount = require('ccount')
  9. var all = require('./all')
  10. var constants = require('./constants')
  11. module.exports = element
  12. /* Constants. */
  13. var EMPTY = ''
  14. /* Characters. */
  15. var SPACE = ' '
  16. var DQ = '"'
  17. var SQ = "'"
  18. var EQ = '='
  19. var LT = '<'
  20. var GT = '>'
  21. var SO = '/'
  22. /* Stringify an element `node`. */
  23. function element(ctx, node, index, parent) {
  24. var parentSchema = ctx.schema
  25. var name = node.tagName
  26. var value = ''
  27. var selfClosing
  28. var close
  29. var omit
  30. var root = node
  31. var content
  32. var attrs
  33. if (parentSchema.space === 'html' && name === 'svg') {
  34. ctx.schema = svg
  35. }
  36. attrs = attributes(ctx, node.properties)
  37. if (ctx.schema.space === 'svg') {
  38. omit = false
  39. close = true
  40. selfClosing = ctx.closeEmpty
  41. } else {
  42. omit = ctx.omit
  43. close = ctx.close
  44. selfClosing = ctx.voids.indexOf(name.toLowerCase()) !== -1
  45. if (name === 'template') {
  46. root = node.content
  47. }
  48. }
  49. content = all(ctx, root)
  50. /* If the node is categorised as void, but it has
  51. * children, remove the categorisation. This
  52. * enables for example `menuitem`s, which are
  53. * void in W3C HTML but not void in WHATWG HTML, to
  54. * be stringified properly. */
  55. selfClosing = content ? false : selfClosing
  56. if (attrs || !omit || !omit.opening(node, index, parent)) {
  57. value = LT + name + (attrs ? SPACE + attrs : EMPTY)
  58. if (selfClosing && close) {
  59. if (!ctx.tightClose || attrs.charAt(attrs.length - 1) === SO) {
  60. value += SPACE
  61. }
  62. value += SO
  63. }
  64. value += GT
  65. }
  66. value += content
  67. if (!selfClosing && (!omit || !omit.closing(node, index, parent))) {
  68. value += LT + SO + name + GT
  69. }
  70. ctx.schema = parentSchema
  71. return value
  72. }
  73. /* Stringify all attributes. */
  74. function attributes(ctx, props) {
  75. var values = []
  76. var key
  77. var value
  78. var result
  79. var length
  80. var index
  81. var last
  82. for (key in props) {
  83. value = props[key]
  84. if (value == null) {
  85. continue
  86. }
  87. result = attribute(ctx, key, value)
  88. if (result) {
  89. values.push(result)
  90. }
  91. }
  92. length = values.length
  93. index = -1
  94. while (++index < length) {
  95. result = values[index]
  96. last = null
  97. if (ctx.schema.space === 'html' && ctx.tight) {
  98. last = result.charAt(result.length - 1)
  99. }
  100. /* In tight mode, don’t add a space after quoted attributes. */
  101. if (index !== length - 1 && last !== DQ && last !== SQ) {
  102. values[index] = result + SPACE
  103. }
  104. }
  105. return values.join(EMPTY)
  106. }
  107. /* Stringify one attribute. */
  108. function attribute(ctx, key, value) {
  109. var schema = ctx.schema
  110. var space = schema.space
  111. var info = find(schema, key)
  112. var name = info.attribute
  113. if (info.overloadedBoolean && (value === name || value === '')) {
  114. value = true
  115. } else if (
  116. info.boolean ||
  117. (info.overloadedBoolean && typeof value !== 'string')
  118. ) {
  119. value = Boolean(value)
  120. }
  121. if (
  122. value == null ||
  123. value === false ||
  124. (typeof value === 'number' && isNaN(value))
  125. ) {
  126. return EMPTY
  127. }
  128. name = attributeName(ctx, name)
  129. if (value === true) {
  130. if (space === 'html') {
  131. return name
  132. }
  133. value = name
  134. }
  135. return name + attributeValue(ctx, key, value, info)
  136. }
  137. /* Stringify the attribute name. */
  138. function attributeName(ctx, name) {
  139. // Always encode without parse errors in non-HTML.
  140. var valid = ctx.schema.space === 'html' ? ctx.valid : 1
  141. var subset = constants.name[valid][ctx.safe]
  142. return entities(name, xtend(ctx.entities, {subset: subset}))
  143. }
  144. /* Stringify the attribute value. */
  145. function attributeValue(ctx, key, value, info) {
  146. var options = ctx.entities
  147. var quote = ctx.quote
  148. var alternative = ctx.alternative
  149. var space = ctx.schema.space
  150. var unquoted
  151. var subset
  152. if (typeof value === 'object' && 'length' in value) {
  153. /* `spaces` doesn’t accept a second argument, but it’s
  154. * given here just to keep the code cleaner. */
  155. value = (info.commaSeparated ? commas : spaces)(value, {
  156. padLeft: !ctx.tightLists
  157. })
  158. }
  159. value = String(value)
  160. if (space !== 'html' || value || !ctx.collapseEmpty) {
  161. unquoted = value
  162. /* Check unquoted value. */
  163. if (space === 'html' && ctx.unquoted) {
  164. subset = constants.unquoted[ctx.valid][ctx.safe]
  165. unquoted = entities(
  166. value,
  167. xtend(options, {subset: subset, attribute: true})
  168. )
  169. }
  170. /* If `value` contains entities when unquoted... */
  171. if (space !== 'html' || !ctx.unquoted || unquoted !== value) {
  172. /* If the alternative is less common than `quote`, switch. */
  173. if (alternative && ccount(value, quote) > ccount(value, alternative)) {
  174. quote = alternative
  175. }
  176. subset = quote === SQ ? constants.single : constants.double
  177. // Always encode without parse errors in non-HTML.
  178. subset = subset[space === 'html' ? ctx.valid : 1][ctx.safe]
  179. value = entities(value, xtend(options, {subset: subset, attribute: true}))
  180. value = quote + value + quote
  181. }
  182. /* Don’t add a `=` for unquoted empties. */
  183. value = value ? EQ + value : value
  184. }
  185. return value
  186. }