Serenity Operating System
1/*
2 * Copyright (c) 2022, Luke Wilde <lukew@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <LibWeb/DOM/CDATASection.h>
8#include <LibWeb/DOM/Comment.h>
9#include <LibWeb/DOM/Document.h>
10#include <LibWeb/DOM/DocumentFragment.h>
11#include <LibWeb/DOM/DocumentType.h>
12#include <LibWeb/DOM/Element.h>
13#include <LibWeb/DOM/Node.h>
14#include <LibWeb/DOM/ProcessingInstruction.h>
15#include <LibWeb/DOM/Text.h>
16#include <LibWeb/DOMParsing/XMLSerializer.h>
17#include <LibWeb/HTML/HTMLTemplateElement.h>
18#include <LibWeb/Infra/Strings.h>
19#include <LibWeb/Namespace.h>
20#include <LibWeb/WebIDL/ExceptionOr.h>
21
22namespace Web::DOMParsing {
23
24WebIDL::ExceptionOr<JS::NonnullGCPtr<XMLSerializer>> XMLSerializer::construct_impl(JS::Realm& realm)
25{
26 return MUST_OR_THROW_OOM(realm.heap().allocate<XMLSerializer>(realm, realm));
27}
28
29XMLSerializer::XMLSerializer(JS::Realm& realm)
30 : PlatformObject(realm)
31{
32}
33
34XMLSerializer::~XMLSerializer() = default;
35
36JS::ThrowCompletionOr<void> XMLSerializer::initialize(JS::Realm& realm)
37{
38 MUST_OR_THROW_OOM(Base::initialize(realm));
39 set_prototype(&Bindings::ensure_web_prototype<Bindings::XMLSerializerPrototype>(realm, "XMLSerializer"));
40
41 return {};
42}
43
44// https://w3c.github.io/DOM-Parsing/#dom-xmlserializer-serializetostring
45WebIDL::ExceptionOr<DeprecatedString> XMLSerializer::serialize_to_string(JS::NonnullGCPtr<DOM::Node const> root)
46{
47 // The serializeToString(root) method must produce an XML serialization of root passing a value of false for the require well-formed parameter, and return the result.
48 return serialize_node_to_xml_string(root, RequireWellFormed::No);
49}
50
51// https://w3c.github.io/DOM-Parsing/#dfn-add
52static void add_prefix_to_namespace_prefix_map(HashMap<DeprecatedFlyString, Vector<DeprecatedString>>& prefix_map, DeprecatedString const& prefix, DeprecatedFlyString const& namespace_)
53{
54 // 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.
55 auto candidates_list_iterator = prefix_map.find(namespace_);
56
57 // 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.
58 if (candidates_list_iterator == prefix_map.end()) {
59 Vector<DeprecatedString> new_list;
60 new_list.append(prefix);
61 prefix_map.set(namespace_, move(new_list));
62 return;
63 }
64
65 // 3. Otherwise, append prefix to the end of candidates list.
66 candidates_list_iterator->value.append(prefix);
67}
68
69// https://w3c.github.io/DOM-Parsing/#dfn-retrieving-a-preferred-prefix-string
70static Optional<DeprecatedString> retrieve_a_preferred_prefix_string(DeprecatedString const& preferred_prefix, HashMap<DeprecatedFlyString, Vector<DeprecatedString>> const& namespace_prefix_map, DeprecatedFlyString const& namespace_)
71{
72 // 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,
73 // then stop running these steps, and return the null value.
74 auto candidates_list_iterator = namespace_prefix_map.find(namespace_);
75 if (candidates_list_iterator == namespace_prefix_map.end())
76 return {};
77
78 // 2. Otherwise, for each prefix value prefix in candidates list, iterating from beginning to end:
79 for (size_t prefix_index = 0; prefix_index < candidates_list_iterator->value.size(); ++prefix_index) {
80 auto const& prefix = candidates_list_iterator->value.at(prefix_index);
81
82 // 1. If prefix matches preferred prefix, then stop running these steps and return prefix.
83 if (prefix == preferred_prefix)
84 return prefix;
85
86 // 2. If prefix is the last item in the candidates list, then stop running these steps and return prefix.
87 if (prefix_index == candidates_list_iterator->value.size() - 1)
88 return prefix;
89 }
90
91 // Spec Note: There will always be at least one prefix value in the list.
92 VERIFY_NOT_REACHED();
93}
94
95// https://w3c.github.io/DOM-Parsing/#dfn-generating-a-prefix
96static DeprecatedString generate_a_prefix(HashMap<DeprecatedFlyString, Vector<DeprecatedString>>& namespace_prefix_map, DeprecatedFlyString const& new_namespace, u64& prefix_index)
97{
98 // 1. Let generated prefix be the concatenation of the string "ns" and the current numerical value of prefix index.
99 auto generated_prefix = DeprecatedString::formatted("ns{}", prefix_index);
100
101 // 2. Let the value of prefix index be incremented by one.
102 ++prefix_index;
103
104 // 3. Add to map the generated prefix given the new namespace namespace.
105 add_prefix_to_namespace_prefix_map(namespace_prefix_map, generated_prefix, new_namespace);
106
107 // 4. Return the value of generated prefix.
108 return generated_prefix;
109}
110
111// https://w3c.github.io/DOM-Parsing/#dfn-found
112static bool prefix_is_in_prefix_map(DeprecatedString const& prefix, HashMap<DeprecatedFlyString, Vector<DeprecatedString>> const& namespace_prefix_map, DeprecatedFlyString const& namespace_)
113{
114 // 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
115 // or if there is no such key, then stop running these steps, and return false.
116 auto candidates_list_iterator = namespace_prefix_map.find(namespace_);
117 if (candidates_list_iterator == namespace_prefix_map.end())
118 return false;
119
120 // 2. If the value of prefix occurs at least once in candidates list, return true, otherwise return false.
121 return candidates_list_iterator->value.contains_slow(prefix);
122}
123
124WebIDL::ExceptionOr<DeprecatedString> serialize_node_to_xml_string_impl(JS::NonnullGCPtr<DOM::Node const> root, Optional<DeprecatedFlyString>& namespace_, HashMap<DeprecatedFlyString, Vector<DeprecatedString>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed);
125
126// https://w3c.github.io/DOM-Parsing/#dfn-xml-serialization
127WebIDL::ExceptionOr<DeprecatedString> serialize_node_to_xml_string(JS::NonnullGCPtr<DOM::Node const> root, RequireWellFormed require_well_formed)
128{
129 // 1. Let namespace be a context namespace with value null. The context namespace tracks the XML serialization algorithm's current default namespace.
130 // The context namespace is changed when either an Element Node has a default namespace declaration, or the algorithm generates a default namespace declaration
131 // for the Element Node to match its own namespace. The algorithm assumes no namespace (null) to start.
132 Optional<DeprecatedFlyString> namespace_;
133
134 // 2. Let prefix map be a new namespace prefix map.
135 HashMap<DeprecatedFlyString, Vector<DeprecatedString>> prefix_map;
136
137 // 3. Add the XML namespace with prefix value "xml" to prefix map.
138 add_prefix_to_namespace_prefix_map(prefix_map, "xml"sv, Namespace::XML);
139
140 // 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
141 // when no suitable existing namespace prefix is available to serialize a node's namespaceURI (or the namespaceURI of one of node's attributes).
142 u64 prefix_index = 1;
143
144 // 5. Return the result of running the XML serialization algorithm on node passing the context namespace namespace, namespace prefix map prefix map,
145 // generated namespace prefix index reference to prefix index, and the flag require well-formed. If an exception occurs during the execution of the algorithm,
146 // then catch that exception and throw an "InvalidStateError" DOMException.
147 // NOTE: InvalidStateError exceptions will be created when needed, as this also allows us to have a specific error message for the exception.
148 return serialize_node_to_xml_string_impl(root, namespace_, prefix_map, prefix_index, require_well_formed);
149}
150
151static WebIDL::ExceptionOr<DeprecatedString> serialize_element(DOM::Element const& element, Optional<DeprecatedFlyString>& namespace_, HashMap<DeprecatedFlyString, Vector<DeprecatedString>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed);
152static WebIDL::ExceptionOr<DeprecatedString> serialize_document(DOM::Document const& document, Optional<DeprecatedFlyString>& namespace_, HashMap<DeprecatedFlyString, Vector<DeprecatedString>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed);
153static WebIDL::ExceptionOr<DeprecatedString> serialize_comment(DOM::Comment const& comment, RequireWellFormed require_well_formed);
154static WebIDL::ExceptionOr<DeprecatedString> serialize_text(DOM::Text const& text, RequireWellFormed require_well_formed);
155static WebIDL::ExceptionOr<DeprecatedString> serialize_document_fragment(DOM::DocumentFragment const& document_fragment, Optional<DeprecatedFlyString>& namespace_, HashMap<DeprecatedFlyString, Vector<DeprecatedString>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed);
156static WebIDL::ExceptionOr<DeprecatedString> serialize_document_type(DOM::DocumentType const& document_type, RequireWellFormed require_well_formed);
157static WebIDL::ExceptionOr<DeprecatedString> serialize_processing_instruction(DOM::ProcessingInstruction const& processing_instruction, RequireWellFormed require_well_formed);
158
159// https://w3c.github.io/DOM-Parsing/#dfn-xml-serialization-algorithm
160WebIDL::ExceptionOr<DeprecatedString> serialize_node_to_xml_string_impl(JS::NonnullGCPtr<DOM::Node const> root, Optional<DeprecatedFlyString>& namespace_, HashMap<DeprecatedFlyString, Vector<DeprecatedString>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed)
161{
162 // Each of the following algorithms for producing an XML serialization of a DOM node take as input a node to serialize and the following arguments:
163 // - A context namespace namespace
164 // - A namespace prefix map prefix map
165 // - A generated namespace prefix index prefix index
166 // - The require well-formed flag
167
168 // The XML serialization algorithm produces an XML serialization of an arbitrary DOM node node based on the node's interface type.
169 // Each referenced algorithm is to be passed the arguments as they were received by the caller and return their result to the caller.
170 // Re-throw any exceptions.
171 // If node's interface is:
172
173 if (is<DOM::Element>(*root)) {
174 // -> Element
175 // Run the algorithm for XML serializing an Element node node.
176 return serialize_element(static_cast<DOM::Element const&>(*root), namespace_, namespace_prefix_map, prefix_index, require_well_formed);
177 }
178
179 if (is<DOM::Document>(*root)) {
180 // -> Document
181 // Run the algorithm for XML serializing a Document node node.
182 return serialize_document(static_cast<DOM::Document const&>(*root), namespace_, namespace_prefix_map, prefix_index, require_well_formed);
183 }
184
185 if (is<DOM::Comment>(*root)) {
186 // -> Comment
187 // Run the algorithm for XML serializing a Comment node node.
188 return serialize_comment(static_cast<DOM::Comment const&>(*root), require_well_formed);
189 }
190
191 if (is<DOM::Text>(*root) || is<DOM::CDATASection>(*root)) {
192 // -> Text
193 // Run the algorithm for XML serializing a Text node node.
194 return serialize_text(static_cast<DOM::Text const&>(*root), require_well_formed);
195 }
196
197 if (is<DOM::DocumentFragment>(*root)) {
198 // -> DocumentFragment
199 // Run the algorithm for XML serializing a DocumentFragment node node.
200 return serialize_document_fragment(static_cast<DOM::DocumentFragment const&>(*root), namespace_, namespace_prefix_map, prefix_index, require_well_formed);
201 }
202
203 if (is<DOM::DocumentType>(*root)) {
204 // -> DocumentType
205 // Run the algorithm for XML serializing a DocumentType node node.
206 return serialize_document_type(static_cast<DOM::DocumentType const&>(*root), require_well_formed);
207 }
208
209 if (is<DOM::ProcessingInstruction>(*root)) {
210 // -> ProcessingInstruction
211 // Run the algorithm for XML serializing a ProcessingInstruction node node.
212 return serialize_processing_instruction(static_cast<DOM::ProcessingInstruction const&>(*root), require_well_formed);
213 }
214
215 if (is<DOM::Attr>(*root)) {
216 // -> An Attr object
217 // Return an empty string.
218 return DeprecatedString::empty();
219 }
220
221 // -> Anything else
222 // Throw a TypeError. Only Nodes and Attr objects can be serialized by this algorithm.
223 return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Can only serialize Nodes or Attributes."sv };
224}
225
226// https://w3c.github.io/DOM-Parsing/#dfn-recording-the-namespace-information
227static Optional<DeprecatedString> record_namespace_information(DOM::Element const& element, HashMap<DeprecatedFlyString, Vector<DeprecatedString>>& namespace_prefix_map, HashMap<DeprecatedString, DeprecatedString>& local_prefix_map)
228{
229 // 1. Let default namespace attr value be null.
230 Optional<DeprecatedString> default_namespace_attribute_value;
231
232 // 2. Main: For each attribute attr in element's attributes, in the order they are specified in the element's attribute list:
233 for (size_t attribute_index = 0; attribute_index < element.attributes()->length(); ++attribute_index) {
234 auto const* attribute = element.attributes()->item(attribute_index);
235 VERIFY(attribute);
236
237 // 1. Let attribute namespace be the value of attr's namespaceURI value.
238 auto const& attribute_namespace = attribute->namespace_uri();
239
240 // 2. Let attribute prefix be the value of attr's prefix.
241 auto const& attribute_prefix = attribute->prefix();
242
243 // 3. If the attribute namespace is the XMLNS namespace, then:
244 if (attribute_namespace == Namespace::XMLNS) {
245 // 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,
246 // returning to Main to visit the next attribute.
247 if (attribute_prefix.is_null()) {
248 default_namespace_attribute_value = attribute->value();
249 continue;
250 }
251
252 // 2. Otherwise, the attribute prefix is not null and attr is a namespace prefix definition. Run the following steps:
253 // 1. Let prefix definition be the value of attr's localName.
254 auto const& prefix_definition = attribute->local_name();
255
256 // 2. Let namespace definition be the value of attr's value.
257 auto namespace_definition = attribute->value();
258
259 // 3. If namespace definition is the XML namespace, then stop running these steps, and return to Main to visit the next attribute.
260 if (namespace_definition == Namespace::XML)
261 continue;
262
263 // 4. If namespace definition is the empty string (the declarative form of having no namespace), then let namespace definition be null instead.
264 if (namespace_definition.is_empty())
265 namespace_definition = {};
266
267 // 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.
268 if (prefix_is_in_prefix_map(prefix_definition, namespace_prefix_map, namespace_definition))
269 continue;
270
271 // 6. Add the prefix prefix definition to map given namespace namespace definition.
272 add_prefix_to_namespace_prefix_map(namespace_prefix_map, prefix_definition, namespace_definition);
273
274 // 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.
275 local_prefix_map.set(prefix_definition, namespace_definition.is_null() ? DeprecatedString::empty() : namespace_definition);
276 }
277 }
278
279 // 3. Return the value of default namespace attr value.
280 return default_namespace_attribute_value;
281}
282
283// https://w3c.github.io/DOM-Parsing/#dfn-serializing-an-attribute-value
284static WebIDL::ExceptionOr<DeprecatedString> serialize_an_attribute_value(DeprecatedString const& attribute_value, [[maybe_unused]] RequireWellFormed require_well_formed)
285{
286 // FIXME: 1. If the require well-formed flag is set (its value is true), and attribute value contains characters that are not matched by the XML Char production,
287 // then throw an exception; the serialization of this attribute value would fail to produce a well-formed element serialization.
288
289 // 2. If attribute value is null, then return the empty string.
290 if (attribute_value.is_null())
291 return DeprecatedString::empty();
292
293 // 3. Otherwise, attribute value is a string. Return the value of attribute value, first replacing any occurrences of the following:
294 auto final_attribute_value = attribute_value;
295
296 // 1. "&" with "&"
297 final_attribute_value = final_attribute_value.replace("&"sv, "&"sv, ReplaceMode::All);
298
299 // 2. """ with """
300 final_attribute_value = final_attribute_value.replace("\""sv, """sv, ReplaceMode::All);
301
302 // 3. "<" with "<"
303 final_attribute_value = final_attribute_value.replace("<"sv, "<"sv, ReplaceMode::All);
304
305 // 4. ">" with ">"
306 final_attribute_value = final_attribute_value.replace(">"sv, ">"sv, ReplaceMode::All);
307
308 return final_attribute_value;
309}
310
311struct LocalNameSetEntry {
312 DeprecatedString namespace_uri;
313 DeprecatedString local_name;
314};
315
316// https://w3c.github.io/DOM-Parsing/#dfn-xml-serialization-of-the-attributes
317static WebIDL::ExceptionOr<DeprecatedString> serialize_element_attributes(DOM::Element const& element, HashMap<DeprecatedFlyString, Vector<DeprecatedString>>& namespace_prefix_map, u64& prefix_index, HashMap<DeprecatedString, DeprecatedString> const& local_prefixes_map, bool ignore_namespace_definition_attribute, RequireWellFormed require_well_formed)
318{
319 auto& realm = element.realm();
320
321 // 1. Let result be the empty string.
322 StringBuilder result;
323
324 // 2. Let localname set be a new empty namespace localname set. This localname set will contain tuples of unique attribute namespaceURI and localName pairs, and is populated as each attr is processed.
325 // Spec Note: This set is used to [optionally] enforce the well-formed constraint that an element cannot have two attributes with the same namespaceURI and localName.
326 // This can occur when two otherwise identical attributes on the same element differ only by their prefix values.
327 Vector<LocalNameSetEntry> local_name_set;
328
329 // 3. Loop: For each attribute attr in element's attributes, in the order they are specified in the element's attribute list:
330 for (size_t attribute_index = 0; attribute_index < element.attributes()->length(); ++attribute_index) {
331 auto const* attribute = element.attributes()->item(attribute_index);
332 VERIFY(attribute);
333
334 // 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,
335 // then throw an exception; the serialization of this attr would fail to produce a well-formed element serialization.
336 if (require_well_formed == RequireWellFormed::Yes) {
337 auto local_name_set_iterator = local_name_set.find_if([&attribute](LocalNameSetEntry const& entry) {
338 return entry.namespace_uri == attribute->namespace_uri() && entry.local_name == attribute->local_name();
339 });
340
341 if (local_name_set_iterator != local_name_set.end())
342 return WebIDL::InvalidStateError::create(realm, "Element contains two attributes with identical namespaces and local names");
343 }
344
345 // 2. Create a new tuple consisting of attr's namespaceURI attribute and localName attribute, and add it to the localname set.
346 LocalNameSetEntry new_local_name_set_entry {
347 .namespace_uri = attribute->namespace_uri(),
348 .local_name = attribute->local_name(),
349 };
350
351 local_name_set.append(move(new_local_name_set_entry));
352
353 // 3. Let attribute namespace be the value of attr's namespaceURI value.
354 auto const& attribute_namespace = attribute->namespace_uri();
355
356 // 4. Let candidate prefix be null.
357 Optional<DeprecatedString> candidate_prefix;
358
359 // 5. If attribute namespace is not null, then run these sub-steps:
360 if (!attribute_namespace.is_null()) {
361 // 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.
362 candidate_prefix = retrieve_a_preferred_prefix_string(attribute->prefix(), namespace_prefix_map, attribute_namespace);
363
364 // 2. If the value of attribute namespace is the XMLNS namespace, then run these steps:
365 if (attribute_namespace == Namespace::XMLNS) {
366 // 1. If any of the following are true, then stop running these steps and goto Loop to visit the next attribute:
367 // - the attr's value is the XML namespace;
368 if (attribute->value() == Namespace::XML)
369 continue;
370
371 // - the attr's prefix is null and the ignore namespace definition attribute flag is true (the Element's default namespace attribute should be skipped);
372 if (attribute->prefix().is_null() && ignore_namespace_definition_attribute)
373 continue;
374
375 // - the attr's prefix is not null and either
376 if (!attribute->prefix().is_null()) {
377 // - the attr's localName is not a key contained in the local prefixes map, or
378 auto name_in_local_prefix_map_iterator = local_prefixes_map.find(attribute->local_name());
379 if (name_in_local_prefix_map_iterator == local_prefixes_map.end())
380 continue;
381
382 // - the attr's localName is present in the local prefixes map but the value of the key does not match attr's value
383 if (name_in_local_prefix_map_iterator->value != attribute->value())
384 continue;
385 }
386
387 // 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
388 // (the current namespace prefix definition was exactly defined previously--on an ancestor element not the current element whose attributes are being processed).
389 if (prefix_is_in_prefix_map(attribute->local_name(), namespace_prefix_map, attribute->value()))
390 continue;
391
392 // 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,
393 // 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.
394 if (require_well_formed == RequireWellFormed::Yes && attribute->value() == Namespace::XMLNS)
395 return WebIDL::InvalidStateError::create(realm, "The XMLNS namespace cannot be used as an element's namespace");
396
397 // 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,
398 // then throw an exception; namespace prefix declarations cannot be used to undeclare a namespace (use a default namespace declaration instead).
399 if (require_well_formed == RequireWellFormed::Yes && attribute->value().is_empty())
400 return WebIDL::InvalidStateError::create(realm, "Attribute's value is empty");
401
402 // 4. [If] the attr's prefix matches the string "xmlns", then let candidate prefix be the string "xmlns".
403 if (attribute->prefix() == "xmlns"sv)
404 candidate_prefix = "xmlns"sv;
405 }
406
407 // 3. Otherwise, the attribute namespace in not the XMLNS namespace. Run these steps:
408 else {
409 // 1. Let candidate prefix be the result of generating a prefix providing map, attribute namespace, and prefix index as input.
410 candidate_prefix = generate_a_prefix(namespace_prefix_map, attribute_namespace, prefix_index);
411
412 // 2. Append the following to result, in the order listed:
413 // 1. " " (U+0020 SPACE);
414 // 2. The string "xmlns:";
415 result.append(" xmlns:"sv);
416
417 // 3. The value of candidate prefix;
418 VERIFY(candidate_prefix.has_value());
419 result.append(candidate_prefix.value());
420
421 // 4. "="" (U+003D EQUALS SIGN, U+0022 QUOTATION MARK);
422 result.append("=\""sv);
423
424 // 5. The result of serializing an attribute value given attribute namespace and the require well-formed flag as input
425 result.append(TRY(serialize_an_attribute_value(attribute_namespace, require_well_formed)));
426
427 // 6. """ (U+0022 QUOTATION MARK).
428 result.append('"');
429 }
430 }
431
432 // 6. Append a " " (U+0020 SPACE) to result.
433 result.append(' ');
434
435 // 7. If candidate prefix is not null, then append to result the concatenation of candidate prefix with ":" (U+003A COLON).
436 if (candidate_prefix.has_value())
437 result.appendff("{}:", candidate_prefix.value());
438
439 // 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)
440 // or does not match the XML Name production or equals "xmlns" and attribute namespace is null, then throw an exception; the serialization of this attr would not be a well-formed attribute.
441 if (require_well_formed == RequireWellFormed::Yes) {
442 if (attribute->local_name().view().contains(':'))
443 return WebIDL::InvalidStateError::create(realm, "Attribute's local name contains a colon");
444
445 // FIXME: Check attribute's local name against the XML Name production.
446
447 if (attribute->local_name() == "xmlns"sv && attribute_namespace.is_null())
448 return WebIDL::InvalidStateError::create(realm, "Attribute's local name is 'xmlns' and the attribute has no namespace");
449 }
450
451 // 9. Append the following strings to result, in the order listed:
452 // 1. The value of attr's localName;
453 result.append(attribute->local_name());
454
455 // 2. "="" (U+003D EQUALS SIGN, U+0022 QUOTATION MARK);
456 result.append("=\""sv);
457
458 // 3. The result of serializing an attribute value given attr's value attribute and the require well-formed flag as input;
459 result.append(TRY(serialize_an_attribute_value(attribute->value(), require_well_formed)));
460
461 // 4. """ (U+0022 QUOTATION MARK).
462 result.append('"');
463 }
464
465 // 4. Return the value of result.
466 return result.to_deprecated_string();
467}
468
469// https://w3c.github.io/DOM-Parsing/#xml-serializing-an-element-node
470static WebIDL::ExceptionOr<DeprecatedString> serialize_element(DOM::Element const& element, Optional<DeprecatedFlyString>& namespace_, HashMap<DeprecatedFlyString, Vector<DeprecatedString>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed)
471{
472 auto& realm = element.realm();
473
474 // 1. If the require well-formed flag is set (its value is true), and this node's localName attribute contains the character ":" (U+003A COLON) or does not match the XML Name production,
475 // then throw an exception; the serialization of this node would not be a well-formed element.
476 if (require_well_formed == RequireWellFormed::Yes) {
477 if (element.local_name().view().contains(':'))
478 return WebIDL::InvalidStateError::create(realm, "Element's local name contains a colon");
479
480 // FIXME: Check element's local name against the XML Char production.
481 }
482
483 // 2. Let markup be the string "<" (U+003C LESS-THAN SIGN).
484 StringBuilder markup;
485 markup.append('<');
486
487 // 3. Let qualified name be an empty string.
488 StringBuilder qualified_name;
489
490 // 4. Let skip end tag be a boolean flag with value false.
491 bool skip_end_tag = false;
492
493 // 5. Let ignore namespace definition attribute be a boolean flag with value false.
494 bool ignore_namespace_definition_attribute = false;
495
496 // 6. Given prefix map, copy a namespace prefix map and let map be the result.
497 HashMap<DeprecatedFlyString, Vector<DeprecatedString>> map;
498
499 // https://w3c.github.io/DOM-Parsing/#dfn-copy-a-namespace-prefix-map
500 // NOTE: This is only used here.
501 // To copy a namespace prefix map map means to copy the map's keys into a new empty namespace prefix map,
502 // and to copy each of the values in the namespace prefix list associated with each keys' value into a new list
503 // which should be associated with the respective key in the new map.
504 for (auto const& map_entry : namespace_prefix_map)
505 map.set(map_entry.key, map_entry.value);
506
507 // 7. Let local prefixes map be an empty map. The map has unique Node prefix strings as its keys, with corresponding namespaceURI Node values
508 // as the map's key values (in this map, the null namespace is represented by the empty string).
509 HashMap<DeprecatedString, DeprecatedString> local_prefixes_map;
510
511 // 8. Let local default namespace be the result of recording the namespace information for node given map and local prefixes map.
512 auto local_default_namespace = record_namespace_information(element, map, local_prefixes_map);
513
514 // 9. Let inherited ns be a copy of namespace.
515 auto inherited_ns = namespace_;
516
517 // 10. Let ns be the value of node's namespaceURI attribute.
518 auto const& ns = element.namespace_uri();
519
520 // 11. If inherited ns is equal to ns, then:
521 if (inherited_ns == ns) {
522 // 1. If local default namespace is not null, then set ignore namespace definition attribute to true.
523 if (local_default_namespace.has_value())
524 ignore_namespace_definition_attribute = true;
525
526 // 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.
527 if (ns == Namespace::XML)
528 qualified_name.appendff("xml:{}", element.local_name());
529
530 // 3. Otherwise, append to qualified name the value of node's localName.
531 else
532 qualified_name.append(element.local_name());
533
534 // 4. Append the value of qualified name to markup.
535 markup.append(qualified_name.to_deprecated_string());
536 }
537
538 // 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:
539 else {
540 // 1. Let prefix be the value of node's prefix attribute.
541 auto prefix = element.prefix();
542
543 // 2. Let candidate prefix be the result of retrieving a preferred prefix string prefix from map given namespace ns.
544 auto candidate_prefix = retrieve_a_preferred_prefix_string(prefix, map, ns);
545
546 // 3. If the value of prefix matches "xmlns", then run the following steps:
547 if (prefix == "xmlns"sv) {
548 // 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.
549 if (require_well_formed == RequireWellFormed::Yes)
550 return WebIDL::InvalidStateError::create(realm, "Elements prefix is 'xmlns'");
551
552 // 2. Let candidate prefix be the value of prefix.
553 candidate_prefix = prefix;
554 }
555
556 // 4. Found a suitable namespace prefix: if candidate prefix is not null (a namespace prefix is defined which maps to ns), then:
557 if (candidate_prefix.has_value()) {
558 // 1. Append to qualified name the concatenation of candidate prefix, ":" (U+003A COLON), and node's localName.
559 qualified_name.appendff("{}:{}", candidate_prefix.value(), element.local_name());
560
561 // 2. If the local default namespace is not null (there exists a locally-defined default namespace declaration attribute) and its value is not the XML namespace,
562 // 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
563 // (the context namespace is changed to the declared default, rather than this node's own namespace).
564 if (local_default_namespace.has_value() && local_default_namespace.value() != Namespace::XML) {
565 if (!local_default_namespace.value().is_empty())
566 inherited_ns = local_default_namespace.value();
567 else
568 inherited_ns = {};
569 }
570
571 // 3. Append the value of qualified name to markup.
572 markup.append(qualified_name.to_deprecated_string());
573 }
574
575 // 5. Otherwise, if prefix is not null, then:
576 else if (!prefix.is_null()) {
577 // 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.
578 if (local_prefixes_map.contains(prefix))
579 prefix = generate_a_prefix(map, ns, prefix_index);
580
581 // 2. Add prefix to map given namespace ns.
582 add_prefix_to_namespace_prefix_map(map, prefix, ns);
583
584 // 3. Append to qualified name the concatenation of prefix, ":" (U+003A COLON), and node's localName.
585 qualified_name.appendff("{}:{}", prefix, element.local_name());
586
587 // 4. Append the value of qualified name to markup.
588 markup.append(qualified_name.to_deprecated_string());
589
590 // 5. Append the following to markup, in the order listed:
591 // 1. " " (U+0020 SPACE);
592 // 2. The string "xmlns:";
593 markup.append(" xmlns:"sv);
594
595 // 3. The value of prefix;
596 markup.append(prefix);
597
598 // 4. "="" (U+003D EQUALS SIGN, U+0022 QUOTATION MARK);
599 markup.append("=\""sv);
600
601 // 5. The result of serializing an attribute value given ns and the require well-formed flag as input;
602 markup.append(TRY(serialize_an_attribute_value(ns, require_well_formed)));
603
604 // 6. """ (U+0022 QUOTATION MARK).
605 markup.append('"');
606
607 // 7. If local default namespace is not null (there exists a locally-defined default namespace declaration attribute),
608 // 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.
609 if (local_default_namespace.has_value()) {
610 if (!local_default_namespace.value().is_empty())
611 inherited_ns = local_default_namespace.value();
612 else
613 inherited_ns = {};
614 }
615 }
616
617 // 6. Otherwise, if local default namespace is null, or local default namespace is not null and its value is not equal to ns, then:
618 else if (!local_default_namespace.has_value() || local_default_namespace.value() != ns) {
619 // 1. Set the ignore namespace definition attribute flag to true.
620 ignore_namespace_definition_attribute = true;
621
622 // 2. Append to qualified name the value of node's localName.
623 qualified_name.append(element.local_name());
624
625 // 3. Let the value of inherited ns be ns.
626 inherited_ns = ns;
627
628 // 4. Append the value of qualified name to markup.
629 markup.append(qualified_name.to_deprecated_string());
630
631 // 5. Append the following to markup, in the order listed:
632 // 1. " " (U+0020 SPACE);
633 // 2. The string "xmlns";
634 // 3. "="" (U+003D EQUALS SIGN, U+0022 QUOTATION MARK);
635 markup.append(" xmlns=\""sv);
636
637 // 4. The result of serializing an attribute value given ns and the require well-formed flag as input;
638 markup.append(TRY(serialize_an_attribute_value(ns, require_well_formed)));
639
640 // 5. """ (U+0022 QUOTATION MARK).
641 markup.append('"');
642 }
643
644 else {
645 // 7. Otherwise, the node has a local default namespace that matches ns.
646 // 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.
647 VERIFY(local_default_namespace.has_value());
648 VERIFY(local_default_namespace.value() == ns);
649
650 qualified_name.append(element.local_name());
651 inherited_ns = ns;
652 markup.append(qualified_name.to_deprecated_string());
653 }
654 }
655
656 // 13. Append to markup the result of the XML serialization of node's attributes given map, prefix index, local prefixes map, ignore namespace definition attribute flag, and require well-formed flag.
657 markup.append(TRY(serialize_element_attributes(element, map, prefix_index, local_prefixes_map, ignore_namespace_definition_attribute, require_well_formed)));
658
659 // 14. If ns is the HTML namespace, and the node's list of children is empty, and the node's localName matches any one of the following void elements:
660 // "area", "base", "basefont", "bgsound", "br", "col", "embed", "frame", "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr";
661 // then append the following to markup, in the order listed:
662 if (ns == Namespace::HTML && !element.has_children() && element.local_name().is_one_of(HTML::TagNames::area, HTML::TagNames::area, HTML::TagNames::base, HTML::TagNames::basefont, HTML::TagNames::bgsound, HTML::TagNames::br, HTML::TagNames::col, HTML::TagNames::embed, HTML::TagNames::frame, HTML::TagNames::hr, HTML::TagNames::img, HTML::TagNames::input, HTML::TagNames::keygen, HTML::TagNames::link, HTML::TagNames::menuitem, HTML::TagNames::meta, HTML::TagNames::param, HTML::TagNames::source, HTML::TagNames::track, HTML::TagNames::wbr)) {
663 // 1. " " (U+0020 SPACE);
664 // 2. "/" (U+002F SOLIDUS).
665 markup.append(" /"sv);
666
667 // and set the skip end tag flag to true.
668 skip_end_tag = true;
669 }
670
671 // 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.
672 if (ns != Namespace::HTML && !element.has_children()) {
673 markup.append('/');
674 skip_end_tag = true;
675 }
676
677 // 16. Append ">" (U+003E GREATER-THAN SIGN) to markup.
678 markup.append('>');
679
680 // 17. If the value of skip end tag is true, then return the value of markup and skip the remaining steps. The node is a leaf-node.
681 if (skip_end_tag)
682 return markup.to_deprecated_string();
683
684 // 18. If ns is the HTML namespace, and the node's localName matches the string "template", then this is a template element.
685 if (ns == Namespace::HTML && element.local_name() == HTML::TagNames::template_) {
686 // Append to markup the result of XML serializing a DocumentFragment node given the template element's template contents (a DocumentFragment), providing inherited ns, map, prefix index, and the require well-formed flag.
687 auto const& template_element = verify_cast<HTML::HTMLTemplateElement>(element);
688 markup.append(TRY(serialize_document_fragment(template_element.content(), inherited_ns, map, prefix_index, require_well_formed)));
689 }
690
691 // 19. Otherwise, append to markup the result of running the XML serialization algorithm on each of node's children, in tree order, providing inherited ns, map, prefix index, and the require well-formed flag.
692 else {
693 for (auto const* element_child = element.first_child(); element_child; element_child = element_child->next_sibling())
694 markup.append(TRY(serialize_node_to_xml_string_impl(*element_child, inherited_ns, map, prefix_index, require_well_formed)));
695 }
696
697 // 20. Append the following to markup, in the order listed:
698 // 1. "</" (U+003C LESS-THAN SIGN, U+002F SOLIDUS);
699 markup.append("</"sv);
700
701 // 2. The value of qualified name;
702 markup.append(qualified_name.to_deprecated_string());
703
704 // 3. ">" (U+003E GREATER-THAN SIGN).
705 markup.append('>');
706
707 // 21. Return the value of markup.
708 return markup.to_deprecated_string();
709}
710
711// https://w3c.github.io/DOM-Parsing/#xml-serializing-a-document-node
712static WebIDL::ExceptionOr<DeprecatedString> serialize_document(DOM::Document const& document, Optional<DeprecatedFlyString>& namespace_, HashMap<DeprecatedFlyString, Vector<DeprecatedString>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed)
713{
714 // If the require well-formed flag is set (its value is true), and this node has no documentElement (the documentElement attribute's value is null),
715 // then throw an exception; the serialization of this node would not be a well-formed document.
716 if (require_well_formed == RequireWellFormed::Yes && !document.document_element())
717 return WebIDL::InvalidStateError::create(document.realm(), "Document has no document element");
718
719 // Otherwise, run the following steps:
720 // 1. Let serialized document be an empty string.
721 StringBuilder serialized_document;
722
723 // 2. For each child child of node, in tree order, run the XML serialization algorithm on the child passing along the provided arguments, and append the result to serialized document.
724 for (auto const* child = document.first_child(); child; child = child->next_sibling())
725 serialized_document.append(TRY(serialize_node_to_xml_string_impl(*child, namespace_, namespace_prefix_map, prefix_index, require_well_formed)));
726
727 // 3. Return the value of serialized document.
728 return serialized_document.to_deprecated_string();
729}
730
731// https://w3c.github.io/DOM-Parsing/#xml-serializing-a-comment-node
732static WebIDL::ExceptionOr<DeprecatedString> serialize_comment(DOM::Comment const& comment, RequireWellFormed require_well_formed)
733{
734 // If the require well-formed flag is set (its value is true), and node's data contains characters that are not matched by the XML Char production
735 // or contains "--" (two adjacent U+002D HYPHEN-MINUS characters) or that ends with a "-" (U+002D HYPHEN-MINUS) character, then throw an exception;
736 // the serialization of this node's data would not be well-formed.
737 if (require_well_formed == RequireWellFormed::Yes) {
738 // FIXME: Check comment's data against the XML Char production.
739
740 if (comment.data().contains("--"sv))
741 return WebIDL::InvalidStateError::create(comment.realm(), "Comment data contains two adjacent hyphens");
742
743 if (comment.data().ends_with('-'))
744 return WebIDL::InvalidStateError::create(comment.realm(), "Comment data ends with a hyphen");
745 }
746
747 // Otherwise, return the concatenation of "<!--", node's data, and "-->".
748 return DeprecatedString::formatted("<!--{}-->", comment.data());
749}
750
751// https://w3c.github.io/DOM-Parsing/#xml-serializing-a-text-node
752static WebIDL::ExceptionOr<DeprecatedString> serialize_text(DOM::Text const& text, [[maybe_unused]] RequireWellFormed require_well_formed)
753{
754 // FIXME: 1. If the require well-formed flag is set (its value is true), and node's data contains characters that are not matched by the XML Char production,
755 // then throw an exception; the serialization of this node's data would not be well-formed.
756
757 // 2. Let markup be the value of node's data.
758 DeprecatedString markup = text.data();
759
760 // 3. Replace any occurrences of "&" in markup by "&".
761 markup = markup.replace("&"sv, "&"sv, ReplaceMode::All);
762
763 // 4. Replace any occurrences of "<" in markup by "<".
764 markup = markup.replace("<"sv, "<"sv, ReplaceMode::All);
765
766 // 5. Replace any occurrences of ">" in markup by ">".
767 markup = markup.replace(">"sv, ">"sv, ReplaceMode::All);
768
769 // 6. Return the value of markup.
770 return markup;
771}
772
773// https://w3c.github.io/DOM-Parsing/#xml-serializing-a-documentfragment-node
774static WebIDL::ExceptionOr<DeprecatedString> serialize_document_fragment(DOM::DocumentFragment const& document_fragment, Optional<DeprecatedFlyString>& namespace_, HashMap<DeprecatedFlyString, Vector<DeprecatedString>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed)
775{
776 // 1. Let markup the empty string.
777 StringBuilder markup;
778
779 // 2. For each child child of node, in tree order, run the XML serialization algorithm on the child given namespace, prefix map, a reference to prefix index,
780 // and flag require well-formed. Concatenate the result to markup.
781 for (auto const* child = document_fragment.first_child(); child; child = child->next_sibling())
782 markup.append(TRY(serialize_node_to_xml_string_impl(*child, namespace_, namespace_prefix_map, prefix_index, require_well_formed)));
783
784 // 3. Return the value of markup.
785 return markup.to_deprecated_string();
786}
787
788// https://w3c.github.io/DOM-Parsing/#xml-serializing-a-documenttype-node
789static WebIDL::ExceptionOr<DeprecatedString> serialize_document_type(DOM::DocumentType const& document_type, RequireWellFormed require_well_formed)
790{
791 if (require_well_formed == RequireWellFormed::Yes) {
792 // FIXME: 1. If the require well-formed flag is true and the node's publicId attribute contains characters that are not matched by the XML PubidChar production,
793 // then throw an exception; the serialization of this node would not be a well-formed document type declaration.
794
795 // 2. If the require well-formed flag is true and the node's systemId attribute contains characters that are not matched by the XML Char production or that contains
796 // both a """ (U+0022 QUOTATION MARK) and a "'" (U+0027 APOSTROPHE), then throw an exception; the serialization of this node would not be a well-formed document type declaration.
797 // FIXME: Check systemId against the XML Char production.
798 if (document_type.system_id().contains('"') && document_type.system_id().contains('\''))
799 return WebIDL::InvalidStateError::create(document_type.realm(), "Document type system ID contains both a quotation mark and an apostrophe");
800 }
801
802 // 3. Let markup be an empty string.
803 StringBuilder markup;
804
805 // 4. Append the string "<!DOCTYPE" to markup.
806 // 5. Append " " (U+0020 SPACE) to markup.
807 markup.append("<!DOCTYPE "sv);
808
809 // 6. Append the value of the node's name attribute to markup. For a node belonging to an HTML document, the value will be all lowercase.
810 markup.append(document_type.name());
811
812 // 7. If the node's publicId is not the empty string then append the following, in the order listed, to markup:
813 if (!document_type.public_id().is_empty()) {
814 // 1. " " (U+0020 SPACE);
815 // 2. The string "PUBLIC";
816 // 3. " " (U+0020 SPACE);
817 // 4. """ (U+0022 QUOTATION MARK);
818 markup.append(" PUBLIC \""sv);
819
820 // 5. The value of the node's publicId attribute;
821 markup.append(document_type.public_id());
822
823 // 6. """ (U+0022 QUOTATION MARK).
824 markup.append('"');
825 }
826
827 // 8. If the node's systemId is not the empty string and the node's publicId is set to the empty string, then append the following, in the order listed, to markup:
828 if (!document_type.system_id().is_empty() && !document_type.public_id().is_empty()) {
829 // 1. " " (U+0020 SPACE);
830 // 2. The string "SYSTEM".
831 markup.append(" SYSTEM"sv);
832 }
833
834 // 9. If the node's systemId is not the empty string then append the following, in the order listed, to markup:
835 if (!document_type.system_id().is_empty()) {
836 // 1. " " (U+0020 SPACE);
837 // 2. """ (U+0022 QUOTATION MARK);
838 markup.append(" \""sv);
839
840 // 3. The value of the node's systemId attribute;
841 markup.append(document_type.system_id());
842
843 // 4. """ (U+0022 QUOTATION MARK).
844 markup.append('"');
845 }
846
847 // 10. Append ">" (U+003E GREATER-THAN SIGN) to markup.
848 markup.append('>');
849
850 // 11. Return the value of markup.
851 return markup.to_deprecated_string();
852}
853
854// https://w3c.github.io/DOM-Parsing/#dfn-xml-serializing-a-processinginstruction-node
855static WebIDL::ExceptionOr<DeprecatedString> serialize_processing_instruction(DOM::ProcessingInstruction const& processing_instruction, RequireWellFormed require_well_formed)
856{
857 if (require_well_formed == RequireWellFormed::Yes) {
858 // 1. If the require well-formed flag is set (its value is true), and node's target contains a ":" (U+003A COLON) character
859 // or is an ASCII case-insensitive match for the string "xml", then throw an exception; the serialization of this node's target would not be well-formed.
860 if (processing_instruction.target().contains(':'))
861 return WebIDL::InvalidStateError::create(processing_instruction.realm(), "Processing instruction target contains a colon");
862
863 if (Infra::is_ascii_case_insensitive_match(processing_instruction.target(), "xml"sv))
864 return WebIDL::InvalidStateError::create(processing_instruction.realm(), "Processing instruction target is equal to 'xml'");
865
866 // 2. If the require well-formed flag is set (its value is true), and node's data contains characters that are not matched by the XML Char production or contains
867 // the string "?>" (U+003F QUESTION MARK, U+003E GREATER-THAN SIGN), then throw an exception; the serialization of this node's data would not be well-formed.
868 // FIXME: Check data against the XML Char production.
869 if (processing_instruction.data().contains("?>"sv))
870 return WebIDL::InvalidStateError::create(processing_instruction.realm(), "Processing instruction data contains a terminator");
871 }
872
873 // 3. Let markup be the concatenation of the following, in the order listed:
874 StringBuilder markup;
875
876 // 1. "<?" (U+003C LESS-THAN SIGN, U+003F QUESTION MARK);
877 markup.append("<?"sv);
878
879 // 2. The value of node's target;
880 markup.append(processing_instruction.target());
881
882 // 3. " " (U+0020 SPACE);
883 markup.append(' ');
884
885 // 4. The value of node's data;
886 markup.append(processing_instruction.data());
887
888 // 5. "?>" (U+003F QUESTION MARK, U+003E GREATER-THAN SIGN).
889 markup.append("?>"sv);
890
891 // 4. Return the value of markup.
892 return markup.to_deprecated_string();
893}
894
895}