bookmaker initial checkin

bookmaker is a tool that generates documentation
backends from a canonical markup. Documentation for
bookmaker itself is evolving at docs/usingBookmaker.bmh,
which is visible online at skia.org/user/api/bmh_usingBookmaker

Change-Id: Ic76ddf29134895b5c2ebfbc84603e40ff08caf09
Reviewed-on: https://skia-review.googlesource.com/28000
Commit-Queue: Cary Clark <caryclark@google.com>
Reviewed-by: Cary Clark <caryclark@google.com>
diff --git a/tools/bookmaker/includeParser.cpp b/tools/bookmaker/includeParser.cpp
new file mode 100644
index 0000000..089dcb3
--- /dev/null
+++ b/tools/bookmaker/includeParser.cpp
@@ -0,0 +1,1733 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "bookmaker.h"
+
+enum class KeyProperty {
+    kNone,
+    kClassSection,
+    kFunction,
+    kModifier,
+    kNumber,
+    kObject,
+    kPreprocessor,
+};
+
+struct IncludeKey {
+    const char* fName;
+    KeyWord fKeyWord;
+    KeyProperty fProperty;
+};
+
+const IncludeKey kKeyWords[] = {
+    { "",           KeyWord::kNone,         KeyProperty::kNone           },
+    { "bool",       KeyWord::kBool,         KeyProperty::kNumber         },
+    { "char",       KeyWord::kChar,         KeyProperty::kNumber         },
+    { "class",      KeyWord::kClass,        KeyProperty::kObject         },
+    { "const",      KeyWord::kConst,        KeyProperty::kModifier       },
+    { "constexpr",  KeyWord::kConstExpr,    KeyProperty::kModifier       },
+    { "define",     KeyWord::kDefine,       KeyProperty::kPreprocessor   },
+    { "double",     KeyWord::kDouble,       KeyProperty::kNumber         },
+    { "elif",       KeyWord::kElif,         KeyProperty::kPreprocessor   },
+    { "else",       KeyWord::kElse,         KeyProperty::kPreprocessor   },
+    { "endif",      KeyWord::kEndif,        KeyProperty::kPreprocessor   },
+    { "enum",       KeyWord::kEnum,         KeyProperty::kObject         },
+    { "float",      KeyWord::kFloat,        KeyProperty::kNumber         },
+    { "friend",     KeyWord::kFriend,       KeyProperty::kModifier       },
+    { "if",         KeyWord::kIf,           KeyProperty::kPreprocessor   },
+    { "ifdef",      KeyWord::kIfdef,        KeyProperty::kPreprocessor   },
+    { "ifndef",     KeyWord::kIfndef,       KeyProperty::kPreprocessor   },
+    { "include",    KeyWord::kInclude,      KeyProperty::kPreprocessor   },
+    { "inline",     KeyWord::kInline,       KeyProperty::kModifier       },
+    { "int",        KeyWord::kInt,          KeyProperty::kNumber         },
+    { "operator",   KeyWord::kOperator,     KeyProperty::kFunction       },
+    { "private",    KeyWord::kPrivate,      KeyProperty::kClassSection   },
+    { "protected",  KeyWord::kProtected,    KeyProperty::kClassSection   },
+    { "public",     KeyWord::kPublic,       KeyProperty::kClassSection   },
+    { "signed",     KeyWord::kSigned,       KeyProperty::kNumber         },
+    { "size_t",     KeyWord::kSize_t,       KeyProperty::kNumber         },
+    { "static",     KeyWord::kStatic,       KeyProperty::kModifier       },
+    { "struct",     KeyWord::kStruct,       KeyProperty::kObject         },
+    { "template",   KeyWord::kTemplate,     KeyProperty::kObject         },
+    { "typedef",    KeyWord::kTypedef,      KeyProperty::kObject         },
+    { "uint32_t",   KeyWord::kUint32_t,     KeyProperty::kNumber         },
+    { "union",      KeyWord::kUnion,        KeyProperty::kObject         },
+    { "unsigned",   KeyWord::kUnsigned,     KeyProperty::kNumber         },
+    { "void",       KeyWord::kVoid,         KeyProperty::kNumber         },
+};
+
+const size_t kKeyWordCount = SK_ARRAY_COUNT(kKeyWords);
+
+KeyWord IncludeParser::FindKey(const char* start, const char* end) {
+    int ch = 0;
+    for (size_t index = 0; index < kKeyWordCount; ) {
+        if (start[ch] > kKeyWords[index].fName[ch]) {
+            ++index;
+            if (ch > 0 && kKeyWords[index - 1].fName[ch - 1] < kKeyWords[index].fName[ch - 1]) {
+                return KeyWord::kNone;
+            }
+            continue;
+        }
+        if (start[ch] < kKeyWords[index].fName[ch]) {
+            return KeyWord::kNone;
+        }
+        ++ch;
+        if (start + ch >= end) {
+            return kKeyWords[index].fKeyWord;
+        }
+    }
+    return KeyWord::kNone;
+}
+
+void IncludeParser::ValidateKeyWords() {
+    for (size_t index = 1; index < kKeyWordCount; ++index) {
+        SkASSERT((int) kKeyWords[index - 1].fKeyWord + 1
+                == (int) kKeyWords[index].fKeyWord);
+        SkASSERT(0 > strcmp(kKeyWords[index - 1].fName, kKeyWords[index].fName));
+    }
+}
+
+void IncludeParser::addKeyword(KeyWord keyWord) {
+    fParent->fTokens.emplace_back(keyWord, fIncludeWord, fChar, fLineCount, fParent);
+    fIncludeWord = nullptr;
+    if (KeyProperty::kObject == kKeyWords[(int) keyWord].fProperty) {
+        Definition* def = &fParent->fTokens.back();
+        this->addDefinition(def);
+        if (KeyWord::kEnum == fParent->fKeyWord) {
+            fInEnum = true;
+        }
+    }
+}
+
+void IncludeParser::checkForMissingParams(const vector<string>& methodParams, 
+        const vector<string>& foundParams) {
+    for (auto& methodParam : methodParams) {
+        bool found = false;
+        for (auto& foundParam : foundParams) {
+            if (methodParam == foundParam) {
+                found = true;
+                break;
+            }
+        }
+        if (!found) {
+            this->keywordStart("Param");
+            fprintf(fOut, "%s  ", methodParam.c_str());
+            this->keywordEnd();
+        }
+    }
+    for (auto& foundParam : foundParams) {
+        bool found = false;
+        for (auto& methodParam : methodParams) {
+            if (methodParam == foundParam) {
+                found = true;
+                break;
+            }
+        }
+        if (!found) {
+            this->reportError("doxygen param does not match method declaration");
+        }
+    }
+}
+
+bool IncludeParser::checkForWord() {
+    if (!fIncludeWord) {
+        return true;
+    }
+    KeyWord keyWord = FindKey(fIncludeWord, fChar);
+    if (KeyWord::kNone != keyWord) {
+        if (KeyProperty::kPreprocessor != kKeyWords[(int) keyWord].fProperty) {
+            this->addKeyword(keyWord);
+            return true;
+        }
+    } else {
+        this->addWord();
+        return true;
+    }
+    Definition* poundDef = fParent;
+    if (!fParent) {
+        return reportError<bool>("expected parent");
+    }
+    if (Definition::Type::kBracket != poundDef->fType) {
+        return reportError<bool>("expected bracket");
+    }
+    if (Bracket::kPound != poundDef->fBracket) {
+        return reportError<bool>("expected preprocessor");
+    }
+    if (KeyWord::kNone != poundDef->fKeyWord) {
+        return reportError<bool>("already found keyword");
+    }
+    poundDef->fKeyWord = keyWord;
+    fIncludeWord = nullptr;
+    switch (keyWord) {
+        // these do not link to other # directives
+        case KeyWord::kDefine:
+        case KeyWord::kInclude:
+        break;
+        // these start a # directive link
+        case KeyWord::kIf:
+        case KeyWord::kIfdef:
+        case KeyWord::kIfndef:
+        break;
+        // these continue a # directive link
+        case KeyWord::kElif:
+        case KeyWord::kElse: {
+            this->popObject();  // pop elif
+            if (Bracket::kPound != fParent->fBracket) {
+                return this->reportError<bool>("expected preprocessor directive");
+            }
+            this->popBracket();  // pop if
+            poundDef->fParent = fParent;
+            this->addDefinition(poundDef);  // push elif back
+        } break;
+        // this ends a # directive link
+        case KeyWord::kEndif:
+        // FIXME : should this be calling popBracket() instead?
+            this->popObject();  // pop endif
+            if (Bracket::kPound != fParent->fBracket) {
+                return this->reportError<bool>("expected preprocessor directive");
+            }
+            this->popBracket();  // pop if/else
+        break;
+        default:
+            SkASSERT(0);
+    }
+    return true;
+}
+
+string IncludeParser::className() const {
+    string name(fParent->fName);
+    size_t slash = name.find_last_of("/");
+    if (string::npos == slash) {
+        slash = name.find_last_of("\\");
+    }
+    SkASSERT(string::npos != slash);
+    string result = name.substr(slash);
+    result = result.substr(1, result.size() - 3);
+    return result;
+}
+
+bool IncludeParser::crossCheck(BmhParser& bmhParser) {
+    string className = this->className();
+    string classPrefix = className + "::";
+    RootDefinition* root = &bmhParser.fClassMap[className];
+    root->clearVisited();
+    for (auto& classMapper : fIClassMap) {
+        if (className != classMapper.first
+                && classPrefix != classMapper.first.substr(0, classPrefix.length())) {
+            continue;
+        }
+        auto& classMap = classMapper.second;
+        auto& tokens = classMap.fTokens;
+        for (const auto& token : tokens) {
+            if (token.fPrivate) {
+                continue;
+            }
+            string fullName = classMapper.first + "::" + token.fName;
+            const Definition* def = root->find(fullName);
+            switch (token.fMarkType) {
+                case MarkType::kMethod: {
+                    if (0 == token.fName.find("internal_")
+                            || 0 == token.fName.find("Internal_")
+                            || 0 == token.fName.find("legacy_")
+                            || 0 == token.fName.find("temporary_")) {
+                        continue;
+                    }
+                    const char* methodID = bmhParser.fMaps[(int) token.fMarkType].fName;
+                    if (!def) {
+                        string paramName = className + "::";
+                        paramName += string(token.fContentStart,
+                                token.fContentEnd - token.fContentStart);
+                        def = root->find(paramName);
+                        if (!def && 0 == token.fName.find("operator")) {
+                            string operatorName = className + "::";
+                            TextParser oper("", token.fStart, token.fContentEnd, 0);
+                            const char* start = oper.strnstr("operator", token.fContentEnd);
+                            SkASSERT(start);
+                            oper.skipTo(start);
+                            oper.skipToEndBracket('(');
+                            int parens = 0;
+                            do {
+                                if ('(' == oper.peek()) {
+                                    ++parens;
+                                } else if (')' == oper.peek()) {
+                                    --parens;
+                                }
+                            } while (!oper.eof() && oper.next() && parens > 0);
+                            operatorName += string(start, oper.fChar - start);
+                            def = root->find(operatorName);
+                        }
+                    }
+                    if (!def) {
+                        int skip = !strncmp(token.fContentStart, "explicit ", 9) ? 9 : 0;
+                        skip = !strncmp(token.fContentStart, "virtual ", 8) ? 8 : skip;
+                        string constructorName = className + "::";
+                        constructorName += string(token.fContentStart + skip,
+                                token.fContentEnd - token.fContentStart - skip);
+                        def = root->find(constructorName);
+                    }
+                    if (!def && 0 == token.fName.find("SK_")) {
+                        string incName = token.fName + "()";
+                        string macroName = className + "::" + incName;
+                        def = root->find(macroName);
+                        if (def) {
+                            if (def->fName == incName) {
+                                def->fVisited = true;
+                                if ("SK_TO_STRING_NONVIRT" == token.fName) {
+                                    def = root->find(className + "::toString");
+                                    if (def) {
+                                        def->fVisited = true;
+                                    } else {
+                                        SkDebugf("missing toString bmh: %s\n", fullName.c_str());
+                                    }
+                                }
+                                break;
+                            } else {
+                                SkDebugf("method macro differs from bmh: %s\n", fullName.c_str());
+                            }
+                        }
+                    }
+                    if (!def) {
+                        bool allLower = true;
+                        for (size_t index = 0; index < token.fName.length(); ++index) {
+                            if (!islower(token.fName[index])) {
+                                allLower = false;
+                                break;
+                            }
+                        }
+                        if (allLower) {
+                            string lowerName = className + "::" + token.fName + "()";
+                            def = root->find(lowerName);
+                        }
+                    }
+                    if (!def) {
+                        SkDebugf("method missing from bmh: %s\n", fullName.c_str());
+                        break;
+                    }
+                    if (def->crossCheck(methodID, token)) {
+                        def->fVisited = true;
+                    } else {
+                       SkDebugf("method differs from bmh: %s\n", fullName.c_str());
+                    }
+                } break;
+                case MarkType::kComment:
+                    break;
+                case MarkType::kEnum: {
+                    if (!def) {
+                        // work backwards from first word to deduce #Enum name
+                        TextParser firstMember("", token.fStart, token.fContentEnd, 0);
+                        SkAssertResult(firstMember.skipName("enum"));
+                        SkAssertResult(firstMember.skipToEndBracket('{'));
+                        firstMember.next();
+                        firstMember.skipWhiteSpace();
+                        SkASSERT('k' == firstMember.peek());
+                        const char* savePos = firstMember.fChar;
+                        firstMember.skipToNonAlphaNum();
+                        const char* wordEnd = firstMember.fChar;
+                        firstMember.fChar = savePos; 
+                        const char* lastUnderscore = nullptr;
+                        do {
+                            if (!firstMember.skipToEndBracket('_')) {
+                                break;
+                            }
+                            if (firstMember.fChar > wordEnd) {
+                                break;
+                            }
+                            lastUnderscore = firstMember.fChar;
+                        } while (firstMember.next());
+                        if (lastUnderscore) {
+                            ++lastUnderscore;
+                            string anonName = className + "::" + string(lastUnderscore, 
+                                    wordEnd - lastUnderscore) + 's';
+                            def = root->find(anonName);
+                        }
+                        if (!def) {
+                            SkDebugf("enum missing from bmh: %s\n", fullName.c_str());
+                            break;
+                        }
+                    }
+                    def->fVisited = true;
+                    for (auto& child : def->fChildren) {
+                        if (MarkType::kCode == child->fMarkType) {
+                            def = child;
+                            break;
+                        }
+                    }
+                    if (MarkType::kCode != def->fMarkType) {
+                        SkDebugf("enum code missing from bmh: %s\n", fullName.c_str());
+                        break;
+                    }
+                    if (def->crossCheck(token)) {
+                        def->fVisited = true;
+                    } else {
+                       SkDebugf("enum differs from bmh: %s\n", def->fName.c_str());
+                    }
+                    for (auto& child : token.fChildren) {
+                        string constName = className + "::" + child->fName;
+                        def = root->find(constName);
+                        if (!def) {
+                            string innerName = classMapper.first + "::" + child->fName;
+                            def = root->find(innerName);
+                        }
+                        if (!def) {
+                            if (string::npos == child->fName.find("Legacy_")) {
+                                SkDebugf("const missing from bmh: %s\n", constName.c_str());
+                            }
+                        } else {
+                            def->fVisited = true;
+                        }
+                    }
+                    } break;
+                case MarkType::kMember:
+                    if (def) {
+                        def->fVisited = true;
+                    } else {
+                        SkDebugf("member missing from bmh: %s\n", fullName.c_str());
+                    }
+                    break;
+                default:
+                    SkASSERT(0);  // unhandled 
+                    break;
+            }
+        }
+    }
+    if (!root->dumpUnVisited()) {
+        SkDebugf("some struct elements not found; struct finding in includeParser is missing\n");
+    }
+    return true;
+}
+
+IClassDefinition* IncludeParser::defineClass(const Definition& includeDef,
+        const string& name) {
+    string className;
+    const Definition* test = fParent;
+    while (Definition::Type::kFileType != test->fType) {
+        if (Definition::Type::kMark == test->fType && KeyWord::kClass == test->fKeyWord) {
+            className = test->fName + "::";
+            break;
+        }
+        test = test->fParent;
+    }
+    className += name; 
+    unordered_map<string, IClassDefinition>& map = fIClassMap;
+    IClassDefinition& markupDef = map[className];
+    if (markupDef.fStart) {
+        typedef IClassDefinition* IClassDefPtr;
+        return INHERITED::reportError<IClassDefPtr>("class already defined");
+    }
+    markupDef.fFileName = fFileName;
+    markupDef.fStart = includeDef.fStart;
+    markupDef.fContentStart = includeDef.fStart;
+    markupDef.fName = className;
+    markupDef.fContentEnd = includeDef.fContentEnd;
+    markupDef.fTerminator = includeDef.fTerminator;
+    markupDef.fParent = fParent;
+    markupDef.fLineCount = fLineCount;
+    markupDef.fMarkType = KeyWord::kStruct == includeDef.fKeyWord ?
+            MarkType::kStruct : MarkType::kClass;
+    markupDef.fKeyWord = includeDef.fKeyWord;
+    markupDef.fType = Definition::Type::kMark;
+    fParent = &markupDef;
+    return &markupDef;
+}
+
+void IncludeParser::dumpClassTokens(IClassDefinition& classDef) {
+    auto& tokens = classDef.fTokens;
+    for (auto& token : tokens) {
+        if (Definition::Type::kMark == token.fType && MarkType::kComment == token.fMarkType) {
+            continue;
+        }
+        if (MarkType::kMember != token.fMarkType) {
+            fprintf(fOut, "%s",
+              "# ------------------------------------------------------------------------------\n");
+            fprintf(fOut, ""                                                                  "\n");
+        }
+        switch (token.fMarkType) {
+            case MarkType::kEnum:
+                fprintf(fOut, "#Enum %s"                                                     "\n",
+                        token.fName.c_str());
+                fprintf(fOut, ""                                                             "\n");
+                fprintf(fOut, "#Code"                                                        "\n");
+                fprintf(fOut, "    enum %s {"                                                "\n",
+                        token.fName.c_str());
+                for (auto& child : token.fChildren) {
+                    fprintf(fOut, "        %s %.*s"                                          "\n",
+                            child->fName.c_str(), child->length(), child->fContentStart);
+                }
+                fprintf(fOut, "    };"                                                       "\n");
+                fprintf(fOut, "##"                                                           "\n");
+                fprintf(fOut, ""                                                             "\n");
+                this->dumpComment(&token);
+                for (auto& child : token.fChildren) {
+                    fprintf(fOut, "#Const %s", child->fName.c_str());
+                    TextParser val(child);
+                    if (!val.eof()) {
+                        if ('=' == val.fStart[0] || ',' == val.fStart[0]) {
+                            val.next();
+                            val.skipSpace();
+                            const char* valEnd = val.anyOf(",\n");
+                            if (!valEnd) {
+                                valEnd = val.fEnd;
+                            }
+                            fprintf(fOut, " %.*s", (int) (valEnd - val.fStart), val.fStart);
+                        } else {
+                            fprintf(fOut, " %.*s", 
+                                    (int) (child->fContentEnd - child->fContentStart),
+                                    child->fContentStart);
+                        }
+                    }
+                    fprintf(fOut, ""                                                         "\n");
+                    for (auto& token : child->fTokens) {
+                        if (MarkType::kComment == token.fMarkType) {
+                            this->dumpComment(&token);
+                        }
+                    }
+                    fprintf(fOut, "##"                                                       "\n");
+                }
+                fprintf(fOut, ""                                                             "\n");
+            break;
+            case MarkType::kMethod:
+                fprintf(fOut, "#Method %.*s"                                                 "\n",
+                        token.length(), token.fStart);
+                lfAlways(1);
+                this->dumpComment(&token);
+            break;
+            case MarkType::kMember:
+                this->keywordStart("Member");
+                fprintf(fOut, "%.*s  %s  ", (int) (token.fContentEnd - token.fContentStart),
+                        token.fContentStart, token.fName.c_str());           
+                lfAlways(1);
+                for (auto child : token.fChildren) {
+                    fprintf(fOut, "%.*s", (int) (child->fContentEnd - child->fContentStart),
+                            child->fContentStart);
+                    lfAlways(1);
+                }
+                this->keywordEnd();
+                continue;
+            break;
+            default:
+                SkASSERT(0);
+        }
+        this->lf(2);
+        fprintf(fOut, "#Example"                                                             "\n");
+        fprintf(fOut, "##"                                                                   "\n");
+        fprintf(fOut, ""                                                                     "\n");
+        fprintf(fOut, "#ToDo incomplete ##"                                                  "\n");
+        fprintf(fOut, ""                                                                     "\n");
+        fprintf(fOut, "##"                                                                   "\n");
+        fprintf(fOut, ""                                                                     "\n");
+    }
+}
+void IncludeParser::dumpComment(Definition* token) {
+    fLineCount = token->fLineCount;
+    fChar = fLine = token->fContentStart;
+    fEnd = token->fContentEnd;
+    bool sawParam = false;
+    bool multiline = false;
+    bool sawReturn = false;
+    bool sawComment = false;
+    bool methodHasReturn = false;
+    vector<string> methodParams;
+    vector<string> foundParams;
+    Definition methodName;
+    TextParser methodParser(token->fFileName, token->fContentStart, token->fContentEnd,
+            token->fLineCount);
+    if (MarkType::kMethod == token->fMarkType) {
+        methodName.fName = string(token->fContentStart,
+                (int) (token->fContentEnd - token->fContentStart));
+        methodHasReturn = !methodParser.startsWith("void ")
+                && !methodParser.strnchr('~', methodParser.fEnd);
+        const char* paren = methodParser.strnchr('(', methodParser.fEnd);
+        const char* nextEnd = paren;
+        do {
+            string paramName;
+            methodParser.fChar = nextEnd + 1;
+            methodParser.skipSpace();
+            if (!methodName.nextMethodParam(&methodParser, &nextEnd, &paramName)) {
+                continue;
+            }
+            methodParams.push_back(paramName);
+        } while (')' != nextEnd[0]);
+    }
+    for (const auto& child : token->fTokens) {
+        if (Definition::Type::kMark == child.fType && MarkType::kComment == child.fMarkType) {
+            if ('@' == child.fContentStart[0]) {
+                TextParser parser(&child);
+                do {
+                    parser.next();
+                    if (parser.startsWith("param ")) {
+                        parser.skipWord("param");
+                        const char* parmStart = parser.fChar;
+                        parser.skipToSpace();
+                        string parmName = string(parmStart, (int) (parser.fChar - parmStart));
+                        parser.skipWhiteSpace();
+                        do {
+                            size_t nextComma = parmName.find(',');
+                            string piece;
+                            if (string::npos == nextComma) {
+                                piece = parmName;
+                                parmName = "";
+                            } else {
+                                piece = parmName.substr(0, nextComma);
+                                parmName = parmName.substr(nextComma + 1);
+                            }
+                            if (sawParam) {
+                                if (multiline) {
+                                    this->lfAlways(1);
+                                }
+                                this->keywordEnd();
+                            } else {
+                                if (sawComment) {
+                                    this->nl();
+                                }
+                                this->lf(2);
+                            }
+                            foundParams.emplace_back(piece);
+                            this->keywordStart("Param");
+                            fprintf(fOut, "%s  ", piece.c_str());
+                            fprintf(fOut, "%.*s", (int) (parser.fEnd - parser.fChar), parser.fChar);
+                            this->lfAlways(1);
+                            sawParam = true;
+                            sawComment = false;
+                        } while (parmName.length());
+                        parser.skipTo(parser.fEnd);
+                    } else if (parser.startsWith("return ") || parser.startsWith("returns ")) {
+                        parser.skipWord("return");
+                        if ('s' == parser.peek()) {
+                            parser.next();
+                        }
+                        if (sawParam) {
+                            if (multiline) {
+                                this->lfAlways(1);
+                            }
+                            this->keywordEnd();
+                        }
+                        this->checkForMissingParams(methodParams, foundParams);
+                        sawParam = false;
+                        sawComment = false;
+                        multiline = false;
+                        this->lf(2);
+                        this->keywordStart("Return");
+                        fprintf(fOut, "%.*s ", (int) (parser.fEnd - parser.fChar),
+                                parser.fChar);
+                        this->lfAlways(1);
+                        sawReturn = true;
+                        parser.skipTo(parser.fEnd);
+                    } else {
+                        this->reportError("unexpected doxygen directive");
+                    }
+                } while (!parser.eof());
+            } else {
+                if (sawComment) {
+                    this->nl();
+                }
+                this->lf(1);
+                fprintf(fOut, "%.*s ", child.length(), child.fContentStart);
+                sawComment = true;
+                if (sawParam || sawReturn) {
+                    multiline = true;
+                }
+            }
+        }
+    }
+    if (sawParam || sawReturn) {
+        if (multiline) {
+            this->lfAlways(1);
+        }
+        this->keywordEnd();
+    }
+    if (!sawReturn) {
+        if (!sawParam) {
+            if (sawComment) {
+                this->nl();
+            }
+            this->lf(2);
+        }
+        this->checkForMissingParams(methodParams, foundParams);
+    }
+    if (methodHasReturn != sawReturn) {
+        if (!methodHasReturn) {
+            this->reportError("unexpected doxygen return");
+        } else {
+            if (sawComment) {
+                this->nl();
+            }
+            this->lf(2);
+            this->keywordStart("Return");
+            this->keywordEnd();
+        }
+    }
+}
+
+    // dump equivalent markup 
+void IncludeParser::dumpTokens()  {
+    string skClassName = this->className();
+    string fileName = skClassName + ".bmh";
+    fOut = fopen(fileName.c_str(), "wb");
+    if (!fOut) {
+        SkDebugf("could not open output file %s\n", fileName.c_str());
+        return;
+    }
+    string prefixName = skClassName.substr(0, 2);
+    string topicName = skClassName.length() > 2 && isupper(skClassName[2]) &&
+        ("Sk" == prefixName || "Gr" == prefixName) ? skClassName.substr(2) : skClassName;
+    fprintf(fOut, "#Topic %s", topicName.c_str());
+    this->lfAlways(2);
+    fprintf(fOut, "#Class %s", skClassName.c_str());
+    this->lfAlways(2);
+    auto& classMap = fIClassMap[skClassName];
+    auto& tokens = classMap.fTokens;
+    for (auto& token : tokens) {
+        if (Definition::Type::kMark != token.fType || MarkType::kComment != token.fMarkType) {
+            continue;
+        }
+        fprintf(fOut, "%.*s", (int) (token.fContentEnd - token.fContentStart),
+                token.fContentStart);
+        this->lfAlways(1);
+    }
+    this->lf(2);
+    string className(skClassName.substr(2));
+    vector<string> sortedClasses;
+    size_t maxLen = 0;
+    for (const auto& oneClass : fIClassMap) {
+        if (skClassName + "::" != oneClass.first.substr(0, skClassName.length() + 2)) {
+            continue;
+        }
+        string structName = oneClass.first.substr(skClassName.length() + 2);
+        maxLen = SkTMax(maxLen, structName.length());
+        sortedClasses.emplace_back(structName);
+    }
+    fprintf(fOut, "#Topic Overview");
+    this->lfAlways(2);
+    fprintf(fOut, "#Subtopic %s_Structs", className.c_str());
+    this->lfAlways(1);
+    fprintf(fOut, "#Table");
+    this->lfAlways(1);
+    fprintf(fOut, "#Legend");
+    this->lfAlways(1);
+    fprintf(fOut, "# %-*s # description ##", (int) maxLen, "struct");
+    this->lfAlways(1);
+    fprintf(fOut, "#Legend ##");
+    this->lfAlways(1);
+    fprintf(fOut, "#Table ##");
+    this->lfAlways(1);
+    for (auto& name : sortedClasses) {
+        fprintf(fOut, "# %-*s # ##", (int) maxLen, name.c_str());
+        this->lfAlways(1);
+    }
+    fprintf(fOut, "#Subtopic ##");
+    this->lfAlways(2);
+    fprintf(fOut, "#Subtopic %s_Member_Functions", className.c_str());
+    this->lfAlways(1);
+    fprintf(fOut, "#Table");
+    this->lfAlways(1);
+    fprintf(fOut, "#Legend");
+    this->lfAlways(1);
+    maxLen = 0;
+    vector<string> sortedNames;
+    for (const auto& token : classMap.fTokens) {
+        if (Definition::Type::kMark != token.fType || MarkType::kMethod != token.fMarkType) {
+            continue;
+        }
+        const string& name = token.fName;
+        if (name.substr(0, 7) == "android" || string::npos != name.find("nternal_")) {
+            continue;
+        }
+        if (name[name.length() - 2] == '_' && isdigit(name[name.length() - 1])) {
+            continue;
+        }
+        size_t paren = name.find('(');
+        size_t funcLen = string::npos == paren ? name.length() : paren;
+        maxLen = SkTMax(maxLen, funcLen);
+        sortedNames.emplace_back(name);
+    }
+    std::sort(sortedNames.begin(), sortedNames.end());
+    fprintf(fOut, "# %-*s # description   ##" "\n",
+            (int) maxLen, "function");
+    fprintf(fOut, "#Legend ##"                                                               "\n");
+    for (auto& name : sortedNames) {
+        size_t paren = name.find('(');
+        size_t funcLen = string::npos == paren ? name.length() : paren;
+        fprintf(fOut, "# %-*s # ##"                                                          "\n",
+                (int) maxLen, name.substr(0, funcLen).c_str());
+    }
+    fprintf(fOut, "#Table ##"                                                                "\n");
+    fprintf(fOut, "#Subtopic ##"                                                             "\n");
+    fprintf(fOut, ""                                                                         "\n");
+    fprintf(fOut, "#Topic ##"                                                                "\n");
+    fprintf(fOut, ""                                                                         "\n");
+
+    for (auto& oneClass : fIClassMap) {
+        if (skClassName + "::" != oneClass.first.substr(0, skClassName.length() + 2)) {
+            continue;
+        }
+        string innerName = oneClass.first.substr(skClassName.length() + 2);
+        fprintf(fOut, "%s",
+            "# ------------------------------------------------------------------------------");
+        this->lfAlways(2);
+        fprintf(fOut, "#Struct %s", innerName.c_str());
+        this->lfAlways(2);
+        for (auto& token : oneClass.second.fTokens) {
+            if (Definition::Type::kMark != token.fType || MarkType::kComment != token.fMarkType) {
+                continue;
+            }
+            fprintf(fOut, "%.*s", (int) (token.fContentEnd - token.fContentStart),
+                    token.fContentStart);
+            this->lfAlways(1);
+        }
+        this->lf(2);
+        this->dumpClassTokens(oneClass.second);
+        this->lf(2);
+        fprintf(fOut, "#Struct %s ##", innerName.c_str());
+        this->lfAlways(2);
+    }
+    this->dumpClassTokens(classMap);
+    fprintf(fOut, "#Class %s ##"                                                             "\n",
+            skClassName.c_str());
+    fprintf(fOut, ""                                                                         "\n");
+    fprintf(fOut, "#Topic %s ##"                                                             "\n",
+            topicName.c_str());
+    fclose(fOut);
+}
+
+bool IncludeParser::findComments(const Definition& includeDef, Definition* markupDef) {
+    // add comment preceding class, if any
+    const Definition* parent = includeDef.fParent;
+    int index = includeDef.fParentIndex;
+    auto wordIter = parent->fTokens.begin();
+    std::advance(wordIter, index);
+    SkASSERT(&*wordIter == &includeDef);
+    while (parent->fTokens.begin() != wordIter) {
+        auto testIter = std::prev(wordIter);
+        if (Definition::Type::kWord != testIter->fType
+            && Definition::Type::kKeyWord != testIter->fType
+            && (Definition::Type::kBracket != testIter->fType
+            || Bracket::kAngle != testIter->fBracket)
+            && (Definition::Type::kPunctuation != testIter->fType
+            || Punctuation::kAsterisk != testIter->fPunctuation)) {
+            break;
+        }
+        wordIter = testIter;
+    }
+    auto commentIter = wordIter;
+    while (parent->fTokens.begin() != commentIter) {
+        auto testIter = std::prev(commentIter);
+        bool isComment = Definition::Type::kBracket == testIter->fType
+                && (Bracket::kSlashSlash == testIter->fBracket
+                || Bracket::kSlashStar == testIter->fBracket);
+        if (!isComment) {
+            break;
+        }
+        commentIter = testIter;
+    }
+    while (commentIter != wordIter) {
+        if (!this->parseComment(commentIter->fFileName, commentIter->fContentStart,
+                commentIter->fContentEnd, commentIter->fLineCount, markupDef)) {
+            return false;
+        }
+        commentIter = std::next(commentIter);
+    }
+    return true;
+}
+
+// caller calls reportError, so just return false here
+bool IncludeParser::parseClass(Definition* includeDef, IsStruct isStruct) {
+    SkASSERT(includeDef->fTokens.size() > 0);
+    if (includeDef->fTokens.size() == 1) {
+        return true;  // forward declaration only
+    }
+    // parse class header
+    auto iter = includeDef->fTokens.begin();
+    if (!strncmp(iter->fStart, "SK_API", iter->fContentEnd - iter->fStart)) {
+        // todo : documentation is ignoring this for now
+        iter = std::next(iter);
+    }
+    string nameStr(iter->fStart, iter->fContentEnd - iter->fStart);
+    includeDef->fName = nameStr;
+    do {
+        if (iter == includeDef->fTokens.end()) {
+            return false;
+        }
+        if ('{' == iter->fStart[0] && Definition::Type::kPunctuation == iter->fType) {
+            break;
+        }   
+    } while (static_cast<void>(iter = std::next(iter)), true);
+    if (Punctuation::kLeftBrace != iter->fPunctuation) {
+        return false;
+    }
+    IClassDefinition* markupDef = this->defineClass(*includeDef, nameStr);
+    if (!markupDef) {
+        return false;
+    }
+    markupDef->fStart = iter->fStart;
+    if (!this->findComments(*includeDef, markupDef)) {
+        return false;
+    }
+//    if (1 != includeDef->fChildren.size()) {
+//        return false;  // fix me: SkCanvasClipVisitor isn't correctly parsed
+//    }
+    includeDef = includeDef->fChildren.front();
+    iter = includeDef->fTokens.begin();
+    // skip until public
+    int publicIndex = 0;
+    if (IsStruct::kNo == isStruct) {
+        const char* publicName = kKeyWords[(int) KeyWord::kPublic].fName;
+        size_t publicLen = strlen(publicName);
+        while (iter != includeDef->fTokens.end()
+                && (publicLen != (size_t) (iter->fContentEnd - iter->fStart)
+                || strncmp(iter->fStart, publicName, publicLen))) {
+            iter = std::next(iter);
+            ++publicIndex;
+        }
+    }
+    auto childIter = includeDef->fChildren.begin();
+    while (childIter != includeDef->fChildren.end() && (*childIter)->fParentIndex < publicIndex) {
+        (*childIter)->fPrivate = true;
+        childIter = std::next(childIter);
+    }
+    int lastPublic = publicIndex;
+    const char* protectedName = kKeyWords[(int) KeyWord::kProtected].fName;
+    size_t protectedLen = strlen(protectedName);
+    const char* privateName = kKeyWords[(int) KeyWord::kPrivate].fName;
+    size_t privateLen = strlen(privateName);
+    while (iter != includeDef->fTokens.end()
+            && (protectedLen != (size_t) (iter->fContentEnd - iter->fStart)
+            || strncmp(iter->fStart, protectedName, protectedLen))
+            && (privateLen != (size_t) (iter->fContentEnd - iter->fStart)
+            || strncmp(iter->fStart, privateName, privateLen))) {
+        iter = std::next(iter);
+        ++lastPublic;
+    }
+    while (childIter != includeDef->fChildren.end() && (*childIter)->fParentIndex < lastPublic) {
+        Definition* child = *childIter;
+        if (!this->parseObject(child, markupDef)) {
+            return false;
+        }
+        childIter = std::next(childIter);
+    }
+    while (childIter != includeDef->fChildren.end()) {
+        (*childIter)->fPrivate = true;
+        childIter = std::next(childIter);
+    }
+    SkASSERT(fParent->fParent);
+    fParent = fParent->fParent;
+    return true;
+}
+
+bool IncludeParser::parseComment(const string& filename, const char* start, const char* end,
+        int lineCount, Definition* markupDef) {
+    TextParser parser(filename, start, end, lineCount);
+    // parse doxygen if present
+    if (parser.startsWith("**")) {
+        parser.next();
+        parser.next();
+        parser.skipWhiteSpace();
+        if ('\\' == parser.peek()) {
+            parser.next();
+            if (!parser.skipWord(kKeyWords[(int) markupDef->fKeyWord].fName)) {
+                return reportError<bool>("missing object type");
+            }
+            if (!parser.skipWord(markupDef->fName.c_str())) {
+                return reportError<bool>("missing object name");
+            }
+
+        }
+    }
+    // remove leading '*' if present
+    Definition* parent = markupDef->fTokens.size() ? &markupDef->fTokens.back() : markupDef;
+    while (!parser.eof() && parser.skipWhiteSpace()) {
+        while ('*' == parser.peek()) {
+            parser.next();
+            if (parser.eof()) {
+                break;
+            }
+            parser.skipWhiteSpace();
+        }
+        if (parser.eof()) {
+            break;
+        }
+        const char* lineEnd = parser.trimmedLineEnd();
+        markupDef->fTokens.emplace_back(MarkType::kComment, parser.fChar, lineEnd, 
+                parser.fLineCount, parent);
+        parser.skipToEndBracket('\n');
+    }
+    return true;
+}
+
+bool IncludeParser::parseDefine() {
+
+    return true;
+}
+
+bool IncludeParser::parseEnum(Definition* child, Definition* markupDef) {
+    string nameStr;
+    if (child->fTokens.size() > 0) {
+        auto token = child->fTokens.begin();
+        if (Definition::Type::kKeyWord == token->fType && KeyWord::kClass == token->fKeyWord) {
+            token = token->fTokens.begin();
+        }
+        if (Definition::Type::kWord == token->fType) {
+            nameStr += string(token->fStart, token->fContentEnd - token->fStart);
+        }
+    }
+    markupDef->fTokens.emplace_back(MarkType::kEnum, child->fContentStart, child->fContentEnd,
+        child->fLineCount, markupDef);
+    Definition* markupChild = &markupDef->fTokens.back();
+    SkASSERT(KeyWord::kNone == markupChild->fKeyWord);
+    markupChild->fKeyWord = KeyWord::kEnum;
+    TextParser enumName(child);
+    enumName.skipExact("enum ");
+    const char* nameStart = enumName.fChar;
+    enumName.skipToSpace();
+    markupChild->fName = markupDef->fName + "::" + 
+            string(nameStart, (size_t) (enumName.fChar - nameStart));
+    if (!this->findComments(*child, markupChild)) {
+        return false;
+    }
+    TextParser parser(child);
+    parser.skipToEndBracket('{');
+    const char* dataEnd;
+    do {
+        parser.next();
+        parser.skipWhiteSpace();
+        if ('}' == parser.peek()) {
+            break;
+        }
+        Definition* comment = nullptr;
+        // note that comment, if any, can be before or after (on the same line, though) as member
+        if ('#' == parser.peek()) {
+            // fixme: handle preprecessor, but just skip it for now
+            parser.skipToLineStart();
+        }
+        while (parser.startsWith("/*") || parser.startsWith("//")) {
+            parser.next();
+            const char* start = parser.fChar;
+            const char* end;
+            if ('*' == parser.peek()) {
+                end = parser.strnstr("*/", parser.fEnd);
+                parser.fChar = end;
+                parser.next();
+                parser.next();
+            } else {
+                end = parser.trimmedLineEnd();
+                parser.skipToLineStart();
+            }
+            markupChild->fTokens.emplace_back(MarkType::kComment, start, end, parser.fLineCount,
+                    markupChild);
+            comment = &markupChild->fTokens.back();
+            comment->fTerminator = end;
+            if (!this->parseComment(parser.fFileName, start, end, parser.fLineCount, comment)) {
+                return false;
+            }
+            parser.skipWhiteSpace();
+        }
+        parser.skipWhiteSpace();
+        const char* memberStart = parser.fChar;
+        if ('}' == memberStart[0]) {
+            break;
+        }
+        parser.skipToNonAlphaNum();
+        string memberName(memberStart, parser.fChar);
+        parser.skipWhiteSpace();
+        const char* dataStart = parser.fChar;
+        SkASSERT('=' == dataStart[0] || ',' == dataStart[0] || '}' == dataStart[0]
+                 || '/' == dataStart[0]);
+        dataEnd = parser.anyOf(",}");
+        markupChild->fTokens.emplace_back(MarkType::kMember, dataStart, dataEnd, parser.fLineCount,
+                markupChild);
+        Definition* member = &markupChild->fTokens.back();
+        member->fName = memberName;
+        if (comment) {
+            member->fChildren.push_back(comment);
+        }
+        markupChild->fChildren.push_back(member);
+        parser.skipToEndBracket(dataEnd[0]);
+    } while (',' == dataEnd[0]);
+    for (size_t index = 1; index < child->fChildren.size(); ++index) {
+        const Definition* follower = child->fChildren[index];
+        if (Definition::Type::kKeyWord == follower->fType) {
+            markupChild->fTokens.emplace_back(MarkType::kMember, follower->fContentStart, 
+                    follower->fContentEnd, follower->fLineCount, markupChild);
+            Definition* member = &markupChild->fTokens.back();
+            member->fName = follower->fName;
+            markupChild->fChildren.push_back(member);
+        }
+    }
+    IClassDefinition& classDef = fIClassMap[markupDef->fName];
+    SkASSERT(classDef.fStart);
+    string uniqueName = this->uniqueName(classDef.fEnums, nameStr);
+    markupChild->fName = uniqueName;
+    classDef.fEnums[uniqueName] = markupChild;
+    return true;
+}
+
+bool IncludeParser::parseInclude(const string& name) {
+    fParent = &fIncludeMap[name];
+    fParent->fName = name;
+    fParent->fFileName = fFileName;
+    fParent->fType = Definition::Type::kFileType;
+    fParent->fContentStart = fChar;
+    fParent->fContentEnd = fEnd;
+    // parse include file into tree
+    while (fChar < fEnd) {
+        if (!this->parseChar()) {
+            return false;
+        }
+    }
+    // parse tree and add named objects to maps
+    fParent = &fIncludeMap[name];
+    if (!this->parseObjects(fParent, nullptr)) {
+        return false;
+    }
+    return true;
+}
+
+bool IncludeParser::parseMember(Definition* child, Definition* markupDef) {
+    const char* typeStart = child->fChildren[0]->fContentStart;
+    markupDef->fTokens.emplace_back(MarkType::kMember, typeStart, child->fContentStart,
+        child->fLineCount, markupDef);
+    Definition* markupChild = &markupDef->fTokens.back();
+    TextParser nameParser(child);
+    nameParser.skipToNonAlphaNum();
+    string nameStr = string(child->fContentStart, nameParser.fChar - child->fContentStart);
+    IClassDefinition& classDef = fIClassMap[markupDef->fName];
+    string uniqueName = this->uniqueName(classDef.fMethods, nameStr);
+    markupChild->fName = uniqueName;
+    classDef.fMembers[uniqueName] = markupChild;
+    if (child->fParentIndex >= 2) {
+        auto comment = child->fParent->fTokens.begin();
+        std::advance(comment, child->fParentIndex - 2);
+        if (Definition::Type::kBracket == comment->fType
+                && (Bracket::kSlashStar == comment->fBracket
+                || Bracket::kSlashSlash == comment->fBracket)) {
+            TextParser parser(&*comment);
+            do {
+                parser.skipToAlpha();
+                if (parser.eof()) {
+                    break;
+                }
+                const char* start = parser.fChar;
+                const char* end = parser.trimmedBracketEnd('\n', OneLine::kYes);
+                if (Bracket::kSlashStar == comment->fBracket) {
+                    const char* commentEnd = parser.strnstr("*/", end);
+                    if (commentEnd) {
+                        end = commentEnd;
+                    }
+                }
+                markupDef->fTokens.emplace_back(MarkType::kComment, start, end, child->fLineCount,
+                        markupDef);
+                Definition* commentChild = &markupDef->fTokens.back();
+                markupChild->fChildren.emplace_back(commentChild);
+                parser.skipTo(end);
+            } while (!parser.eof());
+        }
+    }
+    return true;
+}
+
+bool IncludeParser::parseMethod(Definition* child, Definition* markupDef) {
+    auto tokenIter = child->fParent->fTokens.begin();
+    std::advance(tokenIter, child->fParentIndex);
+    tokenIter = std::prev(tokenIter);
+    string nameStr(tokenIter->fStart, tokenIter->fContentEnd - tokenIter->fStart);
+    while (tokenIter != child->fParent->fTokens.begin()) {
+        auto testIter = std::prev(tokenIter);
+        switch (testIter->fType) {
+            case Definition::Type::kWord:
+                goto keepGoing;
+            case Definition::Type::kKeyWord: {
+                KeyProperty keyProperty = kKeyWords[(int) testIter->fKeyWord].fProperty;
+                if (KeyProperty::kNumber == keyProperty || KeyProperty::kModifier == keyProperty) {
+                    goto keepGoing;
+                }
+            } break;
+            case Definition::Type::kBracket:
+                if (Bracket::kAngle == testIter->fBracket) {
+                    goto keepGoing;
+                }
+                break;
+            case Definition::Type::kPunctuation:
+                if (Punctuation::kSemicolon == testIter->fPunctuation
+                        || Punctuation::kLeftBrace == testIter->fPunctuation
+                        || Punctuation::kColon == testIter->fPunctuation) {
+                    break;
+                }
+            keepGoing:
+                tokenIter = testIter;
+                continue;
+            default:
+                break;
+        }
+        break;
+    }
+    tokenIter->fName = nameStr;
+    tokenIter->fMarkType = MarkType::kMethod;
+    auto testIter = child->fParent->fTokens.begin();
+    SkASSERT(child->fParentIndex > 0);
+    std::advance(testIter, child->fParentIndex - 1);
+    const char* start = tokenIter->fContentStart;
+    const char* end = tokenIter->fContentEnd;
+    const char kDebugCodeStr[] = "SkDEBUGCODE";
+    const size_t kDebugCodeLen = sizeof(kDebugCodeStr) - 1;
+    if (end - start == kDebugCodeLen && !strncmp(start, kDebugCodeStr, kDebugCodeLen)) {
+        std::advance(testIter, 1);
+        start = testIter->fContentStart + 1;
+        end = testIter->fContentEnd - 1;
+    } else {
+        end = testIter->fContentEnd;
+        while (testIter != child->fParent->fTokens.end()) {
+            testIter = std::next(testIter);
+            switch (testIter->fType) {
+                case Definition::Type::kPunctuation:
+                    SkASSERT(Punctuation::kSemicolon == testIter->fPunctuation
+                            || Punctuation::kLeftBrace == testIter->fPunctuation
+                            || Punctuation::kColon == testIter->fPunctuation);
+                    end = testIter->fStart;
+                    break;
+                case Definition::Type::kKeyWord: {
+                    KeyProperty keyProperty = kKeyWords[(int) testIter->fKeyWord].fProperty;
+                    if (KeyProperty::kNumber == keyProperty || KeyProperty::kModifier == keyProperty) {
+                        continue;
+                    }
+                    } break;
+                default:
+                    continue;
+            }
+            break;
+        }
+    }
+    markupDef->fTokens.emplace_back(MarkType::kMethod, start, end, tokenIter->fLineCount,
+            markupDef);
+    Definition* markupChild = &markupDef->fTokens.back();
+    // do find instead -- I wonder if there is a way to prevent this in c++
+    IClassDefinition& classDef = fIClassMap[markupDef->fName];
+    SkASSERT(classDef.fStart);
+    string uniqueName = this->uniqueName(classDef.fMethods, nameStr);
+    markupChild->fName = uniqueName;
+    if (!this->findComments(*child, markupChild)) {
+        return false;
+    }
+    classDef.fMethods[uniqueName] = markupChild;
+    return true;
+}
+
+void IncludeParser::keywordEnd() {
+    fprintf(fOut, "##");
+    this->lfAlways(1);
+}
+
+void IncludeParser::keywordStart(const char* keyword) {
+    this->lf(1);
+    fprintf(fOut, "#%s ", keyword);
+}
+
+bool IncludeParser::parseObjects(Definition* parent, Definition* markupDef) {
+    for (auto& child : parent->fChildren) {
+        if (!this->parseObject(child, markupDef)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+bool IncludeParser::parseObject(Definition* child, Definition* markupDef) {
+    // set up for error reporting
+    fLine = fChar = child->fStart;
+    fEnd = child->fContentEnd;
+    // todo: put original line number in child as well
+    switch (child->fType) {
+        case Definition::Type::kKeyWord:
+            switch (child->fKeyWord) {
+                case KeyWord::kClass: 
+                    if (!this->parseClass(child, IsStruct::kNo)) {
+                        return this->reportError<bool>("failed to parse class");
+                    }
+                    break;
+                case KeyWord::kEnum:
+                    if (!this->parseEnum(child, markupDef)) {
+                        return this->reportError<bool>("failed to parse enum");
+                    }
+                    break;
+                case KeyWord::kStruct:
+                    if (!this->parseClass(child, IsStruct::kYes)) {
+                        return this->reportError<bool>("failed to parse struct");
+                    }
+                    break;
+                case KeyWord::kTemplate:
+                    if (!this->parseTemplate()) {
+                        return this->reportError<bool>("failed to parse template");
+                    }
+                    break;
+                case KeyWord::kTypedef:
+                    if (!this->parseTypedef()) {
+                        return this->reportError<bool>("failed to parse typedef");
+                    }
+                    break;
+                case KeyWord::kUnion:
+                    if (!this->parseUnion()) {
+                        return this->reportError<bool>("failed to parse union");
+                    }
+                    break;
+                default:
+                    return this->reportError<bool>("unhandled keyword");
+            }
+            break;
+        case Definition::Type::kBracket:
+            switch (child->fBracket) {
+                case Bracket::kParen:
+                    if (!this->parseMethod(child, markupDef)) {
+                        return this->reportError<bool>("failed to parse method");
+                    }
+                    break;
+                case Bracket::kSlashSlash:
+                case Bracket::kSlashStar:
+                    // comments are picked up by parsing objects first
+                    break;
+                case Bracket::kPound:
+                    // special-case the #xxx xxx_DEFINED entries
+                    switch (child->fKeyWord) {
+                        case KeyWord::kIfndef:
+                        case KeyWord::kIfdef:
+                            if (child->boilerplateIfDef(fParent)) {
+                                if (!this->parseObjects(child, markupDef)) {
+                                    return false;
+                                }
+                                break;
+                            }
+                            goto preproError;
+                        case KeyWord::kDefine:
+                            if (child->boilerplateDef(fParent)) {
+                                break;
+                            }
+                            goto preproError;
+                        case KeyWord::kEndif:
+                            if (child->boilerplateEndIf()) {
+                                break;
+                            }
+                        case KeyWord::kInclude:
+                            // ignored for now
+                            break;
+                        case KeyWord::kElse:
+                        case KeyWord::kElif:
+                            // todo: handle these
+                            break;
+                        default:
+                        preproError:
+                            return this->reportError<bool>("unhandled preprocessor");
+                    }
+                    break;
+                case Bracket::kAngle:
+                    // pick up templated function pieces when method is found
+                    break;
+                default:
+                    return this->reportError<bool>("unhandled bracket");
+            }
+            break;
+        case Definition::Type::kWord:
+            if (MarkType::kMember != child->fMarkType) {
+                return this->reportError<bool>("unhandled word type");
+            }
+            if (!this->parseMember(child, markupDef)) {
+                return this->reportError<bool>("unparsable member");
+            }
+            break;
+        default:
+            return this->reportError<bool>("unhandled type");
+            break;
+    }
+    return true;
+}
+
+bool IncludeParser::parseTemplate() {
+
+    return true;
+}
+
+bool IncludeParser::parseTypedef() {
+
+    return true;
+}
+
+bool IncludeParser::parseUnion() {
+
+    return true;
+}
+
+bool IncludeParser::parseChar() {
+    char test = *fChar;
+    if ('\\' == fPrev) {
+        if ('\n' == test) {
+            ++fLineCount;
+            fLine = fChar + 1;
+        }
+        goto done;
+    }
+    switch (test) {
+        case '\n':
+            ++fLineCount;
+            fLine = fChar + 1;
+            if (fInChar) {
+                return reportError<bool>("malformed char");
+            }
+            if (fInString) {
+                return reportError<bool>("malformed string");
+            }
+            if (!this->checkForWord()) {
+                return false;
+            }
+            if (Bracket::kPound == this->topBracket()) {
+                KeyWord keyWord = fParent->fKeyWord;
+                if (KeyWord::kNone == keyWord) {
+                    return this->reportError<bool>("unhandled preprocessor directive");
+                }
+                if (KeyWord::kInclude == keyWord || KeyWord::kDefine == keyWord) {
+                    this->popBracket();
+                }
+            } else if (Bracket::kSlashSlash == this->topBracket()) {
+                this->popBracket();
+            }
+            break;
+        case '*':
+            if (!fInCharCommentString && '/' == fPrev) {
+                this->pushBracket(Bracket::kSlashStar);
+            }
+            if (!this->checkForWord()) {
+                return false;
+            }
+            if (!fInCharCommentString) {
+                this->addPunctuation(Punctuation::kAsterisk);
+            }
+            break;
+        case '/':
+            if ('*' == fPrev) {
+                if (!fInCharCommentString) {
+                    return reportError<bool>("malformed closing comment");
+                }
+                if (Bracket::kSlashStar == this->topBracket()) {
+                    this->popBracket();
+                }
+                break;
+            } 
+            if (!fInCharCommentString && '/' == fPrev) {
+                this->pushBracket(Bracket::kSlashSlash);
+                break;
+            }
+            if (!this->checkForWord()) {
+                return false;
+            }
+            break;
+        case '\'':
+            if (Bracket::kChar == this->topBracket()) {
+                this->popBracket();
+            } else if (!fInComment && !fInString) {
+                if (fIncludeWord) {
+                    return this->reportError<bool>("word then single-quote");
+                }
+                this->pushBracket(Bracket::kChar);
+            }
+            break;
+        case '\"':
+            if (Bracket::kString == this->topBracket()) {
+                this->popBracket();
+            } else if (!fInComment && !fInChar) {
+                if (fIncludeWord) {
+                    return this->reportError<bool>("word then double-quote");
+                }
+                this->pushBracket(Bracket::kString);
+            }
+            break;
+        case ':':
+        case '(':
+        case '[':
+        case '{': {
+            if (fInCharCommentString) {
+                break;
+            }
+            if (':' == test && (fInBrace || ':' == fChar[-1] || ':' == fChar[1])) {
+                break;
+            }
+            if (!fInBrace) {
+                if (!this->checkForWord()) {
+                    return false;
+                }
+                if (':' == test && !fInFunction) {
+                    break;
+                }
+                if ('{' == test) {
+                    this->addPunctuation(Punctuation::kLeftBrace);
+                } else if (':' == test) {
+                    this->addPunctuation(Punctuation::kColon);
+                }
+            }
+            if (fInBrace && '{' == test && Definition::Type::kBracket == fInBrace->fType
+                    && Bracket::kColon == fInBrace->fBracket) {
+                Definition* braceParent = fParent->fParent;
+                braceParent->fChildren.pop_back();
+                braceParent->fTokens.pop_back();
+                fParent = braceParent;
+                fInBrace = nullptr;
+            }
+            this->pushBracket(
+                    '(' == test ? Bracket::kParen :
+                    '[' == test ? Bracket::kSquare :
+                    '{' == test ? Bracket::kBrace :
+                                  Bracket::kColon);
+            if (!fInBrace
+                    && ('{' == test || (':' == test && ' ' >= fChar[1]))
+                    && fInFunction) {
+                fInBrace = fParent;
+            }
+            } break;
+        case '<':
+            if (fInCharCommentString || fInBrace) {
+                break;
+            }
+            if (!this->checkForWord()) {
+                return false;
+            }
+            if (fInEnum) {
+                break;
+            }
+            this->pushBracket(Bracket::kAngle);
+            break;
+        case ')':
+        case ']':
+        case '}': {
+            if (fInCharCommentString) {
+                break;
+            }
+            if (!fInBrace) {
+                if (!this->checkForWord()) {
+                    return false;
+                }
+            }
+            bool popBraceParent = fInBrace == fParent;
+            if ((')' == test ? Bracket::kParen :
+                    ']' == test ? Bracket::kSquare : Bracket::kBrace) == this->topBracket()) {
+                this->popBracket();
+                if (!fInFunction) {
+                    bool deprecatedMacro = false;
+                    if (')' == test) {
+                        auto iter = fParent->fTokens.end();
+                        bool lookForWord = false;
+                        while (fParent->fTokens.begin() != iter) {
+                            --iter;
+                            if (lookForWord) {
+                                if (Definition::Type::kWord != iter->fType) {
+                                    break;
+                                }
+                                string word(iter->fContentStart, iter->length());
+                                if ("SK_ATTR_EXTERNALLY_DEPRECATED" == word) {
+                                    deprecatedMacro = true;
+                                    // remove macro paren (would confuse method parsing later)
+                                    fParent->fTokens.pop_back();  
+                                    fParent->fChildren.pop_back();
+                                }
+                                break;
+                            }
+                            if (Definition::Type::kBracket != iter->fType) {
+                                break;
+                            }
+                            if (Bracket::kParen != iter->fBracket) {
+                                break;
+                            }
+                            lookForWord = true;
+                        }
+                    }
+                    fInFunction = ')' == test && !deprecatedMacro;
+                } else {
+                    fInFunction = '}' != test;
+                }
+            } else {
+                return reportError<bool>("malformed close bracket");
+            }
+            if (popBraceParent) {
+                Definition* braceParent = fInBrace->fParent;
+                braceParent->fChildren.pop_back();
+                braceParent->fTokens.pop_back();
+                fInBrace = nullptr;
+            }
+            } break;
+        case '>':
+            if (fInCharCommentString || fInBrace) {
+                break;
+            }
+            if (!this->checkForWord()) {
+                return false;
+            }
+            if (fInEnum) {
+                break;
+            }
+            if (Bracket::kAngle == this->topBracket()) {
+                this->popBracket();
+            } else {
+                return reportError<bool>("malformed close angle bracket");
+            }
+            break;
+        case '#': {
+            if (fInCharCommentString || fInBrace) {
+                break;
+            }
+            SkASSERT(!fIncludeWord);  // don't expect this, curious if it is triggered
+            this->pushBracket(Bracket::kPound);
+            break;
+        }
+        case '&':
+        case ',':
+        case ' ':
+            if (fInCharCommentString || fInBrace) {
+                break;
+            }
+            if (!this->checkForWord()) {
+                return false;
+            }
+            break;
+        case ';':
+            if (fInCharCommentString || fInBrace) {
+                break;
+            }
+            if (!this->checkForWord()) {
+                return false;
+            }
+            if (Definition::Type::kKeyWord == fParent->fType
+                    && KeyProperty::kObject == (kKeyWords[(int) fParent->fKeyWord].fProperty)) {
+                if (KeyWord::kEnum == fParent->fKeyWord) {
+                    fInEnum = false;
+                }
+                this->popObject();
+            } else if (Definition::Type::kBracket == fParent->fType
+                    && fParent->fParent && Definition::Type::kKeyWord == fParent->fParent->fType
+                    && KeyWord::kStruct == fParent->fParent->fKeyWord) {
+                list<Definition>::iterator baseIter = fParent->fTokens.end();
+                list<Definition>::iterator namedIter  = fParent->fTokens.end();
+                for (auto tokenIter = fParent->fTokens.end();
+                        fParent->fTokens.begin() != tokenIter--; ) {
+                    if (tokenIter->fLineCount == fLineCount) {
+                        if ('f' == tokenIter->fStart[0] && isupper(tokenIter->fStart[1])) {
+                            if (namedIter != fParent->fTokens.end()) {
+                                return reportError<bool>("found two named member tokens");
+                            }
+                            namedIter = tokenIter;
+                        }
+                        baseIter = tokenIter;
+                    } else {
+                        break;
+                    }
+                }
+                // FIXME: if a member definition spans multiple lines, this won't work
+                if (namedIter != fParent->fTokens.end()) {
+                    if (baseIter == namedIter) {
+                        return this->reportError<bool>("expected type before named token");
+                    }
+                    Definition* member = &*namedIter;
+                    member->fMarkType = MarkType::kMember;
+                    fParent->fChildren.push_back(member);
+                    for (auto nameType = baseIter; nameType != namedIter; ++nameType) {
+                        member->fChildren.push_back(&*nameType);
+                    }
+
+                }
+            } else if (fParent->fChildren.size() > 0) {
+                auto lastIter = fParent->fChildren.end();
+                Definition* priorEnum;
+                while (fParent->fChildren.begin() != lastIter) {
+                    std::advance(lastIter, -1);
+                    priorEnum = *lastIter;
+                    if (Definition::Type::kBracket != priorEnum->fType ||
+                            (Bracket::kSlashSlash != priorEnum->fBracket
+                            && Bracket::kSlashStar != priorEnum->fBracket)) {
+                        break;
+                    }
+                }
+                if (Definition::Type::kKeyWord == priorEnum->fType
+                        && KeyWord::kEnum == priorEnum->fKeyWord) {
+                    auto tokenWalker = fParent->fTokens.begin();
+                    std::advance(tokenWalker, priorEnum->fParentIndex);
+                    SkASSERT(KeyWord::kEnum == tokenWalker->fKeyWord);
+                    while (tokenWalker != fParent->fTokens.end()) {
+                        std::advance(tokenWalker, 1);
+                        if (Punctuation::kSemicolon == tokenWalker->fPunctuation) {
+                            break;
+                        }
+                    }
+                    while (tokenWalker != fParent->fTokens.end()) {
+                        std::advance(tokenWalker, 1);
+                        const Definition* test = &*tokenWalker;
+                        if (Definition::Type::kBracket != test->fType ||
+                                (Bracket::kSlashSlash != test->fBracket
+                                && Bracket::kSlashStar != test->fBracket)) {
+                            break;
+                        }
+                    }
+                    Definition* start = &*tokenWalker;
+                    bool foundExpected = true;
+                    for (KeyWord expected : {KeyWord::kStatic, KeyWord::kConstExpr, KeyWord::kInt}){
+                        const Definition* test = &*tokenWalker;
+                        if (expected != test->fKeyWord) {
+                            foundExpected = false;
+                            break;
+                        }
+                        if (tokenWalker == fParent->fTokens.end()) {
+                            break;
+                        }
+                        std::advance(tokenWalker, 1);
+                    }
+                    if (foundExpected && tokenWalker != fParent->fTokens.end()) {
+                        const char* nameStart = tokenWalker->fStart;
+                        std::advance(tokenWalker, 1);
+                        if (tokenWalker != fParent->fTokens.end()) {
+                            TextParser tp(fFileName, nameStart, tokenWalker->fStart, fLineCount);
+                            tp.skipToNonAlphaNum();
+                            start->fName = string(nameStart, tp.fChar - nameStart);
+                            start->fContentEnd = fChar;
+                            priorEnum->fChildren.emplace_back(start);
+                       }
+                    }
+                }
+            }
+            this->addPunctuation(Punctuation::kSemicolon);
+            fInFunction = false;
+            break;
+        case '~':
+            if (fInEnum) {
+                break;
+            }
+        case '0': case '1': case '2': case '3': case '4':
+        case '5': case '6': case '7': case '8': case '9':
+            // TODO: don't want to parse numbers, but do need to track for enum defs
+        //    break;
+        case 'A': case 'B': case 'C': case 'D': case 'E':
+        case 'F': case 'G': case 'H': case 'I': case 'J':
+        case 'K': case 'L': case 'M': case 'N': case 'O':
+        case 'P': case 'Q': case 'R': case 'S': case 'T':
+        case 'U': case 'V': case 'W': case 'X': case 'Y':
+        case 'Z': case '_':
+        case 'a': case 'b': case 'c': case 'd': case 'e':
+        case 'f': case 'g': case 'h': case 'i': case 'j':
+        case 'k': case 'l': case 'm': case 'n': case 'o':
+        case 'p': case 'q': case 'r': case 's': case 't':
+        case 'u': case 'v': case 'w': case 'x': case 'y':
+        case 'z': 
+            if (fInCharCommentString || fInBrace) {
+                break;
+            }
+            if (!fIncludeWord) {
+                fIncludeWord = fChar;
+            }
+            break;
+    }
+done:
+    fPrev = test;
+    ++fChar;
+    return true;
+}
+
+void IncludeParser::validate() const {
+    for (int index = 0; index <= (int) Last_MarkType; ++index) {
+        SkASSERT(fMaps[index].fMarkType == (MarkType) index);
+    }
+    IncludeParser::ValidateKeyWords();
+}