diff --git a/python/Scripts/mxupdate.py b/python/Scripts/mxupdate.py index 5d808ecc53..fdbeda28b7 100644 --- a/python/Scripts/mxupdate.py +++ b/python/Scripts/mxupdate.py @@ -21,6 +21,7 @@ def main(): try: readOptions = mx.XmlReadOptions() readOptions.readComments = True + readOptions.readNewlines = True mx.readFromXmlFile(doc, filename, mx.FileSearchPath(), readOptions) validDocs[filename] = doc except mx.Exception: diff --git a/resources/Materials/Examples/StandardSurface/standard_surface_chess_set.mtlx b/resources/Materials/Examples/StandardSurface/standard_surface_chess_set.mtlx index c94e21aeb3..96e8a6580c 100644 --- a/resources/Materials/Examples/StandardSurface/standard_surface_chess_set.mtlx +++ b/resources/Materials/Examples/StandardSurface/standard_surface_chess_set.mtlx @@ -1,555 +1,555 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/MaterialXCore/Element.cpp b/source/MaterialXCore/Element.cpp index 1140f6b34a..18d028efd3 100644 --- a/source/MaterialXCore/Element.cpp +++ b/source/MaterialXCore/Element.cpp @@ -731,6 +731,7 @@ INSTANTIATE_CONCRETE_SUBCLASS(Look, "look") INSTANTIATE_CONCRETE_SUBCLASS(LookGroup, "lookgroup") INSTANTIATE_CONCRETE_SUBCLASS(MaterialAssign, "materialassign") INSTANTIATE_CONCRETE_SUBCLASS(Member, "member") +INSTANTIATE_CONCRETE_SUBCLASS(NewlineElement, "newline") INSTANTIATE_CONCRETE_SUBCLASS(Node, "node") INSTANTIATE_CONCRETE_SUBCLASS(NodeDef, "nodedef") INSTANTIATE_CONCRETE_SUBCLASS(NodeGraph, "nodegraph") diff --git a/source/MaterialXCore/Element.h b/source/MaterialXCore/Element.h index 8f526a02e2..8e43d9aa33 100644 --- a/source/MaterialXCore/Element.h +++ b/source/MaterialXCore/Element.h @@ -22,6 +22,7 @@ class TypedElement; class ValueElement; class Token; class CommentElement; +class NewlineElement; class GenericElement; class StringResolver; class Document; @@ -51,6 +52,11 @@ using CommentElementPtr = shared_ptr; /// A shared pointer to a const CommentElement using ConstCommentElementPtr = shared_ptr; +/// A shared pointer to a NewlineElement +using NewlineElementPtr = shared_ptr; +/// A shared pointer to a const NewlineElement +using ConstNewlineElementPtr = shared_ptr; + /// A shared pointer to a GenericElement using GenericElementPtr = shared_ptr; /// A shared pointer to a const GenericElement @@ -1153,6 +1159,21 @@ class MX_CORE_API CommentElement : public Element static const string CATEGORY; }; +/// @class NewlineElement +/// An element representing a newline within a document. +class MX_CORE_API NewlineElement : public Element +{ + public: + NewlineElement(ElementPtr parent, const string& name) : + Element(parent, CATEGORY, name) + { + } + virtual ~NewlineElement() { } + + public: + static const string CATEGORY; +}; + /// @class GenericElement /// A generic element subclass, for instantiating elements with unrecognized categories. class MX_CORE_API GenericElement : public Element diff --git a/source/MaterialXFormat/External/PugiXML/pugixml.cpp b/source/MaterialXFormat/External/PugiXML/pugixml.cpp index e61cf592ee..73b3525028 100644 --- a/source/MaterialXFormat/External/PugiXML/pugixml.cpp +++ b/source/MaterialXFormat/External/PugiXML/pugixml.cpp @@ -3419,7 +3419,31 @@ PUGI__NS_BEGIN { mark = s; // Save this offset while searching for a terminator. - PUGI__SKIPWS(); // Eat whitespace if no genuine PCDATA here. + // MaterialX: Enable newline tracking when processing whitespace. + if (PUGI__OPTSET(parse_newlines)) + { + if (PUGI__IS_CHARTYPE(*s, ct_space)) + { + unsigned int lineCount = 0; + while (PUGI__IS_CHARTYPE(*s, ct_space)) + { + if (s[0] == '\n') + { + lineCount++; + } + ++s; + } + for (size_t i=1; ivalue ? node->value + 0 : PUGIXML_TEXT("")); break; + // MaterialX: Handle newline output + case node_newline: + writer.write_string(""); + break; + case node_pi: writer.write('<', '?'); writer.write_string(node->name ? node->name + 0 : default_name); @@ -4246,8 +4275,12 @@ PUGI__NS_BEGIN if ((indent_flags & indent_newline) && (flags & format_raw) == 0) writer.write('\n'); - if ((indent_flags & indent_indent) && indent_length) - text_output_indent(writer, indent, indent_length, depth); + // MaterialX: don't indent new line nodes + if (PUGI__NODETYPE(node) != node_newline) + { + if ((indent_flags & indent_indent) && indent_length) + text_output_indent(writer, indent, indent_length, depth); + } if (PUGI__NODETYPE(node) == node_element) { diff --git a/source/MaterialXFormat/External/PugiXML/pugixml.hpp b/source/MaterialXFormat/External/PugiXML/pugixml.hpp index 86403be312..5392259678 100644 --- a/source/MaterialXFormat/External/PugiXML/pugixml.hpp +++ b/source/MaterialXFormat/External/PugiXML/pugixml.hpp @@ -142,6 +142,7 @@ namespace pugi node_pcdata, // Plain character data, i.e. 'text' node_cdata, // Character data, i.e. '' node_comment, // Comment tag, i.e. '' + node_newline, // MaterialX: A newline node node_pi, // Processing instruction, i.e. '' node_declaration, // Document declaration, i.e. '' node_doctype // Document type declaration, i.e. '' @@ -201,6 +202,9 @@ namespace pugi // This flag is off by default. const unsigned int parse_embed_pcdata = 0x2000; + // MaterialX: This flag determines if newlines are added to the DOM tree. This flag is off by default. + const unsigned int parse_newlines = 0x4000; + // The default parsing mode. // Elements, PCDATA and CDATA sections are added to the DOM tree, character/reference entities are expanded, // End-of-Line characters are normalized, attribute values are normalized using CDATA normalization rules. diff --git a/source/MaterialXFormat/XmlIo.cpp b/source/MaterialXFormat/XmlIo.cpp index 6e7d3509e8..67d41225a4 100644 --- a/source/MaterialXFormat/XmlIo.cpp +++ b/source/MaterialXFormat/XmlIo.cpp @@ -62,11 +62,18 @@ void elementFromXml(const xml_node& xmlNode, ElementPtr elem, const XmlReadOptio ElementPtr child = elem->addChildOfCategory(category, name); elementFromXml(xmlChild, child, readOptions); - // Handle the interpretation of XML comments. - if (readOptions && readOptions->readComments && category.empty()) + // Handle the interpretation of XML comments and newlines. + if (readOptions && category.empty()) { - child = elem->changeChildCategory(child, CommentElement::CATEGORY); - child->setDocString(xmlChild.value()); + if (readOptions->readComments && xmlChild.type() == node_comment) + { + child = elem->changeChildCategory(child, CommentElement::CATEGORY); + child->setDocString(xmlChild.value()); + } + else if (readOptions->readNewlines && xmlChild.type() == node_newline) + { + child = elem->changeChildCategory(child, NewlineElement::CATEGORY); + } } } } @@ -131,6 +138,14 @@ void elementToXml(ConstElementPtr elem, xml_node& xmlNode, const XmlWriteOptions continue; } + // Write XML newlines. + if (child->getCategory() == NewlineElement::CATEGORY) + { + xml_node xmlChild = xmlNode.append_child(node_newline); + xmlChild.set_value("\n"); + continue; + } + xml_node xmlChild = xmlNode.append_child(child->getCategory().c_str()); elementToXml(child, xmlChild, writeOptions); } @@ -253,9 +268,16 @@ void validateParseResult(const xml_parse_result& result, const FilePath& filenam unsigned int getParseOptions(const XmlReadOptions* readOptions) { unsigned int parseOptions = parse_default; - if (readOptions && readOptions->readComments) + if (readOptions) { - parseOptions |= parse_comments; + if (readOptions->readComments) + { + parseOptions |= parse_comments; + } + if (readOptions->readNewlines) + { + parseOptions |= parse_newlines; + } } return parseOptions; } @@ -268,6 +290,7 @@ unsigned int getParseOptions(const XmlReadOptions* readOptions) XmlReadOptions::XmlReadOptions() : readComments(false), + readNewlines(false), upgradeVersion(true), readXIncludeFunction(readFromXmlFile) { diff --git a/source/MaterialXFormat/XmlIo.h b/source/MaterialXFormat/XmlIo.h index b8e24e1fc3..b098c0fb3a 100644 --- a/source/MaterialXFormat/XmlIo.h +++ b/source/MaterialXFormat/XmlIo.h @@ -38,6 +38,10 @@ class MX_FORMAT_API XmlReadOptions /// Defaults to false. bool readComments; + /// If true, then XML newlines will be read into documents as newline elements. + /// Defaults to false. + bool readNewlines; + /// If true, then documents from earlier versions of MaterialX will be upgraded /// to the current version. Defaults to true. bool upgradeVersion; diff --git a/source/MaterialXTest/MaterialXFormat/XmlIo.cpp b/source/MaterialXTest/MaterialXFormat/XmlIo.cpp index d103bfb56f..1aea085bb2 100644 --- a/source/MaterialXTest/MaterialXFormat/XmlIo.cpp +++ b/source/MaterialXTest/MaterialXFormat/XmlIo.cpp @@ -240,6 +240,27 @@ TEST_CASE("Load content", "[xmlio]") REQUIRE_THROWS_AS(mx::readFromXmlFile(nonExistentDoc, "NonExistent.mtlx", mx::FileSearchPath(), &readOptions), mx::ExceptionFileMissing); } +TEST_CASE("Comments and newlines", "[xmlio]") +{ + mx::FilePath testPath("resources/Materials/Examples/StandardSurface/standard_surface_chess_set.mtlx"); + + // Read the example file into an XML string buffer. + std::string origXml = mx::readFile(testPath); + + // Convert the string to a document with comments and newlines preserved. + mx::DocumentPtr doc = mx::createDocument(); + mx::XmlReadOptions readOptions; + readOptions.readComments = true; + readOptions.readNewlines = true; + mx::readFromXmlString(doc, origXml, mx::FileSearchPath(), &readOptions); + + // Write the document to a new XML string buffer. + std::string newXml = mx::writeToXmlString(doc); + + // Verify that the XML string buffers are identical. + REQUIRE(origXml == newXml); +} + TEST_CASE("Locale region testing", "[xmlio]") { // In the United States, the thousands separator is a comma, while in Germany it is a period. diff --git a/source/PyMaterialX/PyMaterialXCore/PyElement.cpp b/source/PyMaterialX/PyMaterialXCore/PyElement.cpp index 77b4ef6c5b..d89cbc81a4 100644 --- a/source/PyMaterialX/PyMaterialXCore/PyElement.cpp +++ b/source/PyMaterialX/PyMaterialXCore/PyElement.cpp @@ -189,6 +189,9 @@ void bindPyElement(py::module& mod) py::class_(mod, "CommentElement") .def_readonly_static("CATEGORY", &mx::CommentElement::CATEGORY); + py::class_(mod, "NewlineElement") + .def_readonly_static("CATEGORY", &mx::NewlineElement::CATEGORY); + py::class_(mod, "GenericElement") .def_readonly_static("CATEGORY", &mx::GenericElement::CATEGORY); diff --git a/source/PyMaterialX/PyMaterialXFormat/PyXmlIo.cpp b/source/PyMaterialX/PyMaterialXFormat/PyXmlIo.cpp index a6c4be112d..9b4b32cf6a 100644 --- a/source/PyMaterialX/PyMaterialXFormat/PyXmlIo.cpp +++ b/source/PyMaterialX/PyMaterialXFormat/PyXmlIo.cpp @@ -17,6 +17,7 @@ void bindPyXmlIo(py::module& mod) .def(py::init()) .def_readwrite("readXIncludeFunction", &mx::XmlReadOptions::readXIncludeFunction) .def_readwrite("readComments", &mx::XmlReadOptions::readComments) + .def_readwrite("readNewlines", &mx::XmlReadOptions::readNewlines) .def_readwrite("upgradeVersion", &mx::XmlReadOptions::upgradeVersion) .def_readwrite("parentXIncludes", &mx::XmlReadOptions::parentXIncludes);