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}