[SVGDevice] Text whitespace unittest

Plumb SkDOM as needed to make it suitable for an SkXMLWriter backend.

Also fix a potential null typeface issue in
SkSVGDevice::AutoElement::addTextAttributes().

R=reed@google.com,mtklein@google.com

Review URL: https://codereview.chromium.org/940283002
diff --git a/gyp/tests.gypi b/gyp/tests.gypi
index 6b4a9e4..fdd892a 100644
--- a/gyp/tests.gypi
+++ b/gyp/tests.gypi
@@ -203,6 +203,7 @@
     '../tests/StrokeTest.cpp',
     '../tests/StrokerTest.cpp',
     '../tests/SurfaceTest.cpp',
+    '../tests/SVGDeviceTest.cpp',
     '../tests/TArrayTest.cpp',
     '../tests/TDPQueueTest.cpp',
     '../tests/Time.cpp',
diff --git a/include/xml/SkDOM.h b/include/xml/SkDOM.h
index e0bb744..df5766f 100644
--- a/include/xml/SkDOM.h
+++ b/include/xml/SkDOM.h
@@ -17,6 +17,9 @@
 struct SkDOMNode;
 struct SkDOMAttr;
 
+class SkDOMParser;
+class SkXMLParser;
+
 class SkDOM {
 public:
     SkDOM();
@@ -32,6 +35,9 @@
 
     const Node* getRootNode() const;
 
+    SkXMLParser* beginParsing();
+    const Node* finishParsing();
+
     enum Type {
         kElement_Type,
         kText_Type
@@ -82,8 +88,10 @@
     SkDEBUGCODE(static void UnitTest();)
 
 private:
-    SkChunkAlloc    fAlloc;
-    Node*           fRoot;
+    SkChunkAlloc               fAlloc;
+    Node*                      fRoot;
+    SkAutoTDelete<SkDOMParser> fParser;
+
     friend class AttrIter;
     friend class SkDOMParser;
 };
diff --git a/src/svg/SkSVGDevice.cpp b/src/svg/SkSVGDevice.cpp
index da52fac..a56a631 100644
--- a/src/svg/SkSVGDevice.cpp
+++ b/src/svg/SkSVGDevice.cpp
@@ -523,14 +523,6 @@
 void SkSVGDevice::AutoElement::addTextAttributes(const SkPaint& paint) {
     this->addAttribute("font-size", paint.getTextSize());
 
-    SkTypeface::Style style = paint.getTypeface()->style();
-    if (style & SkTypeface::kItalic) {
-        this->addAttribute("font-style", "italic");
-    }
-    if (style & SkTypeface::kBold) {
-        this->addAttribute("font-weight", "bold");
-    }
-
     if (const char* textAlign = svg_text_align(paint.getTextAlign())) {
         this->addAttribute("text-anchor", textAlign);
     }
@@ -538,7 +530,17 @@
     SkString familyName;
     SkTHashSet<SkString, hash_family_string> familySet;
     SkAutoTUnref<const SkTypeface> tface(paint.getTypeface() ?
-        SkRef(paint.getTypeface()) : SkTypeface::RefDefault(style));
+        SkRef(paint.getTypeface()) : SkTypeface::RefDefault());
+
+    SkASSERT(tface);
+    SkTypeface::Style style = tface->style();
+    if (style & SkTypeface::kItalic) {
+        this->addAttribute("font-style", "italic");
+    }
+    if (style & SkTypeface::kBold) {
+        this->addAttribute("font-weight", "bold");
+    }
+
     SkAutoTUnref<SkTypeface::LocalizedStrings> familyNameIter(tface->createFamilyNameIterator());
     SkTypeface::LocalizedString familyString;
     while (familyNameIter->next(&familyString)) {
diff --git a/src/xml/SkDOM.cpp b/src/xml/SkDOM.cpp
index 9854608..eb1bc09 100644
--- a/src/xml/SkDOM.cpp
+++ b/src/xml/SkDOM.cpp
@@ -8,11 +8,12 @@
 
 
 #include "SkDOM.h"
+#include "SkStream.h"
+#include "SkXMLWriter.h"
 
 /////////////////////////////////////////////////////////////////////////
 
 #include "SkXMLParser.h"
-
 bool SkXMLParser::parse(const SkDOM& dom, const SkDOMNode* node)
 {
     const char* elemName = dom.getName(node);
@@ -199,19 +200,22 @@
 }
 
 class SkDOMParser : public SkXMLParser {
-    bool fNeedToFlush;
 public:
     SkDOMParser(SkChunkAlloc* chunk) : SkXMLParser(&fParserError), fAlloc(chunk)
     {
+        fAlloc->reset();
         fRoot = NULL;
         fLevel = 0;
         fNeedToFlush = true;
     }
     SkDOM::Node* getRoot() const { return fRoot; }
     SkXMLParserError fParserError;
+
 protected:
     void flushAttributes()
     {
+        SkASSERT(fLevel > 0);
+
         int attrCount = fAttrs.count();
 
         SkDOM::Node* node = (SkDOM::Node*)fAlloc->alloc(sizeof(SkDOM::Node) + attrCount * sizeof(SkDOM::Attr),
@@ -220,7 +224,7 @@
         node->fName = fElemName;
         node->fFirstChild = NULL;
         node->fAttrCount = SkToU16(attrCount);
-        node->fType = SkDOM::kElement_Type;
+        node->fType = fElemType;
 
         if (fRoot == NULL)
         {
@@ -240,24 +244,20 @@
         fAttrs.reset();
 
     }
-    virtual bool onStartElement(const char elem[])
-    {
-        if (fLevel > 0 && fNeedToFlush)
-            this->flushAttributes();
-        fNeedToFlush = true;
-        fElemName = dupstr(fAlloc, elem);
-        ++fLevel;
+
+    bool onStartElement(const char elem[]) override {
+        this->startCommon(elem, SkDOM::kElement_Type);
         return false;
     }
-    virtual bool onAddAttribute(const char name[], const char value[])
-    {
+
+    bool onAddAttribute(const char name[], const char value[]) override {
         SkDOM::Attr* attr = fAttrs.append();
         attr->fName = dupstr(fAlloc, name);
         attr->fValue = dupstr(fAlloc, value);
         return false;
     }
-    virtual bool onEndElement(const char elem[])
-    {
+
+    bool onEndElement(const char elem[]) override {
         --fLevel;
         if (fNeedToFlush)
             this->flushAttributes();
@@ -279,20 +279,40 @@
         parent->fFirstChild = prev;
         return false;
     }
+
+    bool onText(const char text[], int len) override {
+        SkString str(text, len);
+        this->startCommon(str.c_str(), SkDOM::kText_Type);
+        this->SkDOMParser::onEndElement(str.c_str());
+
+        return false;
+    }
+
 private:
+    void startCommon(const char elem[], SkDOM::Type type) {
+        if (fLevel > 0 && fNeedToFlush)
+            this->flushAttributes();
+
+        fNeedToFlush = true;
+        fElemName = dupstr(fAlloc, elem);
+        fElemType = type;
+        ++fLevel;
+    }
+
     SkTDArray<SkDOM::Node*> fParentStack;
-    SkChunkAlloc*   fAlloc;
-    SkDOM::Node*    fRoot;
+    SkChunkAlloc*           fAlloc;
+    SkDOM::Node*            fRoot;
+    bool                    fNeedToFlush;
 
     // state needed for flushAttributes()
     SkTDArray<SkDOM::Attr>  fAttrs;
     char*                   fElemName;
+    SkDOM::Type             fElemType;
     int                     fLevel;
 };
 
 const SkDOM::Node* SkDOM::build(const char doc[], size_t len)
 {
-    fAlloc.reset();
     SkDOMParser parser(&fAlloc);
     if (!parser.parse(doc, len))
     {
@@ -310,6 +330,11 @@
 static void walk_dom(const SkDOM& dom, const SkDOM::Node* node, SkXMLParser* parser)
 {
     const char* elem = dom.getName(node);
+    if (dom.getType(node) == SkDOM::kText_Type) {
+        SkASSERT(dom.countChildren(node) == 0);
+        parser->text(elem, SkToInt(strlen(elem)));
+        return;
+    }
 
     parser->startElement(elem);
 
@@ -331,7 +356,6 @@
 
 const SkDOM::Node* SkDOM::copy(const SkDOM& dom, const SkDOM::Node* node)
 {
-    fAlloc.reset();
     SkDOMParser parser(&fAlloc);
 
     walk_dom(dom, node, &parser);
@@ -340,6 +364,21 @@
     return fRoot;
 }
 
+SkXMLParser* SkDOM::beginParsing() {
+    SkASSERT(!fParser);
+    fParser.reset(SkNEW_ARGS(SkDOMParser, (&fAlloc)));
+
+    return fParser.get();
+}
+
+const SkDOM::Node* SkDOM::finishParsing() {
+    SkASSERT(fParser);
+    fRoot = fParser->getRoot();
+    fParser.free();
+
+    return fRoot;
+}
+
 //////////////////////////////////////////////////////////////////////////
 
 int SkDOM::countChildren(const Node* node, const char elem[]) const
@@ -427,41 +466,14 @@
 
 #ifdef SK_DEBUG
 
-static void tab(int level)
-{
-    while (--level >= 0)
-        SkDebugf("\t");
-}
-
 void SkDOM::dump(const Node* node, int level) const
 {
     if (node == NULL)
         node = this->getRootNode();
-    if (node)
-    {
-        tab(level);
-        SkDebugf("<%s", this->getName(node));
 
-        const Attr* attr = node->attrs();
-        const Attr* stop = attr + node->fAttrCount;
-        for (; attr < stop; attr++)
-            SkDebugf(" %s=\"%s\"", attr->fName, attr->fValue);
-
-        const Node* child = this->getFirstChild(node);
-        if (child)
-        {
-            SkDebugf(">\n");
-            while (child)
-            {
-                this->dump(child, level+1);
-                child = this->getNextSibling(child);
-            }
-            tab(level);
-            SkDebugf("</%s>\n", node->fName);
-        }
-        else
-            SkDebugf("/>\n");
-    }
+    SkDebugWStream debugStream;
+    SkXMLStreamWriter xmlWriter(&debugStream);
+    xmlWriter.writeDOM(*this, node, false);
 }
 
 void SkDOM::UnitTest()
diff --git a/src/xml/SkXMLWriter.cpp b/src/xml/SkXMLWriter.cpp
index 62e9668..7a1b042 100644
--- a/src/xml/SkXMLWriter.cpp
+++ b/src/xml/SkXMLWriter.cpp
@@ -169,7 +169,14 @@
 {
     if (!skipRoot)
     {
-        w->startElement(dom.getName(node));
+        const char* elem = dom.getName(node);
+        if (dom.getType(node) == SkDOM::kText_Type) {
+            SkASSERT(dom.countChildren(node) == 0);
+            w->addText(elem, strlen(elem));
+            return;
+        }
+
+        w->startElement(elem);
 
         SkDOM::AttrIter iter(dom, node);
         const char* name;
diff --git a/tests/SVGDeviceTest.cpp b/tests/SVGDeviceTest.cpp
new file mode 100644
index 0000000..c973f8b
--- /dev/null
+++ b/tests/SVGDeviceTest.cpp
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkCanvas.h"
+#include "SkData.h"
+#include "SkDOM.h"
+#include "SkParse.h"
+#include "SkStream.h"
+#include "SkSVGCanvas.h"
+#include "SkXMLWriter.h"
+#include "Test.h"
+
+#include <string.h>
+
+namespace {
+
+void check_text_node(skiatest::Reporter* reporter,
+                     const SkDOM& dom,
+                     const SkDOM::Node* root,
+                     const SkPoint& offset,
+                     unsigned scalarsPerPos,
+                     const char* expected) {
+    if (root == NULL) {
+        ERRORF(reporter, "root element not found.");
+        return;
+    }
+
+    const SkDOM::Node* textElem = dom.getFirstChild(root, "text");
+    if (textElem == NULL) {
+        ERRORF(reporter, "<text> element not found.");
+        return;
+    }
+    REPORTER_ASSERT(reporter, dom.getType(textElem) == SkDOM::kElement_Type);
+
+    const SkDOM::Node* textNode= dom.getFirstChild(textElem);
+    REPORTER_ASSERT(reporter, textNode != NULL);
+    if (textNode != NULL) {
+        REPORTER_ASSERT(reporter, dom.getType(textNode) == SkDOM::kText_Type);
+        REPORTER_ASSERT(reporter, strcmp(expected, dom.getName(textNode)) == 0);
+    }
+
+    int textLen = SkToInt(strlen(expected));
+
+    const char* x = dom.findAttr(textElem, "x");
+    REPORTER_ASSERT(reporter, x != NULL);
+    if (x != NULL) {
+        int xposCount = (scalarsPerPos < 1) ? 1 : textLen;
+        REPORTER_ASSERT(reporter, SkParse::Count(x) == xposCount);
+
+        SkAutoTMalloc<SkScalar> xpos(xposCount);
+        SkParse::FindScalars(x, xpos.get(), xposCount);
+        if (scalarsPerPos < 1) {
+            REPORTER_ASSERT(reporter, xpos[0] == offset.x());
+        } else {
+            for (int i = 0; i < xposCount; ++i) {
+                REPORTER_ASSERT(reporter, xpos[i] == SkIntToScalar(expected[i]));
+            }
+        }
+    }
+
+    const char* y = dom.findAttr(textElem, "y");
+    REPORTER_ASSERT(reporter, y != NULL);
+    if (y != NULL) {
+        int yposCount = (scalarsPerPos < 2) ? 1 : textLen;
+        REPORTER_ASSERT(reporter, SkParse::Count(y) == yposCount);
+
+        SkAutoTMalloc<SkScalar> ypos(yposCount);
+        SkParse::FindScalars(y, ypos.get(), yposCount);
+        if (scalarsPerPos < 2) {
+            REPORTER_ASSERT(reporter, ypos[0] == offset.y());
+        } else {
+            for (int i = 0; i < yposCount; ++i) {
+                REPORTER_ASSERT(reporter, ypos[i] == -SkIntToScalar(expected[i]));
+            }
+        }
+    }
+}
+
+void test_whitespace_pos(skiatest::Reporter* reporter,
+                         const char* txt,
+                         const char* expected) {
+    size_t len = strlen(txt);
+
+    SkDOM dom;
+    SkPaint paint;
+    SkPoint offset = SkPoint::Make(10, 20);
+
+    {
+        SkXMLParserWriter writer(dom.beginParsing());
+        SkAutoTUnref<SkCanvas> svgCanvas(SkSVGCanvas::Create(SkRect::MakeWH(100, 100),
+                                                             &writer));
+        svgCanvas->drawText(txt, len, offset.x(), offset.y(), paint);
+    }
+    check_text_node(reporter, dom, dom.finishParsing(), offset, 0, expected);
+
+    {
+        SkAutoTMalloc<SkScalar> xpos(len);
+        for (int i = 0; i < SkToInt(len); ++i) {
+            xpos[i] = SkIntToScalar(txt[i]);
+        }
+
+        SkXMLParserWriter writer(dom.beginParsing());
+        SkAutoTUnref<SkCanvas> svgCanvas(SkSVGCanvas::Create(SkRect::MakeWH(100, 100),
+                                                             &writer));
+        svgCanvas->drawPosTextH(txt, len, xpos, offset.y(), paint);
+    }
+    check_text_node(reporter, dom, dom.finishParsing(), offset, 1, expected);
+
+    {
+        SkAutoTMalloc<SkPoint> pos(len);
+        for (int i = 0; i < SkToInt(len); ++i) {
+            pos[i] = SkPoint::Make(SkIntToScalar(txt[i]), -SkIntToScalar(txt[i]));
+        }
+
+        SkXMLParserWriter writer(dom.beginParsing());
+        SkAutoTUnref<SkCanvas> svgCanvas(SkSVGCanvas::Create(SkRect::MakeWH(100, 100),
+                                                             &writer));
+        svgCanvas->drawPosText(txt, len, pos, paint);
+    }
+    check_text_node(reporter, dom, dom.finishParsing(), offset, 2, expected);
+}
+
+}
+
+DEF_TEST(SVGDevice_whitespace_pos, reporter) {
+    static const struct {
+        const char* tst_in;
+        const char* tst_out;
+    } tests[] = {
+        { "abcd"      , "abcd" },
+        { "ab cd"     , "ab cd" },
+        { "ab \t\t cd", "ab cd" },
+        { " abcd"     , "abcd" },
+        { "  abcd"    , "abcd" },
+        { " \t\t abcd", "abcd" },
+        { "abcd "     , "abcd " }, // we allow one trailing whitespace char
+        { "abcd  "    , "abcd " }, // because it makes no difference and
+        { "abcd\t  "  , "abcd\t" }, // simplifies the implementation
+        { "\t\t  \t ab \t\t  \t cd \t\t   \t  ", "ab cd " },
+    };
+
+    for (unsigned i = 0; i < SK_ARRAY_COUNT(tests); ++i) {
+        test_whitespace_pos(reporter, tests[i].tst_in, tests[i].tst_out);
+    }
+}