serialize.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. "use strict";
  2. const xnv = require("xml-name-validator");
  3. const attributeUtils = require("./attributes");
  4. const { NAMESPACES, VOID_ELEMENTS, NODE_TYPES } = require("./constants");
  5. const XML_CHAR = /^(\x09|\x0A|\x0D|[\x20-\uD7FF]|[\uE000-\uFFFD]|(?:[\uD800-\uDBFF][\uDC00-\uDFFF]))*$/;
  6. const PUBID_CHAR = /^(\x20|\x0D|\x0A|[a-zA-Z0-9]|[-'()+,./:=?;!*#@$_%])*$/;
  7. function asciiCaseInsensitiveMatch(a, b) {
  8. if (a.length !== b.length) {
  9. return false;
  10. }
  11. for (let i = 0; i < a.length; ++i) {
  12. if ((a.charCodeAt(i) | 32) !== (b.charCodeAt(i) | 32)) {
  13. return false;
  14. }
  15. }
  16. return true;
  17. }
  18. function recordNamespaceInformation(element, map, prefixMap) {
  19. let defaultNamespaceAttrValue = null;
  20. for (let i = 0; i < element.attributes.length; ++i) {
  21. const attr = element.attributes[i];
  22. if (attr.namespaceURI === NAMESPACES.XMLNS) {
  23. if (attr.prefix === null) {
  24. defaultNamespaceAttrValue = attr.value;
  25. continue;
  26. }
  27. let namespaceDefinition = attr.value;
  28. if (namespaceDefinition === NAMESPACES.XML) {
  29. continue;
  30. }
  31. // This is exactly the other way than the spec says, but that's intended.
  32. // All the maps coalesce null to the empty string (explained in the
  33. // spec), so instead of doing that every time, just do it once here.
  34. if (namespaceDefinition === null) {
  35. namespaceDefinition = "";
  36. }
  37. if (
  38. namespaceDefinition in map &&
  39. map[namespaceDefinition].includes(attr.localName)
  40. ) {
  41. continue;
  42. }
  43. if (!(namespaceDefinition in map)) {
  44. map[namespaceDefinition] = [];
  45. }
  46. map[namespaceDefinition].push(attr.localName);
  47. prefixMap[attr.localName] = namespaceDefinition;
  48. }
  49. }
  50. return defaultNamespaceAttrValue;
  51. }
  52. function serializeDocumentType(node, namespace, prefixMap, requireWellFormed) {
  53. if (requireWellFormed && !PUBID_CHAR.test(node.publicId)) {
  54. throw new Error("Failed to serialize XML: document type node publicId is not well-formed.");
  55. }
  56. if (
  57. requireWellFormed &&
  58. (!XML_CHAR.test(node.systemId) ||
  59. (node.systemId.includes('"') && node.systemId.includes("'")))
  60. ) {
  61. throw new Error("Failed to serialize XML: document type node systemId is not well-formed.");
  62. }
  63. let markup = `<!DOCTYPE ${node.name}`;
  64. if (node.publicId !== "") {
  65. markup += ` PUBLIC "${node.publicId}"`;
  66. } else if (node.systemId !== "") {
  67. markup += " SYSTEM";
  68. }
  69. if (node.systemId !== "") {
  70. markup += ` "${node.systemId}"`;
  71. }
  72. return markup + ">";
  73. }
  74. function serializeProcessingInstruction(
  75. node,
  76. namespace,
  77. prefixMap,
  78. requireWellFormed
  79. ) {
  80. if (
  81. requireWellFormed &&
  82. (node.target.includes(":") || asciiCaseInsensitiveMatch(node.target, "xml"))
  83. ) {
  84. throw new Error("Failed to serialize XML: processing instruction node target is not well-formed.");
  85. }
  86. if (
  87. requireWellFormed &&
  88. (!XML_CHAR.test(node.data) || node.data.includes("?>"))
  89. ) {
  90. throw new Error("Failed to serialize XML: processing instruction node data is not well-formed.");
  91. }
  92. return `<?${node.target} ${node.data}?>`;
  93. }
  94. function serializeDocument(
  95. node,
  96. namespace,
  97. prefixMap,
  98. requireWellFormed,
  99. refs
  100. ) {
  101. if (requireWellFormed && node.documentElement === null) {
  102. throw new Error("Failed to serialize XML: document does not have a document element.");
  103. }
  104. let serializedDocument = "";
  105. for (const child of node.childNodes) {
  106. serializedDocument += xmlSerialization(
  107. child,
  108. namespace,
  109. prefixMap,
  110. requireWellFormed,
  111. refs
  112. );
  113. }
  114. return serializedDocument;
  115. }
  116. function serializeDocumentFragment(
  117. node,
  118. namespace,
  119. prefixMap,
  120. requireWellFormed,
  121. refs
  122. ) {
  123. let markup = "";
  124. for (const child of node.childNodes) {
  125. markup += xmlSerialization(
  126. child,
  127. namespace,
  128. prefixMap,
  129. requireWellFormed,
  130. refs
  131. );
  132. }
  133. return markup;
  134. }
  135. function serializeText(node, namespace, prefixMap, requireWellFormed) {
  136. if (requireWellFormed && !XML_CHAR.test(node.data)) {
  137. throw new Error("Failed to serialize XML: text node data is not well-formed.");
  138. }
  139. return node.data
  140. .replace(/&/g, "&amp;")
  141. .replace(/</g, "&lt;")
  142. .replace(/>/g, "&gt;");
  143. }
  144. function serializeComment(node, namespace, prefixMap, requireWellFormed) {
  145. if (requireWellFormed && !XML_CHAR.test(node.data)) {
  146. throw new Error("Failed to serialize XML: comment node data is not well-formed.");
  147. }
  148. if (
  149. requireWellFormed &&
  150. (node.data.includes("--") || node.data.endsWith("-"))
  151. ) {
  152. throw new Error("Failed to serialize XML: found hyphens in illegal places in comment node data.");
  153. }
  154. return `<!--${node.data}-->`;
  155. }
  156. function serializeElement(node, namespace, prefixMap, requireWellFormed, refs) {
  157. if (
  158. requireWellFormed &&
  159. (node.localName.includes(":") || !xnv.name(node.localName))
  160. ) {
  161. throw new Error("Failed to serialize XML: element node localName is not a valid XML name.");
  162. }
  163. let markup = "<";
  164. let qualifiedName = "";
  165. let skipEndTag = false;
  166. let ignoreNamespaceDefinitionAttr = false;
  167. const map = Object.assign({}, prefixMap);
  168. const localPrefixesMap = Object.create(null);
  169. const localDefaultNamespace = recordNamespaceInformation(
  170. node,
  171. map,
  172. localPrefixesMap
  173. );
  174. let inheritedNs = namespace;
  175. const ns = node.namespaceURI;
  176. if (inheritedNs === ns) {
  177. if (localDefaultNamespace !== null) {
  178. ignoreNamespaceDefinitionAttr = true;
  179. }
  180. if (ns === NAMESPACES.XML) {
  181. qualifiedName = "xml:" + node.localName;
  182. } else {
  183. qualifiedName = node.localName;
  184. }
  185. markup += qualifiedName;
  186. } else {
  187. let { prefix } = node;
  188. let candidatePrefix = attributeUtils.preferredPrefixString(map, ns, prefix);
  189. if (prefix === "xmlns") {
  190. if (requireWellFormed) {
  191. throw new Error("Failed to serialize XML: element nodes can't have a prefix of \"xmlns\".");
  192. }
  193. candidatePrefix = "xmlns";
  194. }
  195. if (candidatePrefix !== null) {
  196. qualifiedName = candidatePrefix + ":" + node.localName;
  197. if (
  198. localDefaultNamespace !== null &&
  199. localDefaultNamespace !== NAMESPACES.XML
  200. ) {
  201. inheritedNs =
  202. localDefaultNamespace === "" ? null : localDefaultNamespace;
  203. }
  204. markup += qualifiedName;
  205. } else if (prefix !== null) {
  206. if (prefix in localPrefixesMap) {
  207. prefix = attributeUtils.generatePrefix(map, ns, refs.prefixIndex++);
  208. }
  209. if (map[ns]) {
  210. map[ns].push(prefix);
  211. } else {
  212. map[ns] = [prefix];
  213. }
  214. qualifiedName = prefix + ":" + node.localName;
  215. markup += `${qualifiedName} xmlns:${prefix}="${attributeUtils.serializeAttributeValue(
  216. ns,
  217. requireWellFormed
  218. )}"`;
  219. if (localDefaultNamespace !== null) {
  220. inheritedNs =
  221. localDefaultNamespace === "" ? null : localDefaultNamespace;
  222. }
  223. } else if (localDefaultNamespace === null || localDefaultNamespace !== ns) {
  224. ignoreNamespaceDefinitionAttr = true;
  225. qualifiedName = node.localName;
  226. inheritedNs = ns;
  227. markup += `${qualifiedName} xmlns="${attributeUtils.serializeAttributeValue(
  228. ns,
  229. requireWellFormed
  230. )}"`;
  231. } else {
  232. qualifiedName = node.localName;
  233. inheritedNs = ns;
  234. markup += qualifiedName;
  235. }
  236. }
  237. markup += attributeUtils.serializeAttributes(
  238. node,
  239. map,
  240. localPrefixesMap,
  241. ignoreNamespaceDefinitionAttr,
  242. requireWellFormed,
  243. refs
  244. );
  245. if (
  246. ns === NAMESPACES.HTML &&
  247. node.childNodes.length === 0 &&
  248. VOID_ELEMENTS.has(node.localName)
  249. ) {
  250. markup += " /";
  251. skipEndTag = true;
  252. } else if (ns !== NAMESPACES.HTML && node.childNodes.length === 0) {
  253. markup += "/";
  254. skipEndTag = true;
  255. }
  256. markup += ">";
  257. if (skipEndTag) {
  258. return markup;
  259. }
  260. if (ns === NAMESPACES.HTML && node.localName === "template") {
  261. markup += xmlSerialization(
  262. node.content,
  263. inheritedNs,
  264. map,
  265. requireWellFormed,
  266. refs
  267. );
  268. } else {
  269. for (const child of node.childNodes) {
  270. markup += xmlSerialization(
  271. child,
  272. inheritedNs,
  273. map,
  274. requireWellFormed,
  275. refs
  276. );
  277. }
  278. }
  279. markup += `</${qualifiedName}>`;
  280. return markup;
  281. }
  282. function serializeCDATASection(node) {
  283. return "<![CDATA[" + node.data + "]]>";
  284. }
  285. /**
  286. * @param {{prefixIndex: number}} refs
  287. */
  288. function xmlSerialization(node, namespace, prefixMap, requireWellFormed, refs) {
  289. switch (node.nodeType) {
  290. case NODE_TYPES.ELEMENT_NODE:
  291. return serializeElement(
  292. node,
  293. namespace,
  294. prefixMap,
  295. requireWellFormed,
  296. refs
  297. );
  298. case NODE_TYPES.DOCUMENT_NODE:
  299. return serializeDocument(
  300. node,
  301. namespace,
  302. prefixMap,
  303. requireWellFormed,
  304. refs
  305. );
  306. case NODE_TYPES.COMMENT_NODE:
  307. return serializeComment(node, namespace, prefixMap, requireWellFormed);
  308. case NODE_TYPES.TEXT_NODE:
  309. return serializeText(node, namespace, prefixMap, requireWellFormed);
  310. case NODE_TYPES.DOCUMENT_FRAGMENT_NODE:
  311. return serializeDocumentFragment(
  312. node,
  313. namespace,
  314. prefixMap,
  315. requireWellFormed,
  316. refs
  317. );
  318. case NODE_TYPES.DOCUMENT_TYPE_NODE:
  319. return serializeDocumentType(
  320. node,
  321. namespace,
  322. prefixMap,
  323. requireWellFormed
  324. );
  325. case NODE_TYPES.PROCESSING_INSTRUCTION_NODE:
  326. return serializeProcessingInstruction(
  327. node,
  328. namespace,
  329. prefixMap,
  330. requireWellFormed
  331. );
  332. case NODE_TYPES.ATTRIBUTE_NODE:
  333. return "";
  334. case NODE_TYPES.CDATA_SECTION_NODE:
  335. return serializeCDATASection(node);
  336. default:
  337. throw new TypeError("Failed to serialize XML: only Nodes can be serialized.");
  338. }
  339. }
  340. module.exports = (root, { requireWellFormed = false } = {}) => {
  341. const namespacePrefixMap = Object.create(null);
  342. namespacePrefixMap["http://www.w3.org/XML/1998/namespace"] = ["xml"];
  343. return xmlSerialization(root, null, namespacePrefixMap, requireWellFormed, {
  344. prefixIndex: 1
  345. });
  346. };