This is part 4/14 of my Implementing IXmlWriter post series.
One of the enhancements that XML introduced over SGML was a shorthand for specifying an element with no content by adding a trailing slash at the end of an open element. For example, <br/> is equivalent to <br></br>. Let’s add this functionality to the previous version of IXmlWriter.
Here’s the test case:
StringXmlWriter xmlWriter;
xmlWriter.WriteStartElement("root");
xmlWriter.WriteStartElement("emptyElement");
xmlWriter.WriteEndElement();
xmlWriter.WriteEndElement();
std::string strXML = xmlWriter.GetXmlString();
// strXML should be <root><emptyElement/></root>
How does this affect our previous implementation?
- We clearly cannot write the > character to close the start element in
WriteStartElement(). WriteEndElement()needs to be able to detect if a still-opened start element was written and if so, to write the/>sequence. Otherwise, it needs to write the full end element string.- Because the
>character will not be written inWriteStartElement(), we also need to worry about closing the start element in virtually every function.
The simplest way to implement this feature that I can think of is to keep an extra bool which remembers whether a unclosed start element has been written, and to handle this case in all relevant functions. Because we have all the previous test cases, I simply made the relevant changes to WriteStartElement() and then kept running the test cases, adding code as necessary to fix failures. I ended up with the following implementation:
class StringXmlWriter
{
private:
std::stack<std::string> m_openedElements;
std::string m_xmlStr;
bool m_unclosedStartElement;
public:
StringXmlWriter() : m_unclosedStartElement(false) {}
void WriteStartElement(const std::string& localName)
{
if (m_unclosedStartElement) {
m_xmlStr += '>';
m_unclosedStartElement = false;
}
m_openedElements.push(localName);
m_xmlStr += '<';
m_xmlStr += localName;
m_unclosedStartElement = true;
}
void WriteEndElement()
{
if (m_unclosedStartElement) {
m_xmlStr += "/>";
m_unclosedStartElement = false;
} else {
std::string lastOpenedElement = m_openedElements.top();
m_xmlStr += '<';
m_xmlStr += lastOpenedElement;
m_xmlStr += '>';
}
m_openedElements.pop();
}
void WriteString(const std::string& value)
{
if (m_unclosedStartElement) {
m_xmlStr += '>';
m_unclosedStartElement = false;
}
typedef std::string::const_iterator iter_t;
for (iter_t iter = value.begin(); iter != value.end(); ++iter) {
if (*iter == '&') {
m_xmlStr += "&";
} else if (*iter == '<') {
m_xmlStr += "<";
} else if (*iter == '>') {
m_xmlStr += ">";
} else {
m_xmlStr += *iter;
}
}
}
void WriteElementString(const std::string& localName,
const std::string& value)
{
WriteStartElement(localName);
WriteString(value);
WriteEndElement();
}
std::string GetXmlString() const
{
return m_xmlStr;
}
};
Remember, although the bool-based approach may not be the most elegant nor the eventual long-term solution, the idea with test-driven development is to write the simplest code possible to pass the existing test cases. If future test cases show the need, I will freely change this implementation detail, as the growing test suite allows me to make changes with great confidence.