Skip to content

Commit c8fadba

Browse files
committed
feat(xml): .NET-faithful XmlWriter text form; XmlReader navigation, namespaces, line info and settings
The writer printed its DOM with tinyxml2's XMLPrinter, which differs from .NET's XmlWriter in five measured ways: a fixed four-space indent that ignored IndentChars, "<a/>" for an empty element where .NET writes "<a />", encoding="UTF-8" in the declaration where .NET writes "utf-8", a trailing newline .NET does not write, and children still indented after text was written into an element, where .NET stops indenting for the rest of that element. ToString() and Flush() now emit .NET's form from XmlWriterSettings alone (Indent, IndentChars, NewLineChars, NewLineHandling, OmitXmlDeclaration), NewLineChars defaults to Environment.NewLine as in .NET, and XmlSerializer sets IndentChars to the four spaces it has always written so its output is unchanged. XmlReader gains MoveToContent, IsStartElement, ReadStartElement(name) with .NET's message, Skip, MoveToFirstAttribute, LocalName/Prefix/Depth/AttributeCount/HasAttributes, LookupNamespace over the xmlns declarations in scope, the IXmlLineInfo members (line only: tinyxml2 records no column), and Create(uri, settings) honouring DtdProcessing (Prohibit with .NET's message), IgnoreComments, IgnoreProcessingInstructions and IgnoreWhitespace. The one-argument factories are unchanged. Needed by CNA's XNA IntermediateSerializer (plans/plan_xnapipeline_parity.md XNAPP-071), whose output must match the genuine serializer's XML byte for byte and whose reader must resolve Type="Alias:Name" through xmlns scoping and report line numbers in errors.
1 parent c3fbb95 commit c8fadba

9 files changed

Lines changed: 851 additions & 29 deletions

File tree

modules/xml-serialization/include/System/Xml/Serialization/XmlSerializer.hpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ namespace System::Xml::Serialization {
3030
* `Samples/ShipGame_4_0/.../level1_spawns.xml` with four -- both are genuine
3131
* `XmlSerializer` output shipped in the same official XNA Game Studio source tree. So the
3232
* element names, their order, their text and the root's namespace declarations are the
33-
* contract; the indentation is not. `Indent` emits the four-space form (tinyxml2's own
34-
* fixed width, which `XmlWriterSettings::IndentChars` does not influence).
33+
* contract; the indentation is not. `Indent` emits the four-space form this serializer has
34+
* always written, set explicitly through `XmlWriterSettings::IndentChars`.
3535
*/
3636
struct XmlSerializationOptions {
3737
/** @brief Pretty-print with tinyxml2's fixed four-space indentation. */
@@ -213,6 +213,7 @@ namespace System::Xml::Serialization {
213213

214214
System::Xml::XmlWriterSettings settings;
215215
settings.Indent = true;
216+
settings.IndentChars = " ";
216217
std::unique_ptr<System::Xml::XmlWriter> writer(
217218
System::Xml::XmlWriter::CreateToString(settings));
218219
doc.Save(*writer);

modules/xml/include/System/Xml/XmlReader.hpp

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@
33
// Portions based on .NET runtime API (MIT License, Copyright .NET Foundation and Contributors)
44
#pragma once
55
#include <memory>
6+
#include <optional>
67
#include <string>
78
#include <vector>
89
#include <utility>
910

11+
#include "SharpRuntime/SharpRuntimeHelper.hpp"
1012
#include "System/Xml/ReadState.hpp"
1113
#include "System/Xml/XmlNodeType.hpp"
1214

1315
namespace System::Xml {
1416

1517
struct XmlReaderState; ///< Opaque tinyxml2 state; defined in XmlReader.cpp.
18+
class XmlReaderSettings;
1619

1720
/**
1821
* @brief Represents a reader that provides fast, non-cached, forward-only access to XML data.
@@ -43,6 +46,25 @@ namespace System::Xml {
4346
* is on no node (including after @c Close()). */
4447
[[nodiscard]] std::string getNameProperty() const;
4548

49+
/** @brief Returns the local part of the current node's name — the text after the
50+
* namespace prefix's colon, or the whole name when it has no prefix. */
51+
[[nodiscard]] std::string getLocalNameProperty() const;
52+
53+
/** @brief Returns the namespace prefix of the current node's name, or @c "" when the
54+
* name has none. */
55+
[[nodiscard]] std::string getPrefixProperty() const;
56+
57+
/** @brief Returns the depth of the current node: 0 for the document's top-level nodes,
58+
* one more for each enclosing element; an attribute is one deeper than its element. */
59+
[[nodiscard]] SharpRuntime::intcs getDepthProperty() const;
60+
61+
/** @brief Returns @c true when the current element has at least one attribute. */
62+
[[nodiscard]] bool getHasAttributesProperty() const;
63+
64+
/** @brief Returns the number of attributes on the current element, or 0 on any other
65+
* node. */
66+
[[nodiscard]] SharpRuntime::intcs getAttributeCountProperty() const;
67+
4668
/** @brief Returns the text value of the current node (Text/CDATA/Comment), or @c ""
4769
* when the reader is on no node (including after @c Close()). */
4870
[[nodiscard]] std::string getValueProperty() const;
@@ -73,6 +95,67 @@ namespace System::Xml {
7395
*/
7496
bool MoveToElement();
7597

98+
/**
99+
* @brief Moves the cursor to the first attribute of the current element.
100+
*
101+
* @return @c true if the element has an attribute; @c false on any other node.
102+
*/
103+
bool MoveToFirstAttribute();
104+
105+
/**
106+
* @brief Skips comments, processing instructions, the XML declaration, document type
107+
* nodes and whitespace until the reader is on a content node (element,
108+
* end element, text or CDATA) or at end of file; an attribute cursor is moved
109+
* back to its element first.
110+
*
111+
* @return The node type the reader stopped on; @c XmlNodeType::None at end of file.
112+
*/
113+
XmlNodeType MoveToContent();
114+
115+
/**
116+
* @brief Calls @c MoveToContent() and tells whether it stopped on a start element.
117+
*
118+
* @return @c true when the current content node is an element.
119+
*/
120+
bool IsStartElement();
121+
122+
/**
123+
* @brief Calls @c MoveToContent() and tells whether it stopped on a start element
124+
* with the given qualified name.
125+
*
126+
* @param name The qualified name to match.
127+
* @return @c true when the current content node is an element named @p name.
128+
*/
129+
bool IsStartElement(const std::string& name);
130+
131+
/**
132+
* @brief Skips the current node and, for a non-empty element, all of its children,
133+
* leaving the reader on the node that follows; on an attribute the element
134+
* is skipped. Does nothing when the reader is on no node.
135+
*/
136+
void Skip();
137+
138+
/**
139+
* @brief Resolves a namespace prefix in the scope of the current node, exactly as the
140+
* @c xmlns declarations on it and its ancestors define it.
141+
*
142+
* @param prefix The prefix to resolve; @c "" asks for the default namespace.
143+
* @return The namespace URI, or @c std::nullopt when the prefix is not declared in
144+
* scope. The @c xml and @c xmlns prefixes resolve to their fixed URIs.
145+
*/
146+
[[nodiscard]] std::optional<std::string> LookupNamespace(const std::string& prefix) const;
147+
148+
/** @brief Always @c true: the parser records the line every node starts on. These three
149+
* members are the @c IXmlLineInfo contract, offered directly because this reader keeps
150+
* no vtable (the class is pinned to a single owning pointer). */
151+
[[nodiscard]] bool HasLineInfo() const;
152+
153+
/** @brief Returns the 1-based line the current node starts on, or 0 on no node. */
154+
[[nodiscard]] SharpRuntime::intcs getLineNumberProperty() const;
155+
156+
/** @brief Returns 0: the parser does not record the column a node starts in. */
157+
[[nodiscard]] SharpRuntime::intcs getLinePositionProperty() const;
158+
76159
/**
77160
* @brief Moves to the next attribute of the current element.
78161
*
@@ -105,6 +188,16 @@ namespace System::Xml {
105188
*/
106189
void ReadStartElement();
107190

191+
/**
192+
* @brief Checks, after @c MoveToContent(), that the current node is a start element
193+
* with the given qualified name and advances past it.
194+
*
195+
* @param name The qualified name the element must have.
196+
* @throws XmlException when the current content node is not that element, with the
197+
* message @c "Element 'name' was not found. Line L, position P."
198+
*/
199+
void ReadStartElement(const std::string& name);
200+
108201
/**
109202
* @brief Verifies that the current node is an end-element and advances the reader.
110203
*
@@ -138,6 +231,20 @@ namespace System::Xml {
138231
*/
139232
static XmlReader* Create(const std::string& inputUri);
140233

234+
/**
235+
* @brief Creates an XmlReader as the one-argument overload does, applying @p settings:
236+
* @c DtdProcessing::Prohibit rejects a document that carries a DOCTYPE,
237+
* @c DtdProcessing::Ignore drops the node, and @c IgnoreComments,
238+
* @c IgnoreProcessingInstructions and @c IgnoreWhitespace drop those nodes.
239+
*
240+
* @param inputUri File path or raw XML text.
241+
* @param settings The reader settings to apply.
242+
* @return Heap-allocated XmlReader; caller owns the pointer.
243+
* @throws XmlException on parse error, or on a DOCTYPE when DTD processing is
244+
* prohibited ("For security reasons DTD is prohibited in this XML document. …").
245+
*/
246+
static XmlReader* Create(const std::string& inputUri, const XmlReaderSettings& settings);
247+
141248
/**
142249
* @brief Creates an XmlReader that parses @p xmlContent as raw XML.
143250
*

modules/xml/include/System/Xml/XmlWriterSettings.hpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#pragma once
55

66
#include <string>
7+
#include "System/Environment.hpp"
78

89
#include "System/Xml/ConformanceLevel.hpp"
910
#include "System/Xml/NewLineHandling.hpp"
@@ -34,7 +35,10 @@ namespace System::Xml {
3435
/** @brief How line breaks are normalized. Not currently consulted (tinyxml2 controls output line breaks). */
3536
System::Xml::NewLineHandling NewLineHandling = System::Xml::NewLineHandling::Replace;
3637
/** @brief The character string to use for line breaks. Not currently consulted. */
37-
std::string NewLineChars = "\r\n";
38+
/** @brief The line terminator written between indented nodes and, under
39+
* @c NewLineHandling::Replace, in place of every line break in text: .NET's
40+
* @c Environment.NewLine, so "\n" here and "\r\n" on Windows. */
41+
std::string NewLineChars = System::Environment::NewLine;
3842
/** @brief Whether to indent elements. Consulted by @c XmlWriter::ToString()/Flush(): @c false
3943
* (the default) emits compact output with no inserted whitespace; @c true pretty-prints
4044
* via tinyxml2's own indentation (which does not consult @c IndentChars). */

0 commit comments

Comments
 (0)