From eb6c89158039203164e80c8d7345910d5bd2e469 Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Tue, 17 Oct 2023 20:10:24 +0200 Subject: [PATCH] Rework prefix resolution during serialization This re-implements the algorithms used to determine what prefixes to assign and what declarations to output for them during serialization. The algorithms in the DOM parsing and serialization spec contain too many bugs that have so far not been addressed, and contain complicated branching that makes it hard to reason about them. In the replacement I've tried to remain close to the original behavior in spirit. That is, existing prefixes (or, for elements, an inherited default namespace) are preferred over authored prefixes but only if they are still in scope. Authored prefixes are preferred over generated ones. A new prefix is generated only if the authored prefix of an attribute conflicts with a declaration on the same element. Declaration attributes in the DOM are only preserved if they actually represent a change to the namespaces in scope for their element and do not cause conflicts - the spec preserved default namespace declarations in a few other cases but those seemed unnecessary. --- .vscode/settings.json | 3 + README.md | 22 +- api/slimdom.api.json | 2 +- src/dom-parsing/NamespacePrefixMap.ts | 310 ++++++------ src/dom-parsing/serializationAlgorithms.ts | 519 ++++----------------- test/dom-parsing/XMLSerializer.tests.ts | 35 +- 6 files changed, 282 insertions(+), 609 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4b76eef --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "jest.jestCommandLine": "npm run test --" +} diff --git a/README.md b/README.md index eeb1a53..a9d5ba0 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Fast, tiny, standards-compliant XML DOM implementation for node and the browser. This is a (partial) implementation of the following specifications: - [DOM living standard][domstandard], as last updated 19 December 2021 -- [DOM Parsing and Serialization W3C Editor's Draft][domparsing], as last updated 20 April 2020 +- [DOM Parsing and Serialization W3C Editor's Draft][domparsing], as last updated 2 May 2021 - [Extensible Markup Language (XML) 1.0 (Fifth Edition)][xmlstandard] - [Namespaces in XML 1.0 (Third Edition)][xml-names] @@ -37,24 +37,24 @@ The package includes both a commonJS-compatible UMD bundle (`dist/slimdom.umd.js ## Usage -Create documents using the slimdom.Document constructor, and manipulate them using the [standard DOM API][domstandard]. +Create documents by parsing XML or start from scratch using the slimdom.Document constructor, and manipulate them using the [standard DOM API][domstandard]. ```javascript import * as slimdom from 'slimdom'; // alternatively, in node and other commonJS environments: // const slimdom = require('slimdom'); -// Start with an empty document: -const document = new slimdom.Document(); -document.appendChild(document.createElementNS('http://www.example.com', 'root')); -const xml = slimdom.serializeToWellFormedString(document); -// -> '' - -// Or parse from a string: +// Parse from a string: const document2 = slimdom.parseXmlDocument('Hello!'); document2.documentElement.setAttribute('attr', 'new value'); const xml2 = slimdom.serializeToWellFormedString(document2); // -> 'Hello!' + +// Or start with an empty document: +const document = new slimdom.Document(); +document.appendChild(document.createElementNS('http://www.example.com', 'root')); +const xml = slimdom.serializeToWellFormedString(document); +// -> '' ``` Some DOM API's, such as the `DocumentFragment` constructor, require the presence of a global document, for instance to set their initial `ownerDocument` property. In these cases, slimdom will use the instance exposed through `slimdom.document`. Although you could mutate this document, it is recommended to always create your own documents (using the `Document` constructor) to avoid conflicts with other code using slimdom in your application. @@ -71,11 +71,11 @@ This library implements: - `XMLSerializer`, and read-only versions of `innerHTML` / `outerHTML` on `Element`. - `DOMParser`, for XML parsing only. -This library is currently aimed at providing a lightweight and consistent experience for dealing with XML and XML-like data. For simplicity and efficiency, this implementation deviates from the spec in a few minor ways. Most notably, normal JavaScript arrays are used instead of `HTMLCollection` / `NodeList` and `NamedNodeMap`, HTML documents are treated no different from other documents and a number of features from in the DOM spec are missing. In most cases, this is because alternatives are available that can be used together with slimdom with minimal effort. +This library is aimed at providing a lightweight and consistent experience for dealing with XML and XML-like data. For simplicity and efficiency, this implementation deviates from the spec in a few minor ways. Most notably, normal JavaScript arrays are used instead of `HTMLCollection` / `NodeList` and `NamedNodeMap`, HTML documents are treated no different from other documents and a number of features from in the DOM spec are missing. In most cases, this is because alternatives are available that can be used together with slimdom with minimal effort. Do not rely on the behavior or presence of any methods and properties not specified in the DOM standard. For example, do not use JavaScript array methods exposed on properties that should expose a NodeList and do not use Element as a constructor. This behavior is _not_ considered public API and may change without warning in a future release. -This library implements the changes from [whatwg/dom#819][dom-adopt-pr], as the specification as currently described has known bugs around adoption. +This library implements the changes from [whatwg/dom#819][dom-adopt-pr], as the DOM specification as currently described has known bugs around adoption. It also deviates from the [DOM serialization algorithms][domparsing] that deal with assigning and resolving conflicts in namespace prefixes, as the specification has a number of bugs that currently remain unaddressed. As serializing XML using the `XMLSerializer` does not enforce well-formedness, you may instead want to use the `serializeToWellFormedString` function which does perform such checks. diff --git a/api/slimdom.api.json b/api/slimdom.api.json index 7e1e492..9a20749 100644 --- a/api/slimdom.api.json +++ b/api/slimdom.api.json @@ -1,7 +1,7 @@ { "metadata": { "toolPackage": "@microsoft/api-extractor", - "toolVersion": "7.35.1", + "toolVersion": "7.38.0", "schemaVersion": 1011, "oldestForwardsCompatibleVersion": 1001, "tsdocConfig": { diff --git a/src/dom-parsing/NamespacePrefixMap.ts b/src/dom-parsing/NamespacePrefixMap.ts index 2b44a25..6870950 100644 --- a/src/dom-parsing/NamespacePrefixMap.ts +++ b/src/dom-parsing/NamespacePrefixMap.ts @@ -1,118 +1,64 @@ +import Attr from '../Attr'; import Element from '../Element'; +import { isAttrNode } from '../util/NodeType'; import { XML_NAMESPACE, XMLNS_NAMESPACE } from '../util/namespaceHelpers'; +export type PrefixIndex = { value: number }; + // 3.2.1.1.2 The Namespace Prefix Map /** - * A namespace prefix map is a map that associates namespaceURI and namespace prefix lists, where - * namespaceURI values are the map's unique keys (which can include the null value representing no - * namespace), and ordered lists of associated prefix values are the map's key values. The namespace - * prefix map will be populated by previously seen namespaceURIs and all their previously - * encountered prefix associations for a given node and its ancestors. + * A namespace prefix map is a map that associates namespaceURI and namespace + * prefix lists. + * + * This deviates from the specification to fix a number of bugs in the spec that + * can cause it to otherwise produce non-well-formed markup or markup that does + * not capture the author's intent. * - * NOTE: the last seen prefix for a given namespaceURI is at the end of its respective list. The - * list is searched to find potentially matching prefixes, and if no matches are found for the given - * namespaceURI, then the last prefix in the list is used. See copy a namespace prefix map and - * retrieve a preferred prefix string for additional details. + * Instead of only tracking candidate prefixes by namespace, this also tracks + * the current prefix to namespace mapping so we can properly detect when + * prefixes have been redefined. This implementation also tracks maps as a tree + * to avoid copying as well as the need to separately track locally defined + * prefixes. */ export class NamespacePrefixMap { - private _map: Map = new Map(); - - /** - * To copy a namespace prefix map map means to copy the map's keys into a new empty namespace - * prefix map, and to copy each of the values in the namespace prefix list associated with each - * keys' value into a new list which should be associated with the respective key in the new - * map. - * - * @returns A copy of the namespace prefix map - */ - public copy(): NamespacePrefixMap { - const copy = new NamespacePrefixMap(); - // Array.from needed to allow compilation to ES5 targets - for (const [namespace, prefixes] of Array.from(this._map.entries())) { - copy._map.set(namespace, prefixes.concat()); - } - return copy; - } + private _parent: NamespacePrefixMap | null; - /** - * To retrieve a preferred prefix string preferred prefix from the namespace prefix map map - * given a namespace ns, the user agent should: - * - * @param preferredPrefix - The prefix to look up - * @param ns - The namespace for the prefix - * - * @returns The matching candidate prefix, if found, or null otherwise - */ - public retrievePreferredPrefixString( - preferredPrefix: string | null, - ns: string | null - ): string | null { - // 1. Let candidates list be the result of retrieving a list from map where there exists a - // key in map that matches the value of ns or if there is no such key, then stop running - // these steps, and return the null value. - const candidatesList = this._map.get(ns); - if (candidatesList === undefined) { - return null; - } + private _nsByPrefix = new Map(); - // 2. Otherwise, for each prefix value prefix in candidates list, iterating from beginning - // to end: - // NOTE: There will always be at least one prefix value in the list. - for (const prefix of candidatesList) { - // 2.1. If prefix matches preferred prefix, then stop running these steps and return - // prefix. - if (prefix === preferredPrefix) { - return prefix; - } + private _prefixCandidatesByNs: Map = new Map(); - // 2.2. If prefix is the last item in the candidates list, then stop running these steps - // and return prefix. - } - return candidatesList[candidatesList.length - 1]; + private constructor(parent: NamespacePrefixMap | null) { + this._parent = parent; } - /** - * To check if a prefix string prefix is found in a namespace prefix map map given a namespace - * ns, the user agent should: - * - * @param prefix - The prefix to check - * @param ns - The namespace to check - * - * @returns Whether the combination of prefix and ns is found in the map - */ - public checkIfFound(prefix: string, ns: string | null): boolean { - // 1. Let candidates list be the result of retrieving a list from map where there exists a - // key in map that matches the value of ns or if there is no such key, then stop running - // these steps, and return false. - const candidatesList = this._map.get(ns); - if (candidatesList === undefined) { - return false; - } - - // 2. If the value of prefix occurs at least once in candidates list, return true, otherwise - // return false. - return candidatesList.indexOf(prefix) >= 0; + public static new(): NamespacePrefixMap { + const map = new NamespacePrefixMap(null); + // Register implicitly declared namespaces + map.add(null, null); + map.add('xml', XML_NAMESPACE); + map.add('xmlns', XMLNS_NAMESPACE); + return map; } /** - * To add a prefix string prefix to the namespace prefix map map given a namespace ns, the user - * agent should: + * To add a prefix string prefix to the namespace prefix map map given a + * namespace ns, the user agent should: * * @param prefix - The prefix to add * @param ns - The namespace to add for prefix */ - public add(prefix: string, ns: string | null): void { + public add(prefix: string | null, ns: string | null): void { // 1. Let candidates list be the result of retrieving a list from map where there exists a // key in map that matches the value of ns or if there is no such key, then let candidates // list be null. // (undefined used instead of null for convenience) - const candidatesList = this._map.get(ns); + const candidatesList = this._prefixCandidatesByNs.get(ns); // 2. If candidates list is null, then create a new list with prefix as the only item in the // list, and associate that list with a new key ns in map. if (candidatesList === undefined) { - this._map.set(ns, [prefix]); + this._prefixCandidatesByNs.set(ns, [prefix]); } else { // 3. Otherwise, append prefix to the end of candidates list. candidatesList.push(prefix); @@ -122,102 +68,126 @@ export class NamespacePrefixMap { // recently used (MRU) prefix associated with a given namespace, which will be the prefix at // the end of the list. This list may contain duplicates of the same prefix value seen // earlier (and that's OK). - } -} -export type LocalPrefixesMap = { [key: string]: string | null }; - -// 3.2.1.1.1 Recording the namespace + this._nsByPrefix.set(prefix, ns); + } -/** - * This following algorithm will update the namespace prefix map with any found namespace prefix - * definitions, add the found prefix definitions to the local prefixes map, and return a local - * default namespace value defined by a default namespace attribute if one exists. Otherwise it - * returns null. - * - * @param element - Element for which to record namespace information - * @param map - The namespace prefix map to update - * @param localPrefixesMap - The local prefixes map to update - * - * @returns The local default namespace value for element, or null if element does not define one - */ -export function recordNamespaceInformation( - element: Element, - map: NamespacePrefixMap, - localPrefixesMap: LocalPrefixesMap -): string | null { - // 1. Let default namespace attr value be null. - let defaultNamespaceAttrValue: string | null = null; - - // 2. Main: For each attribute attr in element's attributes, in the order they are specified in - // the element's attribute list: - // NOTE: The following conditional steps find namespace prefixes. Only attributes in the XMLNS - // namespace are considered (e.g., attributes made to look like namespace declarations via - // setAttribute("xmlns:pretend-prefix", "pretend-namespace") are not included). - for (const attr of element.attributes) { - // 2.1. Let attribute namespace be the value of attr's namespaceURI value. - const attributeNamespace = attr.namespaceURI; - - // 2.2. Let attribute prefix be the value of attr's prefix. - const attributePrefix = attr.prefix; - - // 2.3. If the attribute namespace is the XMLNS namespace, then: - if (attributeNamespace === XMLNS_NAMESPACE) { - // 2.3.1. If attribute prefix is null, then attr is a default namespace declaration. Set - // the default namespace attr value to attr's value and stop running these steps, - // returning to Main to visit the next attribute. - if (attributePrefix === null) { - defaultNamespaceAttrValue = attr.value; + public recordNamespaceInformation(element: Element): NamespacePrefixMap { + const map = new NamespacePrefixMap(this); + for (const attr of element.attributes) { + if (attr.namespaceURI !== XMLNS_NAMESPACE) { + // Not a namespace declaration attribute continue; } - // 2.3.2. Otherwise, the attribute prefix is non-null and attr is a namespace prefix - // definition. Run the following steps: - // 2.3.2.1. Let prefix definition be the value of attr's localName. - const prefixDefinition = attr.localName; - - // 2.3.2.2. Let namespace definition be the value of attr's value. - let namespaceDefinition: string | null = attr.value; - - // 2.3.2.3. If namespace definition is the XML namespace, then stop running these steps, - // and return to Main to visit the next attribute. - // NOTE: XML namespace definitions in prefixes are completely ignored (in order to avoid - // unnecessary work when there might be prefix conflicts). XML namespaced elements are - // always handled uniformly by prefixing (and overriding if necessary) the element's - // localname with the reserved "xml" prefix. - if (namespaceDefinition === XML_NAMESPACE) { - continue; + const namespaceUri = attr.value === '' ? null : attr.value; + const definedPrefix = attr.prefix === null ? null : attr.localName; + map.add(definedPrefix, namespaceUri); + } + return map; + } + + private _localPrefixToNamespace(prefix: string | null): string | null | undefined { + return this._nsByPrefix.get(prefix); + } + + private _inheritedPrefixToNamespace(prefix: string | null): string | null | undefined { + return this._parent?.prefixToNamespace(prefix); + } + + public prefixToNamespace(prefix: string | null): string | null | undefined { + const ns = this._localPrefixToNamespace(prefix); + if (ns !== undefined) { + return ns; + } + return this._inheritedPrefixToNamespace(prefix); + } + + public shouldSerializeDeclaration(prefix: string | null, ns: string | null): boolean { + // An existing declaration attribute should be skipped if it doesn't + // match the local scope. It can be skipped if it doesn't change the + // inherited value. + return this.prefixToNamespace(prefix) === ns && this._inheritedPrefixToNamespace(prefix) !== ns; + } + + private _getCandidatePrefix(namespaceUri: string | null): string | null | undefined { + const candidates = this._prefixCandidatesByNs.get(namespaceUri); + if (candidates !== undefined) { + for (let i = candidates.length - 1; i >= 0; --i) { + const candidate = candidates[i]; + if (this.prefixToNamespace(candidate) === namespaceUri) { + return candidate; + } } + } + return undefined; + } + + public getPreferredPrefix(node: Element | Attr, prefixIndex: PrefixIndex): string | null { + // XML namespace must use the "xml" prefix + if (node.namespaceURI === XML_NAMESPACE) { + return 'xml'; + } - // 2.3.2.4. If namespace definition is the empty string (the declarative form of having - // no namespace), then let namespace definition be null instead. - if (namespaceDefinition === '') { - namespaceDefinition = null; + // XMLNS namespace must use "xmlns", except for default namespace + // declarations, which use no prefix + const isAttr = isAttrNode(node); + if (node.namespaceURI === XMLNS_NAMESPACE) { + if (isAttr && node.prefix === null) { + return null; } + return 'xmlns'; + } - // 2.3.2.5. If prefix definition is found in map given the namespace namespace - // definition, then stop running these steps, and return to Main to visit the next - // attribute. - // NOTE: This step avoids adding duplicate prefix definitions for the same namespace in - // the map. This has the side-effect of avoiding later serialization of duplicate - // namespace prefix declarations in any descendant nodes. - if (map.checkIfFound(prefixDefinition, namespaceDefinition)) { - continue; + // attributes in the null namespace don't have a prefix + if (isAttr && node.namespaceURI === null) { + return null; + } + + // elements use no prefix if their namespace is the inherited default + // namespace + if (!isAttr) { + let inheritedNs = this._inheritedPrefixToNamespace(null) ?? null; + if (node.namespaceURI === inheritedNs) { + // The caller should add this to the map to ensure that any + // current default namespace declaration is ignored. + return null; } + } - // 2.3.2.6. Add the prefix prefix definition to map given namespace namespace - // definition. - map.add(prefixDefinition, namespaceDefinition); + // If the authored prefix resolves to the requested namespace in scope, + // we can use it, except that attributes in a namespace can't use an + // empty prefix. + if ((!isAttr || node.prefix !== null) && this.prefixToNamespace(node.prefix) === node.namespaceURI) { + return node.prefix; + } - // 2.3.2.7. Add the value of prefix definition as a new key to the local prefixes map, - // with the namespace definition as the key's value replacing the value of null with the - // empty string if applicable. - localPrefixesMap[prefixDefinition] = - namespaceDefinition === null ? '' : namespaceDefinition; + // If any prefixes in scope resolve to the requested namespace, use the + // most recent one. + const candidatePrefix = this._getCandidatePrefix(node.namespaceURI); + if (candidatePrefix !== undefined) { + return candidatePrefix; } - } - // 3. Return the value of default namespace attr value. - // NOTE: The empty string is a legitimate return value and is not converted to null. - return defaultNamespaceAttrValue; + // No suitable existing declaration, try to use the authored prefix + + // Attributes can't use the authored prefix if it conflicts with an existing local declaration + if (isAttr) { + const namespaceForPrefix = this._localPrefixToNamespace(node.prefix); + const isValidPrefix = node.prefix !== null && (namespaceForPrefix === undefined || namespaceForPrefix === node.namespaceURI); + + if (!isValidPrefix) { + // Collision - generate a new prefix + while (true) { + const generatedPrefix = `ns${prefixIndex.value}`; + prefixIndex.value += 1; + if (this._localPrefixToNamespace(generatedPrefix) === undefined) { + return generatedPrefix; + } + } + } + } + + return node.prefix; + } } diff --git a/src/dom-parsing/serializationAlgorithms.ts b/src/dom-parsing/serializationAlgorithms.ts index 88e5327..512c8df 100644 --- a/src/dom-parsing/serializationAlgorithms.ts +++ b/src/dom-parsing/serializationAlgorithms.ts @@ -15,9 +15,8 @@ import { throwInvalidStateError } from '../util/errorHelpers'; import { HTML_NAMESPACE, XML_NAMESPACE, XMLNS_NAMESPACE } from '../util/namespaceHelpers'; import { NodeType } from '../util/NodeType'; import { - recordNamespaceInformation, - LocalPrefixesMap, NamespacePrefixMap, + PrefixIndex, } from './NamespacePrefixMap'; const HTML_VOID_ELEMENTS = [ @@ -81,8 +80,6 @@ export function serializeFragment( // 3.2.1. XML Serialization -type PrefixIndex = { value: number }; - /** * To produce an XML serialization of a Node node given a flag require well-formed, run the * following steps: @@ -96,18 +93,11 @@ export function produceXmlSerialization( requireWellFormed: boolean, result: string[] ): void { - // 1. Let namespace be a context namespace with value null. The context namespace tracks the XML - // serialization algorithm's current default namespace. The context namespace is changed when - // either an Element Node has a default namespace declaration, or the algorithm generates a - // default namespace declaration for the Element Node to match its own namespace. The algorithm - // assumes no namespace (null) to start. - const namespace: string | null = null; - // 2. Let prefix map be a new namespace prefix map. - const prefixMap = new NamespacePrefixMap(); + const prefixMap = NamespacePrefixMap.new(); // 3. Add the XML namespace with prefix value "xml" to prefix map. - prefixMap.add('xml', XML_NAMESPACE); + // (handled above) // 4. Let prefix index be a generated namespace prefix index with value 1. The generated // namespace prefix index is used to generate a new unique prefix value when no suitable @@ -123,7 +113,6 @@ export function produceXmlSerialization( try { runXmlSerializationAlgorithm( node, - namespace, prefixMap, prefixIndex, requireWellFormed, @@ -140,7 +129,6 @@ export function produceXmlSerialization( * were recieved by the caller and return their result to the caller. Re-throw any exceptions. * * @param node - The node to serialize - * @param namespace - The context namespace * @param prefixMap - The namespace prefix map * @param prefixIndex - A reference to the generated namespace prefix index * @param requireWellFormed - Determines whether the result needs to be well-formed @@ -150,7 +138,6 @@ export function produceXmlSerialization( */ function runXmlSerializationAlgorithm( node: Node, - namespace: null | string, prefixMap: NamespacePrefixMap, prefixIndex: PrefixIndex, requireWellFormed: boolean, @@ -162,7 +149,6 @@ function runXmlSerializationAlgorithm( case NodeType.ELEMENT_NODE: serializeElementNode( node, - namespace, prefixMap, prefixIndex, requireWellFormed, @@ -174,7 +160,6 @@ function runXmlSerializationAlgorithm( case NodeType.DOCUMENT_NODE: serializeDocumentNode( node, - namespace, prefixMap, prefixIndex, requireWellFormed, @@ -186,9 +171,6 @@ function runXmlSerializationAlgorithm( case NodeType.COMMENT_NODE: serializeCommentNode( node, - namespace, - prefixMap, - prefixIndex, requireWellFormed, result ); @@ -201,9 +183,6 @@ function runXmlSerializationAlgorithm( case NodeType.CDATA_SECTION_NODE: serializeCDATASectionNode( node, - namespace, - prefixMap, - prefixIndex, requireWellFormed, result ); @@ -211,14 +190,13 @@ function runXmlSerializationAlgorithm( // Text: Run the algorithm for XML serializing a Text node node. case NodeType.TEXT_NODE: - serializeTextNode(node, namespace, prefixMap, prefixIndex, requireWellFormed, result); + serializeTextNode(node, requireWellFormed, result); return; // DocumentFragment: Run the algorithm for XML serializing a DocumentFragment node node. case NodeType.DOCUMENT_FRAGMENT_NODE: serializeDocumentFragmentNode( node, - namespace, prefixMap, prefixIndex, requireWellFormed, @@ -230,9 +208,6 @@ function runXmlSerializationAlgorithm( case NodeType.DOCUMENT_TYPE_NODE: serializeDocumentTypeNode( node, - namespace, - prefixMap, - prefixIndex, requireWellFormed, result ); @@ -243,9 +218,6 @@ function runXmlSerializationAlgorithm( case NodeType.PROCESSING_INSTRUCTION_NODE: serializeProcessingInstructionNode( node, - namespace, - prefixMap, - prefixIndex, requireWellFormed, result ); @@ -268,7 +240,6 @@ function runXmlSerializationAlgorithm( * 3.2.1.1 XML serializing an Element node * * @param node - The node to serialize - * @param namespace - The context namespace * @param prefixMap - The namespace prefix map * @param prefixIndex - A reference to the generated namespace prefix index * @param requireWellFormed - Determines whether the result needs to be well-formed @@ -276,7 +247,6 @@ function runXmlSerializationAlgorithm( */ function serializeElementNode( node: Node, - namespace: string | null, prefixMap: NamespacePrefixMap, prefixIndex: PrefixIndex, requireWellFormed: boolean, @@ -306,10 +276,10 @@ function serializeElementNode( let skipEndTag = false; // 5. Let ignore namespace definition attribute be a boolean flag with value false. - let ignoreNamespaceDefinitionAttribute = false; + // (alternate approach used to determine if declarations should be omitted) // 6. Given prefix map, copy a namespace prefix map and let map be the result. - const map = prefixMap.copy(); + const map = prefixMap.recordNamespaceInformation(element); // 7. Let local prefixes map be an empty map. The map has unique Node prefix strings as its // keys, with corresponding namespaceURI Node values as the map's key values (in this map, the @@ -319,186 +289,51 @@ function serializeElementNode( // enable skipping of duplicate prefix definitions when writing an element's attributes: the map // allows the algorithm to distinguish between a prefix in the namespace prefix map that might // be locally-defined (to the current Element) and one that is not. - const localPrefixesMap: LocalPrefixesMap = {}; + // (local prefixes tracked in prefixMap) // 8. Let local default namespace be the result of recording the namespace information for node // given map and local prefixes map. // NOTE: The above step will update map with any found namespace prefix definitions, add the // found prefix definitions to the local prefixes map and return a local default namespace value // defined by a default namespace attribute if one exists. Otherwise it returns null. - const localDefaultNamespace = recordNamespaceInformation(element, map, localPrefixesMap); + // (default namespace tracked in prefixMap) // 9. Let inherited ns be a copy of namespace. - let inheritedNs = namespace; + // (inherited namespace tracked in prefixMap) // 10. Let ns be the value of node's namespaceURI attribute. - const ns = element.namespaceURI; - - // 11. If inherited ns is equal to ns, then: - if (inheritedNs === ns) { - // 11.1. If local default namespace is non-null, then set ignore namespace definition - // attribute to true. - if (localDefaultNamespace !== null) { - ignoreNamespaceDefinitionAttribute = true; - } + // (unnecessary alias) - // 11.2. If ns is the XML namespace, then append to qualified name the concatenation of the - // string "xml:" and the value of node's localName. - if (ns === XML_NAMESPACE) { - qualifiedName += 'xml:' + element.localName; - } else { - // 11.3. Otherwise, append to qualified name the value of node's localName. The node's - // prefix if it exists, is dropped. - qualifiedName += element.localName; - } - // 11.4. Append the value of qualified name to markup. - result.push(qualifiedName); - } else { - // 12. Otherwise, inherited ns is not equal to ns (the node's own namespace is different - // from the context namespace of its parent). Run these sub-steps: - // 12.1. Let prefix be the value of node's prefix attribute. - let prefix = element.prefix; - - // 12.2. Let candidate prefix be the result of retrieving a preferred prefix string prefix - // from map given namespace ns. - // NOTE: The above may return null if no namespace key ns exists in map. - let candidatePrefix = map.retrievePreferredPrefixString(prefix, ns); - - // 12.3. If the value of prefix matches "xmlns", then run the following steps: - if (prefix === 'xmlns') { - // 12.3.1. If the require well-formed flag is set, then throw an error. An Element with - // prefix "xmlns" will not legally round-trip in a conforming XML parser. - if (requireWellFormed) { - throw new Error( - 'Can not serialize an element with prefix "xmlns" because it will ' + - 'not legally round-trip in a conforming XML parser.' - ); - } + // (various branches omitted as the specification contains bugs) - // 12.3.2. Let candidate prefix be the value of prefix. - candidatePrefix = prefix; - } + // 12.3. If the value of prefix matches "xmlns", then run the following steps: + // 12.3.1. If the require well-formed flag is set, then throw an error. An Element with + // prefix "xmlns" will not legally round-trip in a conforming XML parser. + if (element.prefix === 'xmlns' && requireWellFormed) { + throw new Error( + 'Can not serialize an element with prefix "xmlns" because it will ' + + 'not legally round-trip in a conforming XML parser.' + ); + } - // 12.4. Found a suitable namespace prefix: if candidate prefix is non-null (a namespace - // prefix is defined which maps to ns), then: - if (candidatePrefix !== null) { - // NOTE: The following may serialize a different prefix than the Element's existing - // prefix if it already had one. However, the retrieving a preferred prefix string - // algorithm already tried to match the existing prefix if possible. - - // 12.4.1. Append to qualified name the concatenation of candidate prefix, ":" (U+003A - // COLON), and node's localName. There exists on this node or the node's ancestry a - // namespace prefix definition that defines the node's namespace. - qualifiedName += candidatePrefix + ':' + element.localName; - - // 12.4.2. If the local default namespace is non-null (there exists a locally-defined - // default namespace declaration attribute) and its value is not the XML namespace, then - // let inherited ns get the value of local default namespace unless the local default - // namespace is the empty string in which case let it get null (the context namespace is - // changed to the declared default, rather than this node's own namespace). - // NOTE: Any default namespace definitions or namespace prefixes that define the XML - // namespace are omitted when serializing this node's attributes. - if (localDefaultNamespace !== null && localDefaultNamespace !== XML_NAMESPACE) { - inheritedNs = localDefaultNamespace === '' ? null : localDefaultNamespace; - } + const prefix = map.getPreferredPrefix(element, prefixIndex); - // 12.4.3. Append the value of qualified name to markup. - result.push(qualifiedName); - } else if (prefix !== null) { - // 12.5. Otherwise, if prefix is non-null, then: - // NOTE: By this step, there is no namespace or prefix mapping declaration in this node - // (or any parent node visited by this algorithm) that defines prefix otherwise the step - // labelled Found a suitable namespace prefix would have been followed. The sub-steps - // that follow will create a new namespace prefix declaration for prefix and ensure that - // prefix does not conflict with an existing namespace prefix declaration of the same - // localName in node's attribute list. - - // 12.5.1. If the local prefixes map contains a key matching prefix, then let prefix be - // the result of generating a prefix providing as input map, ns, and prefix index. - if (prefix in localPrefixesMap) { - prefix = generatePrefix(map, ns, prefixIndex); - } + if (prefix !== null) { + qualifiedName += `${prefix}:` + } + qualifiedName += element.localName; + result.push(qualifiedName); - // 12.5.2. Add prefix to map given namespace ns. - map.add(prefix, ns); - - // 12.5.3. Append to qualified name the concatenation of prefix, ":" (U+003A COLON), and - // node's localName. - qualifiedName += prefix + ':' + element.localName; - - // 12.5.4. Append the value of qualified name to markup. - result.push(qualifiedName); - - // 12.5.5. Append the following to markup, in the order listed: - // NOTE: The following serializes a namespace prefix declaration for prefix which was - // just added to the map. - // 12.5.5.1. " " (U+0020 SPACE); - // 12.5.5.2. The string "xmlns:"; - // 12.5.5.3. The value of prefix; - // 12.5.5.4. "="" (U+003D EQUALS SIGN, U+0022 QUOTATION MARK); - // 12.5.5.5. The result of serializing an attribute value given ns and the require - // well-formed flag as input; - // 12.5.5.6. """ (U+0022 QUOTATION MARK). - result.push( - ' xmlns:', - prefix, - '="', - serializeAttributeValue(ns, requireWellFormed), - '"' - ); + if (map.prefixToNamespace(prefix) !== element.namespaceURI) { + // We may have redeclared this prefix or default namespace + map.add(prefix, element.namespaceURI); - // 12.5.5.7. If local default namespace is non-null (there exists a locally-defined - // default namespace declaration attribute), then let inherited ns get the value of - // local default namespace unless the local default namespace is the empty string in - // which case let it get null. - if (localDefaultNamespace !== null) { - inheritedNs = localDefaultNamespace === '' ? null : localDefaultNamespace; - } - } else if ( - localDefaultNamespace === null || - (localDefaultNamespace !== null && localDefaultNamespace !== ns) - ) { - // 12.6. Otherwise, if local default namespace is null, or local default namespace is - // non-null and its value is not equal to ns, then: - // NOTE: At this point, the namespace for this node still needs to be serialized, but - // there's no prefix (or candidate prefix) availble; the following uses the default - // namespace declaration to define the namespace --optionally replacing an existing - // default declaration if present. - - // 12.6.1. Set the ignore namespace definition attribute flag to true. - ignoreNamespaceDefinitionAttribute = true; - - // 12.6.2. Append to qualified name the value of node's localName. - qualifiedName += element.localName; - - // 12.6.3. Let the value of inherited ns be ns. - // NOTE: The new default namespace will be used in the serialization to define this - // node's namespace and act as the context namespace for its children. - inheritedNs = ns; - - // 12.6.4. Append the value of qualified name to markup. - result.push(qualifiedName); - - // 12.6.5. Append the following to markup, in the order listed: - // NOTE: The following serializes the new (or replacement) default namespace definition. - // 12.6.5.1. " " (U+0020 SPACE); - // 12.6.5.2. The string "xmlns"; - // 12.6.5.3. "="" (U+003D EQUALS SIGN, U+0022 QUOTATION MARK); - // 12.6.5.4. The result of serializing an attribute value given ns and the require - // well-formed flag as input; - // 12.6.5.5. """ (U+0022 QUOTATION MARK). - result.push(' xmlns="', serializeAttributeValue(ns, requireWellFormed), '"'); + if (prefix === null) { + result.push(' xmlns="'); } else { - // 12.7. Otherwise, the node has a local default namespace that matches ns. Append to - // qualified name the value of node's localName, let the value of inherited ns be ns, - // and append the value of qualified name to markup. - qualifiedName += element.localName; - inheritedNs = ns; - result.push(qualifiedName); + result.push(' xmlns:', prefix, '="'); } - - // NOTE: All of the combinations where ns is not equal to inherited ns are handled above - // such that node will be serialized preserving its original namespaceURI. + result.push(serializeAttributeValue(element.namespaceURI, requireWellFormed), '"'); } // 13. Append to markup the result of the XML serialization of node's attributes given map, @@ -508,8 +343,6 @@ function serializeElementNode( element, map, prefixIndex, - localPrefixesMap, - ignoreNamespaceDefinitionAttribute, requireWellFormed, result ); @@ -520,7 +353,7 @@ function serializeElementNode( // "meta", "param", "source", "track", "wbr"; then append the following to markup, in the order // listed: if ( - ns === HTML_NAMESPACE && + element.namespaceURI === HTML_NAMESPACE && !element.hasChildNodes() && HTML_VOID_ELEMENTS.indexOf(element.localName) >= 0 ) { @@ -534,7 +367,7 @@ function serializeElementNode( // 15. If ns is not the HTML namespace, and the node's list of children is empty, then append // "/" (U+002F SOLIDUS) to markup and set the skip end tag flag to true. - if (ns !== HTML_NAMESPACE && !element.hasChildNodes()) { + if (element.namespaceURI !== HTML_NAMESPACE && !element.hasChildNodes()) { result.push('/'); skipEndTag = true; } @@ -562,7 +395,6 @@ function serializeElementNode( for (const child of node.childNodes) { runXmlSerializationAlgorithm( child, - inheritedNs, map, prefixIndex, requireWellFormed, @@ -599,8 +431,6 @@ function serializeAttributes( element: Element, map: NamespacePrefixMap, prefixIndex: PrefixIndex, - localPrefixesMap: LocalPrefixesMap, - ignoreNamespaceDefinitionAttribute: boolean, requireWellFormed: boolean, result: string[] ): void { @@ -618,6 +448,59 @@ function serializeAttributes( // 3. Loop: For each attribute attr in element's attributes, in the order they are specified in // the element's attribute list: for (const attr of element.attributes) { + // (various branches omitted as the specification contains bugs) + let prefix = map.getPreferredPrefix(attr, prefixIndex); + + if (attr.namespaceURI === XMLNS_NAMESPACE) { + // Namespace declaration attribute + const declaredNamespaceUri = attr.value === "" ? null : attr.value; + + // 3.5.2.2. If the require well-formed flag is set (its value is + // true), and the value of attr's value attribute matches the XMLNS + // namespace, then throw an exception; the serialization of this + // attribute would produce invalid XML because the XMLNS namespace + // is reserved and cannot be applied as an element's namespace via + // XML parsing. + // NOTE: DOM APIs do allow creation of elements in the XMLNS + // namespace but with strict qualifications. + if (requireWellFormed && attr.value === XMLNS_NAMESPACE) { + throw new Error( + 'The serialization of this attribute would produce invalid XML because ' + + 'the XMLNS namespace is reserved and cannot be applied as an ' + + "element's namespace via XML parsing." + ); + } + + // Don't declare the XML or XMLNS namespaces + if (declaredNamespaceUri === XML_NAMESPACE || declaredNamespaceUri === XMLNS_NAMESPACE) { + continue; + } + + const declaredPrefix = attr.prefix === null ? null : attr.localName; + // 3.5.2.3. If the require well-formed flag is set (its value is + // true), and the value of attr's value attribute is the empty + // string, then throw an exception; namespace prefix declarations + // cannot be used to undeclare a namespace (use a default namespace + // declaration instead). + // (we deviate from the spec here by only throwing for prefix + // declarations, the implementations of this in browsers and the + // spec text suggest that default namespace declarations should be + // allowed to reset the default namespace to null) + if (requireWellFormed && attr.prefix !== null && attr.value === '') { + throw new Error( + 'Namespace prefix declarations cannot be used to undeclare a namespace. ' + + 'Use a default namespace declaration instead.' + ); + } + + // The following does not cause an ordering issue as prefixes + // determined for attributes on the current element will not cause + // this to change. + if (!map.shouldSerializeDeclaration(declaredPrefix, declaredNamespaceUri)) { + continue; + } + } + // 3.1. If the require well-formed flag is set (its value is true), and the localname set // contains a tuple whose values match those of a new tuple consisting of attr's // namespaceURI attribute and localName attribute, then throw an exception; the @@ -638,147 +521,6 @@ function serializeAttributes( // attribute, and add it to the localname set. localNameSet.push({ namespaceURI: attr.namespaceURI, localName: attr.localName }); - // 3.3. Let attribute namespace be the value of attr's namespaceURI value. - const attributeNamespace = attr.namespaceURI; - - // 3.4. Let candidate prefix be null. - let candidatePrefix: string | null = null; - - // 3.5. If attribute namespace is non-null, then run these sub-steps: - if (attributeNamespace !== null) { - // 3.5.1. Let candidate prefix be the result of retrieving a preferred prefix string - // from map given namespace attribute namespace with preferred prefix being attr's - // prefix value. - candidatePrefix = map.retrievePreferredPrefixString(attr.prefix, attributeNamespace); - - // 3.5.2. If the value of attribute namespace is the XMLNS namespace, then run these - // steps: - if (attributeNamespace === XMLNS_NAMESPACE) { - // 3.5.2.1. If any of the following are true, then stop running these steps and goto - // Loop to visit the next attribute: - // - the attr's value is the XML namespace; - // NOTE: The XML namespace cannot be redeclared and survive round-tripping (unless - // it defines the prefix "xml"). To avoid this problem, this algorithm always - // prefixes elements in the XML namespace with "xml" and drops any related - // definitions as seen in the above condition. - if (attr.value === XML_NAMESPACE) { - continue; - } - - // - the attr's prefix is null and the ignore namespace definition attribute flag is - // true (the Element's default namespace attribute should be skipped); - if (attr.prefix === null && ignoreNamespaceDefinitionAttribute) { - continue; - } - - // - the attr's prefix is non-null and either - // - the attr's localName is not a key contained in the local prefixes map, or - // - the attr's localName is present in the local prefixes map but the value of - // the key does not match attr's value - if ( - attr.prefix !== null && - (!(attr.localName in localPrefixesMap) || - localPrefixesMap[attr.localName] !== attr.value) - ) { - // and furthermore that the attr's localName (as the prefix to find) is found in - // the namespace prefix map given the namespace consisting of the attr's value - // (the current namespace prefix definition was exactly defined previously--on - // an ancestor element not the current element whose attributes are being - // processed). - // (the only ways that this xmlns:* attribute can be omitted from the - // localPrefixesMap is if it is either the XML namespace (control flow would not - // reach this point), or if it was defined on an ancestor (and is therefore - // certainly in the map). This last condition seems to be a duplicate attempt to - // prevent repeated declarations in the spec, which is already prevented by the - // check in recordNamespaceInformation.) - continue; - } - - // 3.5.2.2. If the require well-formed flag is set (its value is true), and the - // value of attr's value attribute matches the XMLNS namespace, then throw an - // exception; the serialization of this attribute would produce invalid XML because - // the XMLNS namespace is reserved and cannot be applied as an element's namespace - // via XML parsing. - // NOTE: DOM APIs do allow creation of elements in the XMLNS namespace but with - // strict qualifications. - if (requireWellFormed && attr.value === XMLNS_NAMESPACE) { - throw new Error( - 'The serialization of this attribute would produce invalid XML because ' + - 'the XMLNS namespace is reserved and cannot be applied as an ' + - "element's namespace via XML parsing." - ); - } - - // 3.5.2.3. If the require well-formed flag is set (its value is true), and the - // value of attr's value attribute is the empty string, then throw an exception; - // namespace prefix declarations cannot be used to undeclare a namespace (use a - // default namespace declaration instead). - // (we deviate from the spec here by only throwing for prefix declarations, the - // implementations of this in browsers and the spec text suggest that default - // namespace declarations should be allowed to reset the default namespace to null) - if (requireWellFormed && attr.prefix !== null && attr.value === '') { - throw new Error( - 'Namespace prefix declarations cannot be used to undeclare a namespace. ' + - 'Use a default namespace declaration instead.' - ); - } - - // 3.5.2.4. the attr's prefix matches the string "xmlns", then let candidate prefix - // be the string "xmlns". - if (attr.prefix === 'xmlns') { - candidatePrefix = 'xmlns'; - } - } else { - // 3.5.3. Otherwise, the attribute namespace in not the XMLNS namespace. Run these - // steps: - // (interpreting this as a typo in the spec: "in" should probably have been "is") - - // (We need to deviate from the spec here, as implementing as specified would always - // generate prefixes for all namespaced attributes. Instead, first check if no valid - // candidate prefix was found in the steps above.) - if (candidatePrefix === null) { - // (Again, we need to deviate from the spec to make sure we prefer the attr's - // own prefix over a generated prefix where that would not conflict with - // existing definitions.) - if (attr.prefix === null || attr.prefix in localPrefixesMap) { - // 3.5.3.1. Let candidate prefix be the result of generating a prefix - // providing map, attribute namespace, and prefix index as input. - candidatePrefix = generatePrefix(map, attributeNamespace, prefixIndex); - } else { - candidatePrefix = attr.prefix; - } - - // Update the local and aggregate prefixes to account for the new declaration. - map.add(candidatePrefix, attr.namespaceURI); - localPrefixesMap[candidatePrefix] = attr.namespaceURI; - - // 3.5.3.2. Append the following to result, in the order listed: - // 3.5.3.2.1. " " (U+0020 SPACE); - // 3.5.3.2.2. The string "xmlns:"; - // 3.5.3.2.3. The value of candidate prefix; - // 3.5.3.2.4. "="" (U+003D EQUALS SIGN, U+0022 QUOTATION MARK); - // 3.5.3.2.5. The result of serializing an attribute value given attribute - // namespace and the require well-formed flag as input; - // 3.5.3.2.7. """ (U+0022 QUOTATION MARK). - result.push( - ' xmlns:', - candidatePrefix, - '="', - serializeAttributeValue(attributeNamespace, requireWellFormed), - '"' - ); - } - } - } - // 3.6. Append a " " (U+0020 SPACE) to result. - result.push(' '); - - // 3.7. If candidate prefix is non-null, then append to result the concatenation of - // candidate prefix with ":" (U+003A COLON). - if (candidatePrefix !== null) { - result.push(candidatePrefix, ':'); - } - // 3.8. If the require well-formed flag is set (its value is true), and this attr's // localName attribute contains the character ":" (U+003A COLON) or does not match the XML // Name production or equals "xmlns" and attribute namespace is null, then throw an @@ -787,19 +529,23 @@ function serializeAttributes( requireWellFormed && (attr.localName.indexOf(':') >= 0 || !matchesNameProduction(attr.localName) || - (attr.localName === 'xmlns' && attributeNamespace === null)) + (attr.localName === 'xmlns' && attr.namespaceURI === null)) ) { throw new Error( `Can not serialize an attribute because the localName "${attr.localName}" is not allowed.` ); } - // 3.9. Append the following strings to result, in the order listed: - // 3.9.1. The value of attr's localName; - // 3.9.2. "="" (U+003D EQUALS SIGN, U+0022 QUOTATION MARK); - // 3.9.3. The result of serializing an attribute value given attr's value attribute and the - // require well-formed flag as input; - // 3.9.4. """ (U+0022 QUOTATION MARK). + // Do we need a declaration? + if (prefix !== null && map.prefixToNamespace(prefix) !== attr.namespaceURI) { + result.push(' xmlns:', prefix, '="', serializeAttributeValue(attr.namespaceURI, requireWellFormed), '"'); + map.add(prefix, attr.namespaceURI); + } + + result.push(' '); + if (prefix !== null) { + result.push(prefix, ':'); + } result.push( attr.localName, '="', @@ -863,42 +609,10 @@ function serializeAttributeValue( // requirement in the XML specification's AttValue production by also replacing ">" characters. } -// 3.2.1.1.4 Generating namespace prefixes - -/** - * To generate a prefix given a namespace prefix map map, a string new namespace, and a reference to - * a generated namespace prefix index prefix index, the user agent must run the following steps: - * - * @param map - The namespace prefix map - * @param newNamespace - The new namespace to generate a prefix for - * @param prefixIndex - The reference to the generated namespace prefix index - * - * @returns The generated prefix for the new namespace - */ -function generatePrefix( - map: NamespacePrefixMap, - newNamespace: string | null, - prefixIndex: PrefixIndex -): string { - // 1. Let generated prefix be the concatenation of the string "ns" and the current numerical - // value of prefix index. - const generatedPrefix = 'ns' + prefixIndex.value; - - // 2. Let the value of prefix index be incremented by one. - prefixIndex.value += 1; - - // 3. Add to map the generated prefix given the new namespace namespace. - map.add(generatedPrefix, newNamespace); - - // 4. Return the value of generated prefix. - return generatedPrefix; -} - /** * 3.2.1.2 XML serializing a Document node * * @param node - The node to serialize - * @param namespace - The context namespace * @param prefixMap - The namespace prefix map * @param prefixIndex - A reference to the generated namespace prefix index * @param requireWellFormed - Determines whether the result needs to be well-formed @@ -906,7 +620,6 @@ function generatePrefix( */ function serializeDocumentNode( node: Node, - namespace: string | null, prefixMap: NamespacePrefixMap, prefixIndex: PrefixIndex, requireWellFormed: boolean, @@ -933,7 +646,6 @@ function serializeDocumentNode( for (const child of document.childNodes) { runXmlSerializationAlgorithm( child, - namespace, prefixMap, prefixIndex, requireWellFormed, @@ -948,17 +660,11 @@ function serializeDocumentNode( * 3.2.1.3 XML serializing a Comment node * * @param node - The node to serialize - * @param namespace - The context namespace - * @param prefixMap - The namespace prefix map - * @param prefixIndex - A reference to the generated namespace prefix index * @param requireWellFormed - Determines whether the result needs to be well-formed * @param result - Array of strings in which to construct the result */ function serializeCommentNode( node: Node, - namespace: string | null, - prefixMap: NamespacePrefixMap, - prefixIndex: PrefixIndex, requireWellFormed: boolean, result: string[] ): void { @@ -984,17 +690,11 @@ function serializeCommentNode( * (not currently in spec) XML serializing a CDATASection node * * @param node - The node to serialize - * @param namespace - The context namespace - * @param prefixMap - The namespace prefix map - * @param prefixIndex - A reference to the generated namespace prefix index * @param requireWellFormed - Determines whether the result needs to be well-formed * @param result - Array of strings in which to construct the result */ function serializeCDATASectionNode( node: Node, - namespace: string | null, - prefixMap: NamespacePrefixMap, - prefixIndex: PrefixIndex, requireWellFormed: boolean, result: string[] ): void { @@ -1010,17 +710,11 @@ function serializeCDATASectionNode( * 3.2.1.4 XML serializing a Text node * * @param node - The node to serialize - * @param namespace - The context namespace - * @param prefixMap - The namespace prefix map - * @param prefixIndex - A reference to the generated namespace prefix index * @param requireWellFormed - Determines whether the result needs to be well-formed * @param result - Array of strings in which to construct the result */ function serializeTextNode( node: Node, - namespace: string | null, - prefixMap: NamespacePrefixMap, - prefixIndex: PrefixIndex, requireWellFormed: boolean, result: string[] ): void { @@ -1052,7 +746,6 @@ function serializeTextNode( * 3.2.1.5 XML serializing a DocumentFragment node * * @param node - The node to serialize - * @param namespace - The context namespace * @param prefixMap - The namespace prefix map * @param prefixIndex - A reference to the generated namespace prefix index * @param requireWellFormed - Determines whether the result needs to be well-formed @@ -1060,7 +753,6 @@ function serializeTextNode( */ function serializeDocumentFragmentNode( node: Node, - namespace: string | null, prefixMap: NamespacePrefixMap, prefixIndex: PrefixIndex, requireWellFormed: boolean, @@ -1075,7 +767,6 @@ function serializeDocumentFragmentNode( for (const child of node.childNodes) { runXmlSerializationAlgorithm( child, - namespace, prefixMap, prefixIndex, requireWellFormed, @@ -1090,17 +781,11 @@ function serializeDocumentFragmentNode( * 3.2.1.6 XML serializing a DocumentType node * * @param node - The node to serialize - * @param namespace - The context namespace - * @param prefixMap - The namespace prefix map - * @param prefixIndex - A reference to the generated namespace prefix index * @param requireWellFormed - Determines whether the result needs to be well-formed * @param result - Array of strings in which to construct the result */ function serializeDocumentTypeNode( node: Node, - namespace: string | null, - prefixMap: NamespacePrefixMap, - prefixIndex: PrefixIndex, requireWellFormed: boolean, result: string[] ): void { @@ -1182,17 +867,11 @@ function serializeDocumentTypeNode( * 3.2.1.7 XML serializing a ProcessingInstruction node * * @param node - The node to serialize - * @param namespace - The context namespace - * @param prefixMap - The namespace prefix map - * @param prefixIndex - A reference to the generated namespace prefix index * @param requireWellFormed - Determines whether the result needs to be well-formed * @param result - Array of strings in which to construct the result */ function serializeProcessingInstructionNode( node: Node, - namespace: string | null, - prefixMap: NamespacePrefixMap, - prefixIndex: PrefixIndex, requireWellFormed: boolean, result: string[] ): void { diff --git a/test/dom-parsing/XMLSerializer.tests.ts b/test/dom-parsing/XMLSerializer.tests.ts index 6bdffe5..1d14875 100644 --- a/test/dom-parsing/XMLSerializer.tests.ts +++ b/test/dom-parsing/XMLSerializer.tests.ts @@ -107,18 +107,20 @@ describe('XMLSerializer', () => { ); }); - it('retains null default namespace definitions on prefixed elements', () => { + it('retains declarations that reset the default namespace to null on prefixed elements', () => { + const root = document.createElementNS('http://www.example.com/root-ns', 'root'); const el = document.createElementNS('http://www.example.com/ns', 'prf:test'); + root.appendChild(el); el.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', ''); const child = document.createElementNS('http://www.example.com/ns', 'prf:child'); child.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', ''); el.appendChild(child); - expect(serializer.serializeToString(el)).toBe( - '' + expect(serializer.serializeToString(root)).toBe( + '' ); }); - it('ignores useless prefix but not default definitions if elements are prefixed', () => { + it('ignores useless repeated declarations', () => { const el = document.createElementNS('http://www.example.com/ns', 'prf:test'); el.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:prf', 'http://www.example.com/ns'); el.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', 'http://www.example.com/ns2'); @@ -127,7 +129,7 @@ describe('XMLSerializer', () => { child.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', 'http://www.example.com/ns2'); el.appendChild(child); expect(serializer.serializeToString(el)).toBe( - '' + '' ); }); @@ -164,7 +166,7 @@ describe('XMLSerializer', () => { const el = document.createElementNS('http://www.example.com/ns', 'prf:test'); el.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:prf', 'http://www.example.com/ns2'); expect(serializer.serializeToString(el)).toBe( - '' + '' ); }); @@ -478,9 +480,28 @@ describe('serializeToWellFormedString', () => { const root = document.appendChild(document.createElementNS('ns_root', 'root')); const child = root.appendChild(document.createElementNS('ns_child', 'p:child')); child.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', ''); - const grandChild = child.appendChild(document.createElementNS(null, 'grandchild')); + child.appendChild(document.createElementNS(null, 'grandchild')); expect(slimdom.serializeToWellFormedString(document)).toBe( '' ); }); + + it('is not affected by spec bug: redefined prefix confusion', () => { + // see https://github.com/w3c/DOM-Parsing/issues/75 + const root = document.appendChild(document.createElementNS('ns1', 'pre:root')); + const child = root.appendChild(document.createElementNS('ns2', 'pre:child')); + child.appendChild(document.createElementNS('ns1', 'pre:grandChild')); + expect(slimdom.serializeToWellFormedString(document)).toBe( + '' + ); + }); + + it('is not affected by spec bug: generated prefix collisions', () => { + // see https://github.com/w3c/DOM-Parsing/issues/75 + const root = document.appendChild(document.createElementNS('ns1', 'ns1:root')); + root.setAttributeNS('ns2', 'attr', 'value'); + expect(slimdom.serializeToWellFormedString(document)).toMatchInlineSnapshot( + `""` + ); + }); });