Implementing IXmlWriter Part 4: Collapsing Empty Elements

This is part 4 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?

  1. We clearly cannot write the > character to close the start element in WriteStartElement().
  2. 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.
  3. Because the > character will not be written in WriteStartElement(), 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 += "&amp;";
            } else if (*iter == '<') {
                m_xmlStr += "&lt;";
            } else if (*iter == '>') {
                m_xmlStr += "&gt;";
            } 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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s