3. Writing XML

3.1. Choosing a writer

XMLBufferWriter retains a document for conversion to a string or immediate reuse by XMLReader. XMLFileWriter associates the same writer interface with an output filename:

xmlio::XMLBufferWriter memory("settings");

xmlio::XMLFileWriter file("settings.xml", "settings");
xmlio::write(file, "count", 4);
file.close();

An explicit close is recommended for files because it reports output errors. The destructor also closes the file but cannot safely propagate an exception.

These writers retain the complete document tree. That enables later queries, subtree composition, and conversion to a string. Use XMLStreamWriter when the output may be too large to keep in memory:

xmlio::XMLStreamWriter events("events.xml", "events");
for (const auto& event : event_catalog) {
  xmlio::write(events, "elem", event);
}
events.closeTag();
events.close();

The stream writer accepts an output filename or an existing std::ostream. It supports the same typed write overloads as XMLWriter and stores only the open-element stack and pending attributes. Its compact output deliberately adds no formatting whitespace to element content.

close verifies that exactly one root has been completed and that every tag is balanced. flush sends bytes already emitted to the destination without closing the document. Explicitly closing is especially important here because a destructor cannot report an incomplete document.

3.2. Creating the document tree

A root name may be supplied to the constructor, or the first push or openTag may create it. push makes a new child current and pop returns to its parent:

xmlio::XMLBufferWriter output("project");
xmlio::push(output, "limits");
xmlio::write(output, "lower", 0.0);
xmlio::write(output, "upper", 1.0);
xmlio::pop(output);

The writer rejects a second document root and attempts to close an element when the stack is empty. depth exposes the number of open elements when a diagnostic or test needs to verify stack balance.

3.3. Attributes and empty elements

set_attr attaches an attribute to the current element. Attribute lists are useful when creating a tag and its attributes together:

xmlio::XMLBufferWriter output;
output.openTag(
    "project",
    xmlio::AttributeList{
        xmlio::Attribute("name", "example"),
        xmlio::Attribute("version", 2)});
output.emptyTag("marker", {xmlio::Attribute("kind", "checkpoint")});
output.closeTag();

String and arithmetic attributes use the same locale-independent conversion as element values.

An XMLStreamWriter delays each start tag until content is emitted. This allows set_attr to add or replace attributes after openTag. Once text, a child, a comment, or an XML fragment has emitted the start tag, changing its attributes raises WriterError.

3.4. Namespaces

Namespace declarations are ordinary xmlns attributes. Qualified elements and attributes require a declared prefix:

xmlio::XMLBufferWriter output;
output.openTag(
    "document",
    {xmlio::Attribute("xmlns:m", "urn:example:metrics")});
output.openTag("m", "score");
output.write(8.5);
output.closeTag();
output.closeTag();

The reader must independently register the prefix before using it in XPath. The prefix chosen for a query need not be the same spelling used in the file; the namespace URI is what identifies the namespace.

3.5. Text, escaping, and raw XML

All normal text and attribute values are escaped by libxml2. A string such as A < B & B < C therefore remains data and cannot accidentally create tags.

write_comment appends a comment under the current element. It rejects the -- sequence and a trailing hyphen because neither is legal in XML comments.

writeXML is different: it parses its argument as a well-formed XML fragment and appends the resulting nodes. Use it only when the caller intentionally has XML rather than plain text. Malformed fragments raise WriterError.

3.6. Embedding documents

A reader or memory writer can be embedded by name:

xmlio::XMLBufferWriter result("result");
xmlio::write(result, "input", original_reader);

This produces <input> containing the original document root. Streaming a reader with operator<< inserts its root directly under the current element. Both forms omit the embedded document’s XML declaration.

3.7. Serialization and output

document_str returns a standalone document with its XML declaration. fragment_str returns the root element without that declaration and is the appropriate form for embedding or database metadata. The established str(bool prologue = true) and printRoot spellings remain available; the explicit names make intent visible at a call site. print writes to a stream, and save replaces a file with the current document.

XMLFileWriter is DOM buffered. flush serializes the complete current document and replaces the associated file. XMLStreamWriter instead emits incrementally. Calling str, document_str, fragment_str, printRoot, print, or save on it raises WriterError because no complete document is retained.

3.8. Incremental output example

This example writes through a caller-owned stream, uses normal typed serialization, closes the document explicitly, and reparses the result:

 1/**
 2 * @file streaming_output.cpp
 3 * @brief Incrementally write and verify a document without retaining its DOM.
 4 */
 5
 6#include <xmlio/xmlio.hpp>
 7
 8#include <sstream>
 9#include <string>
10
11int main() {
12  std::ostringstream destination;
13  xmlio::XMLStreamWriter output(destination, "samples", false);
14  output.set_attr("source", "demonstration");
15
16  for (int index = 0; index < 3; ++index) {
17    output.openTag("elem", {xmlio::Attribute("index", index)});
18    xmlio::write(output, "value", 0.25 * static_cast<double>(index));
19    output.closeTag();
20  }
21
22  output.closeTag();
23  output.close();
24
25  auto input = xmlio::XMLReader::from_string(destination.str());
26  if (input.count("/samples/elem") != 3 ||
27      xmlio::read<double>(input, "/samples/elem[3]/value") != 0.5) {
28    return 1;
29  }
30  return 0;
31}

3.9. Complete composition example

This example preserves an input document inside a result and also extracts deferred operation definitions:

 1/**
 2 * @file composition.cpp
 3 * @brief Extract deferred XML groups and preserve an input document in output.
 4 */
 5
 6#include <xmlio/xmlio.hpp>
 7
 8#include <iostream>
 9#include <string>
10
11int main() {
12  auto input = xmlio::XMLReader::from_string(R"xml(
13<control>
14  <operations>
15    <elem><type>constant</type><value>2.0</value></elem>
16    <elem><type>linear</type><slope>0.5</slope></elem>
17  </operations>
18</control>
19)xml");
20
21  const auto groups =
22      xmlio::read_xml_vector_group(input, "/control/operations", "type");
23  for (const auto& group : groups) {
24    std::cout << "deferred operation: " << group.id << '\n';
25  }
26
27  xmlio::XMLBufferWriter output("result");
28  xmlio::write(output, "input", input);
29  xmlio::write(output, "operation_count", groups.size());
30
31  const auto result = xmlio::XMLReader::from_string(output.str());
32  std::cout << result.xpath_str("/result/input/control/operations") << '\n';
33}