docs with more pop

Replace a hunk of documentation in docs/*.bmh
with #Populate, which instructs bookmaker to
retrieve the documentation from include/core.

Check spelling for all documentation retrieved
from include/core against Skia declarations
and a list of words in spelling.txt.

TBR=caryclark@google.com
Docs-Preview: https://skia.org/?cl=163491
Bug: skia:
Change-Id: If057c3a1336e312ad59c084a3a130f0276802496
Reviewed-on: https://skia-review.googlesource.com/c/163491
Commit-Queue: Cary Clark <caryclark@skia.org>
Reviewed-by: Cary Clark <caryclark@skia.org>
Auto-Submit: Cary Clark <caryclark@skia.org>
diff --git a/tools/bookmaker/bookmaker.cpp b/tools/bookmaker/bookmaker.cpp
index 7f0b9f9..beb7dd6 100644
--- a/tools/bookmaker/bookmaker.cpp
+++ b/tools/bookmaker/bookmaker.cpp
@@ -12,6 +12,8 @@
 #include <Windows.h>
 #endif
 
+const string kSpellingFileName("spelling.txt");
+
 DEFINE_string2(status, a, "", "File containing status of documentation. (Use in place of -b -i)");
 DEFINE_string2(bmh, b, "", "Path to a *.bmh file or a directory.");
 DEFINE_bool2(catalog, c, false, "Write example catalog.htm. (Requires -b -f -r)");
@@ -32,6 +34,8 @@
 DEFINE_bool2(validate, V, false, "Validate that all anchor references have definitions. (Requires -r)");
 DEFINE_bool2(skip, z, false, "Skip degenerate missed in legacy preprocessor.");
 
+// -b docs -i include/core/SkRect.h -f fiddleout.json -r site/user/api
+// -b docs/SkIRect_Reference.bmh -H
 /* todos:
 
 if #Subtopic contains #SeeAlso or #Example generate horizontal rule at end
@@ -217,6 +221,9 @@
             if (nullptr == fRoot) {
                 fRoot = this->findBmhObject(markType, name);
                 fRoot->fFileName = fFileName;
+                fRoot->fName = name;
+                fRoot->fNames.fName = name;
+                fRoot->fNames.fParent = &fGlobalNames;
                 definition = fRoot;
             } else {
                 if (nullptr == fParent) {
@@ -241,6 +248,10 @@
                         (fRoot->fBranches)[name] = childRoot;
                         childRoot->setRootParent(fRoot);
                         childRoot->fFileName = fFileName;
+                        SkASSERT(MarkType::kSubtopic != fRoot->fMarkType
+                                && MarkType::kTopic != fRoot->fMarkType);
+                        childRoot->fNames.fName = name;
+                        childRoot->fNames.fParent = &fRoot->fNames;
                         fRoot = childRoot;
                         definition = fRoot;
                     } else {
@@ -353,6 +364,7 @@
                     definition->fName += typeNameBuilder[0];
                     definition->fFiddle = parent->fFiddle + '_';
                 }
+                rootDefinition->fNames.fName = rootDefinition->fName;
                 definition->fFiddle += Definition::NormalizedName(typeNameBuilder[0]);
                 this->setAsParent(definition);
             }
@@ -897,7 +909,7 @@
         }
         this->skipToAlpha();
         const char* wordStart = fChar;
-        this->skipToNonName();
+        this->skipToWhiteSpace();
         if (fChar - wordStart > 0) {
             fExternals.emplace_front(MarkType::kExternal, wordStart, fChar, fLineCount, fParent,
                     fMC);
@@ -1225,6 +1237,237 @@
     return result;
 }
 
+string BmhParser::loweredTopic(string name, Definition* def) {
+    string lowered;
+    SkASSERT('_' != name[0]);
+    char last = '_';
+    for (char c : name) {
+        SkASSERT(' ' != c);
+        if (isupper(last)) {
+            lowered += islower(c) ? tolower(last) : last;
+            last = '\0';
+        }
+        if ('_' == c) {
+            last = c;
+            c = ' ';
+        } else if ('_' == last && isupper(c)) {
+            last = c;
+            continue;
+        }
+        lowered += c;
+        if (' ' == c) {
+            this->setUpPartialSubstitute(lowered);
+        }
+    }
+    if (isupper(last)) {
+        lowered += tolower(last);
+    }
+    return lowered;
+}
+
+void BmhParser::setUpGlobalSubstitutes() {
+    for (auto& entry : fExternals) {
+        string externalName = entry.fName;
+        SkASSERT(fGlobalNames.fRefMap.end() == fGlobalNames.fRefMap.find(externalName));
+        fGlobalNames.fRefMap[externalName] = nullptr;
+    }
+    for (auto bMap : { &fClassMap, &fConstMap, &fDefineMap, &fEnumMap, &fMethodMap,
+            &fTypedefMap } ) {
+        for (auto& entry : *bMap) {
+            Definition* parent = (Definition*) &entry.second;
+            string name = parent->fName;
+            SkASSERT(fGlobalNames.fLinkMap.end() == fGlobalNames.fLinkMap.find(name));
+            string ref = ParserCommon::HtmlFileName(parent->fFileName) + '#' + parent->fFiddle;
+            fGlobalNames.fLinkMap[name] = ref;
+            SkASSERT(fGlobalNames.fRefMap.end() == fGlobalNames.fRefMap.find(name));
+            fGlobalNames.fRefMap[name] = const_cast<Definition*>(parent);
+            NameMap* names = MarkType::kClass == parent->fMarkType
+                    || MarkType::kStruct == parent->fMarkType
+                    || MarkType::kEnumClass == parent->fMarkType ? &parent->asRoot()->fNames :
+                    &fGlobalNames;
+            this->setUpSubstitutes(parent, names);
+            if (names != &fGlobalNames) {
+                names->copyToParent(&fGlobalNames);
+            }
+        }
+    }
+    for (auto& topic : fTopicMap) {
+        bool hasSubstitute = false;
+        for (auto& child : topic.second->fChildren) {
+            bool isAlias = MarkType::kAlias == child->fMarkType;
+            bool isSubstitute = MarkType::kSubstitute == child->fMarkType;
+            if (!isAlias && !isSubstitute) {
+                continue;
+            }
+            hasSubstitute |= isSubstitute;
+            string name(child->fContentStart, child->length());
+            if (isAlias) {
+                name = ParserCommon::ConvertRef(name, false);
+                for (auto aliasChild : child->fChildren) {
+                    if (MarkType::kSubstitute == aliasChild->fMarkType) {
+                        string sub(aliasChild->fContentStart, aliasChild->length());
+                        this->setUpSubstitute(sub, topic.second);
+                    }
+                }
+            }
+            this->setUpSubstitute(name, topic.second);
+        }
+        if (hasSubstitute) {
+            continue;
+        }
+        string lowered = this->loweredTopic(topic.first, topic.second);
+        SkDEBUGCODE(auto globalIter = fGlobalNames.fLinkMap.find(lowered));
+        SkASSERT(fGlobalNames.fLinkMap.end() == globalIter);
+        fGlobalNames.fLinkMap[lowered] =
+                ParserCommon::HtmlFileName(topic.second->fFileName) + '#' + topic.first;
+        SkASSERT(fGlobalNames.fRefMap.end() == fGlobalNames.fRefMap.find(lowered));
+        fGlobalNames.fRefMap[lowered] = topic.second;
+    }
+    size_t slash = fRawFilePathDir.rfind('/');
+    size_t bslash = fRawFilePathDir.rfind('\\');
+    string spellFile;
+    if (string::npos == slash && string::npos == bslash) {
+        spellFile = fRawFilePathDir;
+    } else {
+        if (string::npos != bslash && bslash > slash) {
+            slash = bslash;
+        }
+        spellFile = fRawFilePathDir.substr(0, slash);
+    }
+    spellFile += '/';
+    spellFile += kSpellingFileName;
+    FILE* file = fopen(spellFile.c_str(), "r");
+    if (!file) {
+        SkDebugf("missing %s\n", spellFile.c_str());
+        return;
+    }
+    fseek(file, 0L, SEEK_END);
+    int sz = (int) ftell(file);
+    rewind(file);
+    char* buffer = new char[sz];
+    memset(buffer, ' ', sz);
+    int read = (int)fread(buffer, 1, sz, file);
+    SkAssertResult(read > 0);
+    sz = read;  // FIXME: ? why are sz and read different?
+    fclose(file);
+    int i = 0;
+    int start = i;
+    string word;
+    do {
+        if (' ' < buffer[i]) {
+            ++i;
+            continue;
+        }
+        word = string(&buffer[start], i - start);
+        if (fGlobalNames.fRefMap.end() == fGlobalNames.fRefMap.find(word)) {
+            fGlobalNames.fRefMap[word] = nullptr;
+        } else {
+            SkDebugf("%s ", word.c_str());  // debugging: word missing from spelling list
+        }
+        do {
+            ++i;
+        } while (i < sz && ' ' >= buffer[i]);
+        start = i;
+    } while (i < sz);
+    delete[] buffer;
+}
+
+void BmhParser::setUpSubstitutes(const Definition* parent, NameMap* names) {
+    for (const auto& child : parent->fChildren) {
+        MarkType markType = child->fMarkType;
+        if (MarkType::kAlias == markType) {
+            continue;
+        }
+        if (MarkType::kSubstitute == markType) {
+            continue;
+        }
+        string name(child->fName);
+        if (&fGlobalNames != names) {
+            size_t lastDC = name.rfind("::");
+            if (string::npos != lastDC) {
+                name = name.substr(lastDC + 2);
+            }
+            if ("" == name) {
+                continue;
+            }
+        }
+        size_t lastUnder = name.rfind('_');
+        if (string::npos != lastUnder && ++lastUnder < name.length()) {
+            bool numbers = true;
+            for (size_t index = lastUnder; index < name.length(); ++index) {
+                numbers &= (bool) isdigit(name[index]);
+            }
+            if (numbers) {
+                continue;
+            }
+        }
+        string ref;
+        if (&fGlobalNames == names) {
+            ref = ParserCommon::HtmlFileName(child->fFileName);
+        }
+        ref += '#' + child->fFiddle;
+        if (MarkType::kClass == markType || MarkType::kStruct == markType
+                || MarkType::kMethod == markType || MarkType::kEnum == markType
+                || MarkType::kEnumClass == markType || MarkType::kConst == markType
+                || MarkType::kMember == markType || MarkType::kDefine == markType
+                || MarkType::kTypedef == markType) {
+            SkASSERT(names->fLinkMap.end() == names->fLinkMap.find(name));
+            names->fLinkMap[name] = ref;
+            SkASSERT(names->fRefMap.end() == names->fRefMap.find(name));
+            names->fRefMap[name] = child;
+        }
+        if (MarkType::kClass == markType || MarkType::kStruct == markType
+                || MarkType::kEnumClass == markType) {
+            RootDefinition* rootDef = child->asRoot();
+            NameMap* nameMap = &rootDef->fNames;
+            this->setUpSubstitutes(child, nameMap);
+            nameMap->copyToParent(names);
+        }
+        if (MarkType::kEnum == markType) {
+            this->setUpSubstitutes(child, names);
+        }
+        if (MarkType::kSubtopic == markType) {
+            if (&fGlobalNames != names && string::npos != child->fName.find('_')) {
+                string lowered = this->loweredTopic(child->fName, child);
+                SkDEBUGCODE(auto refIter = names->fRefMap.find(lowered));
+                SkDEBUGCODE(auto iter = names->fLinkMap.find(lowered));
+                SkASSERT(names->fLinkMap.end() == iter);
+                names->fLinkMap[lowered] = '#' + child->fName;
+                SkASSERT(names->fRefMap.end() == refIter);
+                names->fRefMap[lowered] = child;
+            }
+            this->setUpSubstitutes(child, names);
+        }
+    }
+}
+
+void BmhParser::setUpPartialSubstitute(string name) {
+    auto iter = fGlobalNames.fRefMap.find(name);
+    if (fGlobalNames.fRefMap.end() != iter) {
+        SkASSERT(nullptr == iter->second);
+        return;
+    }
+    fGlobalNames.fRefMap[name] = nullptr;
+}
+
+void BmhParser::setUpSubstitute(string name, Definition* def) {
+    SkASSERT(fGlobalNames.fRefMap.end() == fGlobalNames.fRefMap.find(name));
+    fGlobalNames.fRefMap[name] = def;
+    SkASSERT(fGlobalNames.fLinkMap.end() == fGlobalNames.fLinkMap.find(name));
+    string str = ParserCommon::HtmlFileName(def->fFileName) + '#' + def->fName;
+    fGlobalNames.fLinkMap[name] = str;
+    size_t stop = name.length();
+    do {
+        size_t space = name.rfind(' ', stop);
+        if (string::npos == space) {
+            break;
+        }
+        string partial = name.substr(0, space + 1);
+        stop = space - 1;
+        this->setUpPartialSubstitute(partial);
+    } while (true);
+}
+
 void BmhParser::setWrapper(Definition* def) const {
     const char drawWrapper[] = "void draw(SkCanvas* canvas) {";
     const char drawNoCanvas[] = "void draw(SkCanvas* ) {";
@@ -1489,9 +1732,8 @@
     return MarkType::kNone;
 }
 
-    // write #In to show containing #Topic
-	// write #Line with one liner from Member_Functions, Constructors, Operators if method,
-	//    from Constants if enum, otherwise from #Subtopic containing match
+    // replace #Method description, #Param, #Return with #Populate
+    // if description, params, return are free of phrase refs
 bool HackParser::hackFiles() {
     string filename(fFileName);
     size_t len = filename.length() - 1;
@@ -1511,7 +1753,10 @@
         return false;
     }
     auto mapEntry = fBmhParser.fClassMap.find(className);
-    SkASSERT(fBmhParser.fClassMap.end() != mapEntry);
+    if (fBmhParser.fClassMap.end() == mapEntry) {
+        remove(filename.c_str());
+        return true;
+    }
     const Definition* classMarkup = &mapEntry->second;
     const Definition* root = classMarkup->fParent;
     SkASSERT(root);
@@ -1520,16 +1765,12 @@
     SkASSERT(!root->fParent);
     fStart = root->fStart;
     fChar = fStart;
-    fClasses = nullptr;
-    fConstants = nullptr;
-    fConstructors = nullptr;
-    fMemberFunctions = nullptr;
-    fMembers = nullptr;
-    fOperators = nullptr;
-    fRelatedFunctions = nullptr;
-    fStructs = nullptr;
-    this->topicIter(root);
-    fprintf(fOut, "%.*s", (int) (fEnd - fChar), fChar);
+    fEnd = root->fTerminator;
+    this->replaceWithPop(root);
+    FPRINTF("%.*s", (int) (fEnd - fChar), fChar);
+    if ('\n' != fEnd[-1]) {
+        FPRINTF("\n");
+    }
     fclose(fOut);
     if (ParserCommon::WrittenFileDiffers(filename, root->fFileName)) {
         SkDebugf("wrote %s\n", filename.c_str());
@@ -1539,186 +1780,59 @@
     return true;
 }
 
-string HackParser::searchTable(const Definition* tableHolder, const Definition* match) {
-    if (!tableHolder) {
-        return "";
-    }
-    string bestMatch;
-    string result;
-    for (auto table : tableHolder->fChildren) {
-        if (MarkType::kTable == table->fMarkType) {
-            for (auto row : table->fChildren) {
-                if (MarkType::kRow == row->fMarkType) {
-                    const Definition* col0 = row->fChildren[0];
-                    size_t len = col0->fContentEnd - col0->fContentStart;
-                    string method = string(col0->fContentStart, len);
-                    if (len - 2 == method.find("()") && islower(method[0])
-                            && Definition::MethodType::kOperator != match->fMethodType) {
-                        method = method.substr(0, len - 2);
-                    }
-                    if (string::npos == match->fName.find(method)) {
-                        continue;
-                    }
-                    if (bestMatch.length() < method.length()) {
-                        bestMatch = method;
-                        const Definition * col1 = row->fChildren[1];
-                        if (col1->fContentEnd <= col1->fContentStart) {
-                            SkASSERT(string::npos != col1->fFileName.find("SkImageInfo"));
-                            result = "incomplete";
-                        } else {
-                            result = string(col1->fContentStart, col1->fContentEnd -
-                                    col1->fContentStart);
-                        }
-                    }
-                }
-            }
-        }
-    }
-    return result;
-}
-
 // returns true if topic has method
-void HackParser::topicIter(const Definition* topic) {
-    if (string::npos != topic->fName.find(SubtopicKeys::kClasses)) {
-        SkASSERT(!fClasses);
-        fClasses = topic;
-    }
-    if (string::npos != topic->fName.find(SubtopicKeys::kStructs)) {
-        SkASSERT(!fStructs);
-        fStructs = topic;
-    }
-    if (string::npos != topic->fName.find(SubtopicKeys::kConstants)) {
-        SkASSERT(!fConstants);
-        fConstants = topic;
-    }
-    if (string::npos != topic->fName.find(SubtopicKeys::kConstructors)) {
-        SkASSERT(!fConstructors);
-        fConstructors = topic;
-    }
-    if (string::npos != topic->fName.find(SubtopicKeys::kMemberFunctions)) {
-        SkASSERT(!fMemberFunctions);
-        fMemberFunctions = topic;
-    }
-    if (string::npos != topic->fName.find(SubtopicKeys::kMembers)) {
-        SkASSERT(!fMembers);
-        fMembers = topic;
-    }
-    if (string::npos != topic->fName.find(SubtopicKeys::kOperators)) {
-        SkASSERT(!fOperators);
-        fOperators = topic;
-    }
-    if (string::npos != topic->fName.find(SubtopicKeys::kRelatedFunctions)) {
-        SkASSERT(!fRelatedFunctions);
-        fRelatedFunctions = topic;
-    }
-    for (auto child : topic->fChildren) {
-        string oneLiner;
-        bool hasIn = false;
-        bool hasLine = false;
-        for (auto part : child->fChildren) {
-            hasIn |= MarkType::kIn == part->fMarkType;
-            hasLine |= MarkType::kLine == part->fMarkType;
+void HackParser::replaceWithPop(const Definition* root) {
+    for (auto child : root->fChildren) {
+        if (MarkType::kClass == child->fMarkType || MarkType::kStruct == child->fMarkType
+                || MarkType::kSubtopic == child->fMarkType) {
+            this->replaceWithPop(child);
         }
-        switch (child->fMarkType) {
-            case MarkType::kMethod: {
-                hasIn |= MarkType::kTopic != topic->fMarkType &&
-                        MarkType::kSubtopic != topic->fMarkType;  // don't write #In if parent is class
-                hasLine |= child->fClone;
-                if (!hasLine) {
-                    // find member_functions, add entry 2nd column text to #Line
-                    for (auto tableHolder : { fMemberFunctions, fConstructors, fOperators }) {
-                        if (!tableHolder) {
-                            continue;
-                        }
-                        if (Definition::MethodType::kConstructor == child->fMethodType
-                                && fConstructors != tableHolder) {
-                            continue;
-                        }
-                        if (Definition::MethodType::kOperator == child->fMethodType
-                                && fOperators != tableHolder) {
-                            continue;
-                        }
-                        string temp = this->searchTable(tableHolder, child);
-                        if ("" != temp) {
-                            SkASSERT("" == oneLiner || temp == oneLiner);
-                            oneLiner = temp;
-                        }
-                    }
-                    if ("" == oneLiner) {
-    #ifdef SK_DEBUG
-                        const Definition* rootParent = topic;
-                        while (rootParent->fParent && MarkType::kClass != rootParent->fMarkType
-                                 && MarkType::kStruct != rootParent->fMarkType) {
-                            rootParent = rootParent->fParent;
-                        }
-    #endif
-                        SkASSERT(rootParent);
-                        SkASSERT(MarkType::kClass == rootParent->fMarkType
-                                || MarkType::kStruct == rootParent->fMarkType);
-                        hasLine = true;
-                    }
-                }
-
-                if (hasIn && hasLine) {
-                    continue;
-                }
-                const char* start = fChar;
-                const char* end = child->fContentStart;
-                fprintf(fOut, "%.*s", (int) (end - start), start);
-                fChar = end;
-                // write to method markup header end
-                if (!hasIn) {
-                    fprintf(fOut, "\n#In %s", topic->fName.c_str());
-                }
-                if (!hasLine) {
-                    fprintf(fOut, "\n#Line # %s ##", oneLiner.c_str());
-                }
-                } break;
-            case MarkType::kTopic:
-            case MarkType::kSubtopic:
-                this->addOneLiner(fRelatedFunctions, child, hasLine, true);
-                this->topicIter(child);
-                break;
-            case MarkType::kStruct:
-                this->addOneLiner(fStructs, child, hasLine, false);
-                this->topicIter(child);
-                break;
-            case MarkType::kClass:
-                this->addOneLiner(fClasses, child, hasLine, false);
-                this->topicIter(child);
-                break;
-            case MarkType::kEnum:
-            case MarkType::kEnumClass:
-                this->addOneLiner(fConstants, child, hasLine, true);
-                break;
-            case MarkType::kMember:
-                this->addOneLiner(fMembers, child, hasLine, false);
-                break;
-            default:
-                ;
+        if (MarkType::kMethod != child->fMarkType) {
+            continue;
         }
-    }
-}
-
-void HackParser::addOneLiner(const Definition* defTable, const Definition* child, bool hasLine,
-        bool lfAfter) {
-    if (hasLine) {
-        return;
-    }
-    string oneLiner = this->searchTable(defTable, child);
-    if ("" == oneLiner) {
-        return;
-    }
-    const char* start = fChar;
-    const char* end = child->fContentStart;
-    fprintf(fOut, "%.*s", (int) (end - start), start);
-    fChar = end;
-    if (!lfAfter) {
-        fprintf(fOut, "\n");
-    }
-    fprintf(fOut, "#Line # %s ##", oneLiner.c_str());
-    if (lfAfter) {
-        fprintf(fOut, "\n");
+        auto& grans = child->fChildren;
+        if (grans.end() != std::find_if(grans.begin(), grans.end(),
+                [](const Definition* def) {
+                    return MarkType::kPopulate == def->fMarkType
+                        || MarkType::kPhraseRef == def->fMarkType
+                        || MarkType::kFormula == def->fMarkType
+                        || MarkType::kAnchor == def->fMarkType
+                        || MarkType::kList == def->fMarkType
+                        || MarkType::kTable == def->fMarkType
+                        || MarkType::kDeprecated == def->fMarkType
+                        || MarkType::kExperimental == def->fMarkType
+                        || MarkType::kPrivate == def->fMarkType;
+                } )) {
+            continue;
+        }
+        // write #Populate in place of description, #Param(s), #Return (if present)
+        const char* keep = child->fContentStart;
+        const char* next = nullptr;
+        for (auto gran : grans) {
+            if (MarkType::kIn == gran->fMarkType || MarkType::kLine == gran->fMarkType) {
+                keep = gran->fTerminator;
+                continue;
+            }
+            if (MarkType::kExample == gran->fMarkType
+                    || MarkType::kNoExample == gran->fMarkType) {
+                next = gran->fStart;
+                break;
+            }
+            if (MarkType::kParam == gran->fMarkType
+                    || MarkType::kReturn == gran->fMarkType
+                    || MarkType::kToDo == gran->fMarkType
+                    || MarkType::kComment == gran->fMarkType) {
+                continue;
+            }
+            SkDebugf("");  // convenient place to set a breakpoint
+        }
+        SkASSERT(next);
+        FPRINTF("%.*s", (int) (keep - fChar), fChar);
+        if ('\n' != keep[-1]) {
+            FPRINTF("\n");
+        }
+        FPRINTF("#Populate\n\n");
+        fChar = next;
     }
 }
 
@@ -2674,13 +2788,20 @@
         }
     }
     if (FLAGS_hack) {
-        if (FLAGS_bmh.isEmpty()) {
-            SkDebugf("-k or --hack requires -b\n");
+        if (FLAGS_bmh.isEmpty() && FLAGS_status.isEmpty()) {
+            SkDebugf("-H or --hack requires -a or -b\n");
             SkCommandLineFlags::PrintUsage();
             return 1;
         }
         HackParser hacker(bmhParser);
-        if (!hacker.parseFile(FLAGS_bmh[0], ".bmh", ParserCommon::OneFile::kNo)) {
+        hacker.fDebugOut = FLAGS_stdout;
+        if (!FLAGS_status.isEmpty() && !hacker.parseStatus(FLAGS_status[0], ".bmh",
+                StatusFilter::kInProgress)) {
+            SkDebugf("hack failed\n");
+            return -1;
+        }
+        if (!FLAGS_bmh.isEmpty() && !hacker.parseFile(FLAGS_bmh[0], ".bmh",
+                ParserCommon::OneFile::kNo)) {
             SkDebugf("hack failed\n");
             return -1;
         }
@@ -2827,3 +2948,18 @@
     }
     return 0;
 }
+
+void NameMap::copyToParent(NameMap* parent) const {
+    size_t colons = fName.rfind("::");
+    string topName = string::npos == colons ? fName : fName.substr(colons + 2);
+    for (auto& entry : fRefMap) {
+        string scoped = topName + "::" + entry.first;
+        SkASSERT(parent->fRefMap.end() == parent->fRefMap.find(scoped));
+        parent->fRefMap[scoped] = entry.second;
+        auto scopedLinkIter = fLinkMap.find(entry.first);
+        if (fLinkMap.end() != scopedLinkIter) {
+            SkASSERT(parent->fLinkMap.end() == parent->fLinkMap.find(scoped));
+            parent->fLinkMap[scoped] = scopedLinkIter->second;
+        }
+    }
+}
diff --git a/tools/bookmaker/bookmaker.h b/tools/bookmaker/bookmaker.h
index 950bad7..5a72315 100644
--- a/tools/bookmaker/bookmaker.h
+++ b/tools/bookmaker/bookmaker.h
@@ -21,6 +21,13 @@
 #include <unordered_map>
 #include <vector>
 
+#define FPRINTF(...)                \
+    if (fDebugOut) {                \
+        SkDebugf(__VA_ARGS__);      \
+    }                               \
+    fprintf(fOut, __VA_ARGS__)
+
+
 // std::to_string isn't implemented on android
 #include <sstream>
 
@@ -290,6 +297,19 @@
         return nullptr;
     }
 
+    bool back(const char* pattern) {
+        size_t len = strlen(pattern);
+        const char* start = fChar - len;
+        if (start <= fStart) {
+            return false;
+        }
+        if (strncmp(start, pattern, len)) {
+            return false;
+        }
+        fChar = start;
+        return true;
+    }
+
     char backup(const char* pattern) const {
         size_t len = strlen(pattern);
         const char* start = fChar - len;
@@ -302,6 +322,12 @@
         return start[-1];
     }
 
+    void backupWord() {
+        while (fChar > fStart && isalpha(fChar[-1])) {
+            --fChar;
+        }
+    }
+
     bool contains(const char* match, const char* lineEnd, const char** loc) const {
         const char* result = this->strnstr(match, lineEnd);
         if (loc) {
@@ -310,6 +336,25 @@
         return result;
     }
 
+    bool containsWord(const char* match, const char* lineEnd, const char** loc) {
+        size_t len = strlen(match);
+        do {
+            const char* result = this->strnstr(match, lineEnd);
+            if (!result) {
+                return false;
+            }
+            if ((result > fStart && isalnum(result[-1])) || (result + len < fEnd
+                    && isalnum(result[len]))) {
+                fChar = result + len;
+                continue;
+            }
+            if (loc) {
+                *loc = result;
+            }
+            return true;
+        } while (true);
+    }
+
     // either /n/n or /n# will stop parsing a typedef
     const char* doubleLF() const {
         const char* ptr = fChar - 1;
@@ -484,14 +529,22 @@
     // differs from skipToNonAlphaNum in that a.b isn't considered a full name,
     // since a.b can't be found as a named definition
     void skipFullName() {
-        while (fChar < fEnd && (isalnum(fChar[0])
-                || '_' == fChar[0]  /* || '-' == fChar[0] */
-                || (':' == fChar[0] && fChar +1 < fEnd && ':' == fChar[1]))) {
-            if (':' == fChar[0] && fChar +1 < fEnd && ':' == fChar[1]) {
+        do {
+            char last = '\0';
+            while (fChar < fEnd && (isalnum(fChar[0])
+                    || '_' == fChar[0]  /* || '-' == fChar[0] */
+                    || (':' == fChar[0] && fChar + 1 < fEnd && ':' == fChar[1]))) {
+                if (':' == fChar[0] && fChar + 1 < fEnd && ':' == fChar[1]) {
+                    fChar++;
+                }
+                last = fChar[0];
                 fChar++;
             }
-            fChar++;
-        }
+            if (fChar + 1 >= fEnd || '/' != fChar[0] || !isalpha(last) || !isalpha(fChar[1])) {
+                break;  // stop unless pattern is xxx/xxx as in I/O
+            }
+            fChar++; // skip slash
+        } while (true);
     }
 
     int skipToLineBalance(char open, char close) {
@@ -1091,6 +1144,16 @@
     static const char* kGeneratedSubtopics[];
 };
 
+struct NameMap {
+    void copyToParent(NameMap* parent) const;
+
+    string fName;
+    NameMap* fParent = nullptr;
+    unordered_map<string, string> fLinkMap;   // from SkRect to #Rect
+    // ref map includes "xxx", "xxx ", "xxx yyy", "xxx zzz", etc.
+    unordered_map<string, Definition*> fRefMap;    // e.g., from #Substitute entry to #Topic entry
+};
+
 class RootDefinition : public Definition {
 public:
     enum class AllowParens {
@@ -1112,10 +1175,16 @@
 
     RootDefinition(MarkType markType, const char* start, int line, Definition* parent, char mc)
             : Definition(markType, start, line, parent, mc) {
+        if (MarkType::kSubtopic != markType && MarkType::kTopic != markType) {
+            if (parent) {
+                fNames.fName = parent->fName;
+                fNames.fParent = &parent->asRoot()->fNames;
+            }
+        }
     }
 
     RootDefinition(MarkType markType, const char* start, const char* end, int line,
-            Definition* parent, char mc) : Definition(markType, start, end,  line, parent, mc) {
+            Definition* parent, char mc) : Definition(markType, start, end, line, parent, mc) {
     }
 
     ~RootDefinition() override {
@@ -1141,6 +1210,7 @@
     unordered_map<string, RootDefinition*> fBranches;
     unordered_map<string, Definition> fLeaves;
     unordered_map<string, SubtopicContents> fPopulators;
+    NameMap fNames;
 private:
     RootDefinition* fRootParent = nullptr;
 };
@@ -1219,9 +1289,10 @@
     }
 
     void checkLineLength(size_t len, const char* str);
+    static string ConvertRef(const string str, bool first);
     static void CopyToFile(string oldFile, string newFile);
-
     static char* FindDateTime(char* buffer, int size);
+    static string HtmlFileName(string bmhFileName);
 
     void indentIn(IndentKind kind) {
         fIndent += 4;
@@ -1239,10 +1310,7 @@
         SkASSERT(column >= fColumn);
         SkASSERT(!fReturnOnWrite);
         SkASSERT(column < 80);
-        if (fDebugOut) {
-            SkDebugf("%*s", column - fColumn, "");
-        }
-        fprintf(fOut, "%*s", column - fColumn, "");
+        FPRINTF("%*s", column - fColumn, "");
         fColumn = column;
         fSpaces += column - fColumn;
     }
@@ -1384,6 +1452,7 @@
     vector<IndentState> fIndentStack;
     Definition* fParent;
     FILE* fOut;
+    string fRawFilePathDir;
     int fLinefeeds;    // number of linefeeds last written, zeroed on non-space
     int fMaxLF;        // number of linefeeds allowed
     int fPendingLF;    // number of linefeeds to write (can be suppressed)
@@ -1455,6 +1524,7 @@
         kLiteral, // output untouched
 		kClone,   // resolved, output, with references to clones as well
         kSimple,  // resolve simple words (used to resolve method declarations)
+        kInclude, // like simple, plus reverse resolve SkXXX to XXX
     };
 
     enum class ExampleOptions {
@@ -1482,7 +1552,7 @@
 
     enum class TrimExtract {
         kNo,
-        kYes
+        kYes,
     };
 
     BmhParser(bool skip) : ParserCommon()
@@ -1524,6 +1594,7 @@
     MarkType getMarkType(MarkLookup lookup) const;
     bool hasEndToken() const;
     static bool IsExemplary(const Definition* );
+    string loweredTopic(string name, Definition* def);
     string memberName();
     string methodName();
     const Definition* parentSpace() const;
@@ -1558,6 +1629,10 @@
         fCheckMethods = false;
     }
 
+    void setUpGlobalSubstitutes();
+    void setUpPartialSubstitute(string name);
+    void setUpSubstitute(string name, Definition* def);
+    void setUpSubstitutes(const Definition* parent, NameMap* );
     void setWrapper(Definition* def) const;
     bool skipNoName();
     bool skipToDefinitionEnd(MarkType markType);
@@ -1602,6 +1677,7 @@
     unordered_map<string, Definition*> fTopicMap;
     unordered_map<string, Definition*> fAliasMap;
     unordered_map<string, Definition*> fPhraseMap;
+    NameMap fGlobalNames;
     RootDefinition* fRoot;
     Definition* fWorkingColumn;
     Definition* fRow;
@@ -2363,9 +2439,6 @@
         this->reset();
     }
 
-    void addOneLiner(const Definition* defTable, const Definition* child, bool hasLine,
-            bool lfAfter);
-
     bool parseFromFile(const char* path) override {
         if (!INHERITED::parseSetup(path)) {
             return false;
@@ -2377,20 +2450,10 @@
         INHERITED::resetCommon();
     }
 
-    string searchTable(const Definition* tableHolder, const Definition* match);
-
-    void topicIter(const Definition* );
+    void replaceWithPop(const Definition* );
 
 private:
     const BmhParser& fBmhParser;
-    const Definition* fClasses;
-    const Definition* fConstants;
-    const Definition* fConstructors;
-    const Definition* fMemberFunctions;
-    const Definition* fMembers;
-    const Definition* fOperators;
-    const Definition* fRelatedFunctions;
-    const Definition* fStructs;
     bool hackFiles();
 
     typedef ParserCommon INHERITED;
@@ -2410,6 +2473,7 @@
         , fIncludeParser(inc) {
         this->reset();
         this->addPopulators();
+        fBmhParser.setUpGlobalSubstitutes();
     }
 
     bool buildReferences(const char* docDir, const char* mdOutDirOrFile);
@@ -2430,19 +2494,21 @@
 
     void addCodeBlock(const Definition* def, string& str) const;
     void addPopulators();
+    string addIncludeReferences(const char* refStart, const char* refEnd);
     string addReferences(const char* start, const char* end, BmhParser::Resolvable );
     string anchorDef(string def, string name);
     string anchorLocalRef(string ref, string name);
     string anchorRef(string def, string name);
-
     bool buildRefFromFile(const char* fileName, const char* outDir);
     bool checkParamReturnBody(const Definition* def);
     Definition* checkParentsForMatch(Definition* test, string ref) const;
     void childrenOut(Definition* def, const char* contentStart);
     Definition* csParent();
+    bool findLink(string ref, string* link);
     Definition* findParamType();
     string getMemberTypeName(const Definition* def, string* memberType);
     static bool HasDetails(const Definition* def);
+    bool hasWordSpace(string ) const;
     void htmlOut(string );
     Definition* isDefined(const TextParser& , string ref, BmhParser::Resolvable );
     Definition* isDefinedByParent(RootDefinition* root, string ref);
@@ -2454,6 +2520,7 @@
     void parameterHeaderOut(TextParser& paramParser, const Definition** prior, Definition* def);
     void parameterTrailerOut();
     bool parseFromFile(const char* path) override { return true; }
+    bool phraseContinues(string phrase, string* priorWord, string* priorLink) const;
     void populateOne(Definition* def,
             unordered_map<string, RootDefinition::SubtopicContents>& populator);
     void populateTables(const Definition* def, RootDefinition* );
@@ -2522,7 +2589,7 @@
     vector<const Definition*> fClassStack;
     unordered_map<string, vector<AnchorDef> > fAllAnchorDefs;
     unordered_map<string, vector<string> > fAllAnchorRefs;
-
+    NameMap* fNames;
     BmhParser& fBmhParser;
     IncludeParser& fIncludeParser;
     const Definition* fEnumClass;
@@ -2604,6 +2671,7 @@
             }
         }
         if (BmhParser::Resolvable::kSimple != resolvable
+                && BmhParser::Resolvable::kInclude != resolvable
                 && (this->startsWith(name.c_str()) || this->startsWith("operator"))) {
             const char* ptr = this->anyOf("\n (");
             if (ptr && '(' ==  *ptr && strncmp(ptr, "(...", 4)) {
diff --git a/tools/bookmaker/definition.cpp b/tools/bookmaker/definition.cpp
index 52620f8..fb96ed1 100644
--- a/tools/bookmaker/definition.cpp
+++ b/tools/bookmaker/definition.cpp
@@ -912,7 +912,7 @@
         if (!parser.skipExact("@param ")) {
             continue;
         }
-        if (parser.skipExact(ref.c_str())) {
+        if (parser.skipExact(ref.c_str()) && ' ' == parser.peek()) {
             return &iter;
         }
     }
diff --git a/tools/bookmaker/includeParser.cpp b/tools/bookmaker/includeParser.cpp
index 0b4576c..2fdd7ae 100644
--- a/tools/bookmaker/includeParser.cpp
+++ b/tools/bookmaker/includeParser.cpp
@@ -1852,8 +1852,12 @@
 }
 
 Definition* IncludeParser::findMethod(const Definition& bmhDef) {
-    auto doubleColon = bmhDef.fName.find("::");
-    SkASSERT(string::npos != doubleColon);  // more work to do to support global refs
+    auto doubleColon = bmhDef.fName.rfind("::");
+    if (string::npos == doubleColon) {
+        const auto& iGlobalMethod = fIFunctionMap.find(bmhDef.fName);
+        SkASSERT(fIFunctionMap.end() != iGlobalMethod);
+        return iGlobalMethod->second;
+    }
     string className = bmhDef.fName.substr(0, doubleColon);
     const auto& iClass = fIClassMap.find(className);
     SkASSERT(fIClassMap.end() != iClass);
@@ -1861,9 +1865,66 @@
     auto& iTokens = iClass->second.fTokens;
     const auto& iMethod = std::find_if(iTokens.begin(), iTokens.end(),
             [methodName](Definition& token) {
-            return MarkType::kMethod == token.fMarkType && methodName == token.fName; } );
-    SkASSERT(iTokens.end() != iMethod);
-    return &*iMethod;
+            return MarkType::kMethod == token.fMarkType
+                    && (methodName == token.fName
+                    || methodName == token.fName + "()"); } );
+    if (iTokens.end() != iMethod) {
+        return &*iMethod;
+    }
+    size_t subClassPos = className.rfind("::");
+    if (string::npos != subClassPos) {
+        className = className.substr(subClassPos + 2);
+    }
+    // match may be constructor; compare strings to see if this is so
+    SkASSERT(string::npos != methodName.find('('));
+    auto stripper = [](string s) -> string {
+        bool last = false;
+        string result;
+        for (char c : s) {
+            if (' ' >= c) {
+                if (!last) {
+                    last = true;
+                    result += ' ';
+                }
+                continue;
+            }
+            result += c;
+            last = false;
+        }
+        return result;
+    };
+    string strippedMethodName = stripper(methodName);
+    if (strippedMethodName == methodName) {
+        strippedMethodName = "";
+    }
+    const auto& cMethod = std::find_if(iTokens.begin(), iTokens.end(),
+            [className, methodName, stripper, strippedMethodName](Definition& token) {
+        if (MarkType::kMethod != token.fMarkType) {
+            return false;
+        }
+        TextParser parser(&token);
+        const char* match = parser.strnstr(className.c_str(), parser.fEnd);
+        if (!match) {
+            return false;
+        }
+        parser.skipTo(match);
+        parser.skipExact(className.c_str());
+        if ('(' != parser.peek()) {
+            return false;
+        }
+        parser.skipToBalancedEndBracket('(', ')');
+        string iMethodName(match, parser.fChar - match);
+        if (methodName == iMethodName) {
+            return true;
+        }
+        if ("" == strippedMethodName) {
+            return false;
+        }
+        string strippedIName = stripper(iMethodName);
+        return strippedIName == strippedMethodName;
+    } );
+    SkAssertResult(iTokens.end() != cMethod);
+    return &*cMethod;
 }
 
 Definition* IncludeParser::parentBracket(Definition* parent) const {
@@ -2570,7 +2631,6 @@
     markupDef->fTokens.emplace_back(MarkType::kMethod, start, end, tokenIter->fLineCount,
             markupDef, '\0');
     Definition* markupChild = &markupDef->fTokens.back();
-    // TODO: I wonder if there is a way to prevent looking up by operator[] (creating empty) ?
     {
         auto mapIter = fIClassMap.find(markupDef->fName);
         SkASSERT(fIClassMap.end() != mapIter);
diff --git a/tools/bookmaker/includeWriter.cpp b/tools/bookmaker/includeWriter.cpp
index 768ccf6..ef854a7 100644
--- a/tools/bookmaker/includeWriter.cpp
+++ b/tools/bookmaker/includeWriter.cpp
@@ -895,7 +895,7 @@
         int commentIndex = child.fParentIndex;
         auto iter = child.fParent->fTokens.begin();
         std::advance(iter, commentIndex);
-        SkDEBUGCODE(bool sawMethod = false);
+        SkDEBUGCODE(bool sawMethod = MarkType::kMethod == iter->fMarkType);
         while (--commentIndex >= 0) {
             std::advance(iter, -1);
             if (Bracket::kSlashStar == iter->fBracket) {
@@ -2157,21 +2157,6 @@
     return allPassed;
 }
 
-// change Xxx_Xxx to xxx xxx
-static string ConvertRef(const string str, bool first) {
-    string substitute;
-    for (char c : str) {
-        if ('_' == c) {
-            c = ' ';  // change Xxx_Xxx to xxx xxx
-        } else if (isupper(c) && !first) {
-            c = tolower(c);
-        }
-        substitute += c;
-        first = false;
-    }
-    return substitute;
-}
-
 string IncludeWriter::resolveMethod(const char* start, const char* end, bool first) {
     string methodname(start, end - start);
     if (string::npos != methodname.find("()")) {
@@ -2322,6 +2307,7 @@
                 // prefer the one mostly closely matching in text
                 if ((MarkType::kClass == child->fMarkType ||
                     MarkType::kStruct == child->fMarkType ||
+                    MarkType::kTypedef == child->fMarkType ||
                     (MarkType::kEnum == child->fMarkType && !child->fAnonymous) ||
                     MarkType::kEnumClass == child->fMarkType) && (match == child->fName ||
                     skmatch == child->fName)) {
@@ -2372,7 +2358,7 @@
                     if (parent->fParent != fRootTopic) {
                         substitute = parent->fName;
                         substitute += ' ';
-                        substitute += ConvertRef(rootDef->fName, false);
+                        substitute += ParserCommon::ConvertRef(rootDef->fName, false);
                     } else {
                         size_t underpos = undername.find('_');
                         if (string::npos != underpos) {
@@ -2390,7 +2376,7 @@
                                 return this->reportError<string>("remove underline");
                             }
                         }
-                        substitute += ConvertRef(undername, first);
+                        substitute += ParserCommon::ConvertRef(undername, first);
                     }
                 }
             }
@@ -2454,7 +2440,7 @@
     string temp = this->resolveRef(&data[start], &data[end], Word::kFirst == word, &refType);
     if (!temp.length()) {
         if (Word::kFirst != word && '_' != last) {
-            temp = ConvertRef(resolved, false);
+            temp = ParserCommon::ConvertRef(resolved, false);
         }
     }
     if (temp.length()) {
diff --git a/tools/bookmaker/mdOut.cpp b/tools/bookmaker/mdOut.cpp
index 7cca263..ced107d 100644
--- a/tools/bookmaker/mdOut.cpp
+++ b/tools/bookmaker/mdOut.cpp
@@ -10,12 +10,6 @@
 #include "SkOSFile.h"
 #include "SkOSPath.h"
 
-#define FPRINTF(...)                \
-    if (fDebugOut) {                \
-        SkDebugf(__VA_ARGS__);      \
-    }                               \
-    fprintf(fOut, __VA_ARGS__)
-
 const char* SubtopicKeys::kGeneratedSubtopics[] = {
     kConstants, kDefines, kTypedefs, kMembers, kClasses, kStructs, kConstructors,
     kOperators, kMemberFunctions, kRelatedFunctions
@@ -55,17 +49,9 @@
 const char* kTopicsTableHeader    = "  <tr>" kTH_Left   "Topic</th>"                            "\n"
                                              kTH_Left   "Description</th>" "</tr>";
 
-static string html_file_name(string bmhFileName) {
-    SkASSERT("docs" == bmhFileName.substr(0, 4));
-    SkASSERT('\\' == bmhFileName[4] || '/' == bmhFileName[4]);
-    SkASSERT(".bmh" == bmhFileName.substr(bmhFileName.length() - 4));
-    string result = bmhFileName.substr(5, bmhFileName.length() - 4 - 5);
-    return result;
-}
-
 string MdOut::anchorDef(string str, string name) {
     if (fValidate) {
-        string htmlName = html_file_name(fFileName);
+        string htmlName = ParserCommon::HtmlFileName(fFileName);
         vector<AnchorDef>& allDefs = fAllAnchorDefs[htmlName];
         if (!std::any_of(allDefs.begin(), allDefs.end(),
                 [str](AnchorDef compare) { return compare.fDef == str; } )) {
@@ -91,7 +77,7 @@
         size_t hashIndex = ref.find('#');
         if (string::npos != hashIndex && "https://" != ref.substr(0, 8)) {
             if (0 == hashIndex) {
-                htmlName = html_file_name(fFileName);
+                htmlName = ParserCommon::HtmlFileName(fFileName);
             } else {
                 htmlName = ref.substr(0, hashIndex);
             }
@@ -303,9 +289,228 @@
     int fBraceCount;
 };
 
+bool MdOut::hasWordSpace(string wordSpace) const {
+    for (NameMap* names = fNames; names; names = names->fParent) {
+        if (names->fRefMap.end() != names->fRefMap.find(wordSpace)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+bool MdOut::phraseContinues(string phrase, string* priorWord, string* priorLink) const {
+    for (NameMap* names = fNames; names; names = names->fParent) {
+        if (names->fRefMap.end() != names->fRefMap.find(phrase + ' ')) {
+            *priorWord = phrase;
+            return true;
+        }
+        if (names->fRefMap.end() != names->fRefMap.find(phrase)) {
+            *priorWord = phrase;
+            *priorLink = names->fLinkMap.end() == names->fLinkMap.find(phrase) ? "" :
+                    names->fLinkMap[phrase];
+            return true;
+        }
+    }
+    return false;
+}
+
+// adds spaces to see if found word is part of registered phrase
+// adds parens to see if found word is all-lowercase function (parens must be in source as well)
+string MdOut::addIncludeReferences(const char* refStart, const char* refEnd) {
+    TextParser matrixParser(fMethod->fFileName, refStart, refEnd, fMethod->fLineCount);
+    const char* bracket = matrixParser.anyOf("|=\n");
+    bool inMatrix = bracket && ('|' == bracket[0] || '=' == bracket[0]);
+    auto& globals = fBmhParser.fGlobalNames;
+    string result;
+    const char* start = refStart;
+    string priorWord;
+    string priorLink;
+    string priorSeparator;
+    do {
+        const char* separatorStart = start;
+        bool whiteSpace = start < refEnd && ' ' >= start[0];
+        while (start < refEnd && !isalpha(start[0])) {
+            whiteSpace &= ' ' >= start[0];
+            ++start;
+        }
+        bool priorSpace = false;
+        string separator(separatorStart, start - separatorStart);
+        if ("" != priorWord && whiteSpace) {
+            string wordSpace = priorWord + ' ';
+            if (this->hasWordSpace(wordSpace)) {
+                priorWord = wordSpace;
+                priorSpace = true;
+            }
+        }
+        if ("()" == separator.substr(0, 2)) {
+            string funcRef = priorWord + "()";
+            if (this->findLink(funcRef, &priorLink)) {
+                priorWord = funcRef;
+                separator = separator.substr(2);
+            }
+        }
+        result += priorSeparator;
+        priorSeparator = separator;
+        const char* end = start;
+        do {
+            while (end < refEnd && (isalnum(end[0]) || '-' == end[0] || '_' == end[0])) {
+                ++end;
+            }
+            if (end + 1 >= refEnd || '/' != end[0] || start == end || !isalpha(end[-1])
+                    || !isalpha(end[1])) {
+                break;  // stop unless pattern is xxx/xxx as in I/O
+            }
+            ++end; // skip slash
+        } while (true);
+        while (start != end && '-' == end[-1]) {
+            --end;
+        }
+        if (start != end && end + 2 < refEnd && "()" == string(end, 2)) {
+            string check = string(start, end - start);
+            if (std::all_of(check.begin(), check.end(), [](char c) { return islower(c); })) {
+                end += 2;
+            }
+        }
+        if (start == end) {
+            break;
+        }
+        string word(start, end - start);
+        if (priorSpace) {
+            string phrase = priorWord + word;
+            if (this->phraseContinues(phrase, &priorWord, &priorLink)) {
+                start = end;
+                continue;
+            }
+            priorWord = priorWord.substr(0, priorWord.length() - 1);
+        }
+        string link;
+        bool found;
+        // TODO: operators have complicated parsing possibilities; handle the easiest for now
+        if ("operator" == priorWord && '(' == separator.back()) {
+            TextParser parser(fMethod->fFileName, separatorStart, refEnd, fMethod->fLineCount);
+            parser.skipToEndBracket('(');
+            const char* parenStart = parser.fChar;
+            parser.skipToBalancedEndBracket('(', ')');
+            word = priorWord + separator + string(parenStart + 1, parser.fChar - parenStart - 1);
+            end = parser.fChar;
+            priorWord = "";
+            priorLink = "";
+            priorSeparator = "";
+        }
+        {
+            auto paramIter = fNames->fRefMap.find(word);
+            if ((found = fNames->fRefMap.end() != paramIter) && paramIter->second) {
+                SkAssertResult(fNames->fLinkMap.end() != fNames->fLinkMap.find(word));
+                link = fNames->fLinkMap[word];
+            }
+        }
+        if (!found && ("." == separator || "->" == separator || "()." == separator
+                    || "()->" == separator)) {
+            if (('f' == word[0] && isupper(word[1])) || "()" == word.substr(word.length() - 2)
+                    || (end + 2 <= refEnd && "()" == string(end, 2))) {
+                if (fNames->fRefMap.end() != fNames->fRefMap.find(priorWord)) {
+            // find prior word's type in fMethod
+                    TextParser parser(fMethod);
+                    SkAssertResult(parser.containsWord(priorWord.c_str(), parser.fEnd,
+                            &parser.fChar));
+            // look up class or struct; trival lookup only class/struct [& * const]
+                    while (parser.back(" ") || parser.back("&") || parser.back("*")
+                            || parser.back("const"))
+                        ;
+                    const char* structEnd = parser.fChar;
+                    parser.backupWord();
+                    if (structEnd != parser.fChar) {
+                        string structName(parser.fChar, structEnd - parser.fChar);
+                        if ("SkVector" == structName) {
+                            // TODO: populate global refmap with typedefs as well as structs
+                            structName = "SkPoint";
+                        }
+                        structName += "::" + word;
+            // look for word as member of class or struct
+                        auto defIter = globals.fRefMap.find(structName);
+                        if (globals.fRefMap.end() == defIter) {
+                            structName += "()";
+                            defIter = globals.fRefMap.find(structName);
+                        }
+                        if (globals.fRefMap.end() != defIter) {
+                            found = true;
+                            SkASSERT(globals.fLinkMap.end() != globals.fLinkMap.find(structName));
+                            link = globals.fLinkMap[structName];
+                        } else {
+                            SkDebugf("probably missing struct or class member in bmh: ");
+                            SkDebugf("%s\n", structName.c_str());
+                        }
+                    }
+                }
+                if (!found) {
+                    auto& parentRefMap = fNames->fParent->fRefMap;
+                    auto priorIter = parentRefMap.find(priorWord);
+                    if (parentRefMap.end() == priorIter) {
+                        priorIter = parentRefMap.find(priorWord + "()");
+                    }
+                    if (parentRefMap.end() != priorIter) {
+                        Definition* priorDef = priorIter->second;
+                        TextParser parser(priorDef->fFileName, priorDef->fStart,
+                                priorDef->fContentStart, priorDef->fLineCount);
+                        parser.skipExact("#Method ");
+                        parser.skipSpace();
+                        parser.skipExact("const ");  // optional
+                        parser.skipSpace();
+                        const char* start = parser.fChar;
+                        parser.skipToNonAlphaNum();
+                        string structName(start, parser.fChar - start);
+                        structName += "::" + word;
+                        auto defIter = globals.fRefMap.find(structName);
+                        if (globals.fRefMap.end() != defIter) {
+                            found = true;
+                            SkASSERT(globals.fLinkMap.end() != globals.fLinkMap.find(structName));
+                            link = globals.fLinkMap[structName];
+                        }
+                    }
+                }
+            } else {
+                SkDebugf("probably missing () after function:");
+                const char* debugStart = end - 20 < refStart ? refStart : end - 20;
+                const char* debugEnd = end + 10 > refEnd ? refEnd : end + 10;
+                SkDebugf("%.*s\n", debugEnd - debugStart, debugStart);
+                SkDebugf("");
+            }
+        }
+        if (!found && "::" == separator) {
+            string fullRef = priorWord + "::" + word;
+            found = this->findLink(fullRef, &link);
+        }
+        if (!found) {
+            found = this->findLink(word, &link);
+        }
+        if (!found) {
+            found = globals.fRefMap.end() != globals.fRefMap.find(word + ' ');
+        }
+        if (!found) {
+            found = globals.fRefMap.end() != globals.fRefMap.find(word + "()");
+        }
+        if (!found) {
+            if (!inMatrix) {
+                SkDebugf("word %s not found\n", word.c_str());
+                fBmhParser.fGlobalNames.fRefMap[word] = nullptr;
+            }
+        }
+        result += "" == priorLink ? priorWord : this->anchorRef(priorLink, priorWord);
+        priorWord = word;
+        priorLink = link;
+        start = end;
+    } while (true);
+    result += "" == priorLink ? priorWord : this->anchorRef(priorLink, priorWord);
+    result += priorSeparator;
+    return result;
+}
+
 // FIXME: preserve inter-line spaces and don't add new ones
 string MdOut::addReferences(const char* refStart, const char* refEnd,
         BmhParser::Resolvable resolvable) {
+    if (BmhParser::Resolvable::kInclude == resolvable) {  // test include resolving
+        return this->addIncludeReferences(refStart, refEnd);
+    }
     string result;
     MethodParser t(fRoot ? fRoot->fName : string(), fFileName, refStart, refEnd, fLineCount);
     bool lineStart = true;
@@ -448,6 +653,7 @@
             }
             SkASSERT(def->fFiddle.length());
             if (BmhParser::Resolvable::kSimple != resolvable
+                    && BmhParser::Resolvable::kInclude != resolvable
                     && !t.eof() && '(' == t.peek() && t.strnchr(')', t.fEnd)) {
                 TextParserSave tSave(&t);
                 if (!t.skipToBalancedEndBracket('(', ')')) {
@@ -511,6 +717,12 @@
             }
 			result += linkRef(leadingSpaces, def, ref, resolvable);
             if (!t.eof() && '(' == t.peek()) {
+                if (BmhParser::Resolvable::kInclude == resolvable
+                       && std::any_of(ref.begin(), ref.end(), [](char c){ return !islower(c); } )) {
+                    t.next();  // skip open paren
+                    SkAssertResult(')' == t.next());  // skip close paren
+                    continue;
+                }
                 result += t.next();  // skip open paren
             }
             continue;
@@ -577,6 +789,7 @@
                         }
                     }
                     if (BmhParser::Resolvable::kSimple != resolvable
+                            && BmhParser::Resolvable::kInclude != resolvable
                             && BmhParser::Resolvable::kOut != resolvable
                             && !formula_or_code(resolvable)) {
                         t.reportError("missed camelCase");
@@ -970,6 +1183,33 @@
     return csParent;
 }
 
+bool MdOut::findLink(string word, string* linkPtr) {
+    NameMap* names = fNames;
+    while ((names = names->fParent)) {
+        auto localIter = names->fRefMap.find(word);
+        if (names->fRefMap.end() != localIter) {
+            if (localIter->second) {
+                SkAssertResult(names->fLinkMap.end() != names->fLinkMap.find(word));
+                *linkPtr = names->fLinkMap[word];
+            }
+            return true;
+        }
+        if (!names->fParent && isupper(word[0])) {
+            SkASSERT(names == &fBmhParser.fGlobalNames);
+            string lower = (char) tolower(word[0]) + word.substr(1);
+            auto globalIter = names->fRefMap.find(lower);
+            if (names->fRefMap.end() != globalIter) {
+                if (globalIter->second) {
+                    SkAssertResult(names->fLinkMap.end() != names->fLinkMap.find(lower));
+                    *linkPtr = names->fLinkMap[lower];
+                }
+                return true;
+            }
+        }
+    }
+    return false;
+}
+
 Definition* MdOut::findParamType() {
     SkASSERT(fMethod);
     TextParser parser(fMethod->fFileName, fMethod->fStart, fMethod->fContentStart,
@@ -1244,6 +1484,15 @@
 // def should not include SkXXX_
 string MdOut::linkRef(string leadingSpaces, Definition* def,
         string ref, BmhParser::Resolvable resolvable) {
+    bool trimRef = BmhParser::Resolvable::kInclude == resolvable && "Sk" == ref.substr(0, 2);
+    if (trimRef) {
+#ifdef SK_DEBUG
+    for (auto c : ref) {
+            SkASSERT(isalpha(c) || isdigit(c));
+        }
+#endif
+        ref = ref.substr(2);
+    }
     string buildup;
     string refName;
     const string* str = &def->fFiddle;
@@ -1253,7 +1502,17 @@
         const Definition* parent = def->csParent();
         SkASSERT(parent);
         classPart = parent->fName;
-        refName = classPart + '_' + def->fParent->fName + '_' + ref;
+        auto bmhMap = fBmhParser.fClassMap.find(classPart);
+        auto defName = def->fParent->fName;
+        SkASSERT(fBmhParser.fClassMap.end() != bmhMap);
+        string fullName = classPart + "::" + defName;
+        auto bmhDef = bmhMap->second.fLeaves.find(fullName);
+        if (bmhMap->second.fLeaves.end() == bmhDef) {
+            bmhDef = bmhMap->second.fLeaves.find(fullName + "()");
+        }
+        SkASSERT(bmhMap->second.fLeaves.end() != bmhDef);
+        refName = bmhDef->second.fFiddle;
+        refName += '_' + ref;
     }
     SkASSERT(classPart.length() > 0);
     bool globalEnumMember = false;
@@ -1285,6 +1544,15 @@
         }
         if (!fromInclude) {
             refName = def->fFiddle;
+            if (trimRef) {
+                SkASSERT("Sk" == refName.substr(0, 2));
+#ifdef SK_DEBUG
+                for (auto c : refName) {
+                    SkASSERT(isalpha(c) || isdigit(c));
+                }
+#endif
+                refName = refName.substr(2);
+            }
         }
     }
     bool classMatch = fRoot->fFileName == def->fFileName || fromInclude;
@@ -1893,9 +2161,41 @@
                 SkASSERT(MarkType::kMethod == parent->fMarkType);
                 // retrieve parameters, return, description from include
                 Definition* iMethod = fIncludeParser.findMethod(*parent);
-                SkDebugf("");
                 bool wroteParam = false;
                 fMethod = iMethod;
+                NameMap paramMap;
+                Definition* pParent = def->csParent();
+                string parentName;
+                NameMap* parentMap;
+                if (pParent) {
+                    parentName = pParent->fName + "::";
+                    parentMap = &pParent->asRoot()->fNames;
+                } else {
+                    parentMap = &fBmhParser.fGlobalNames;
+                }
+                paramMap.fName = parentName + iMethod->fName;
+                paramMap.fParent = parentMap;
+                fNames = &paramMap;
+                for (auto& param : iMethod->fTokens) {
+                    if (MarkType::kComment != param.fMarkType) {
+                        continue;
+                    }
+                    TextParser paramParser(&param);
+                    if (!paramParser.skipExact("@param ")) { // write parameters, if any
+                        continue;
+                    }
+                    paramParser.skipSpace();
+                    const char* start = paramParser.fChar;
+                    paramParser.skipToSpace();
+                    string paramName(start, paramParser.fChar - start);
+                #ifdef SK_DEBUG
+                    for (char c : paramName) {
+                        SkASSERT(isalnum(c) || '_' == c);
+                    }
+                #endif
+                    paramMap.fRefMap[paramName] = &param;
+                    paramMap.fLinkMap[paramName] = '#' + def->fFiddle + '_' + paramName;
+                }
                 for (auto& entry : iMethod->fTokens) {
                     if (MarkType::kComment != entry.fMarkType) {
                         continue;
@@ -1903,7 +2203,8 @@
                     TextParser parser(&entry);
                     if (parser.skipExact("@param ")) { // write parameters, if any
                         this->parameterHeaderOut(parser, prior, def);
-                        this->resolveOut(parser.fChar, parser.fEnd, BmhParser::Resolvable::kYes);
+                        this->resolveOut(parser.fChar, parser.fEnd,
+                                BmhParser::Resolvable::kInclude);
                         this->parameterTrailerOut();
                         wroteParam = true;
                         continue;
@@ -1917,18 +2218,29 @@
                     }
                     if (parser.skipExact("@return ")) { // write return, if any
                         this->returnHeaderOut(prior, def);
-                        this->resolveOut(parser.fChar, parser.fEnd, BmhParser::Resolvable::kYes);
+                        this->resolveOut(parser.fChar, parser.fEnd,
+                                BmhParser::Resolvable::kInclude);
                         this->lf(2);
                         continue;
                     }
                     if (1 == entry.length() && '/' == entry.fContentStart[0]) {
                         continue;
                     }
+                    if ("/!< " == string(entry.fContentStart, entry.length()).substr(0, 4)) {
+                        continue;
+                    }
+                    const char* backwards = entry.fContentStart;
+                    while (' ' == *--backwards)
+                        ;
+                    if ('\n' == backwards[0] && '\n' == backwards[-1]) {
+                        this->lf(2);
+                    }
                     this->resolveOut(entry.fContentStart, entry.fContentEnd,
-                            BmhParser::Resolvable::kYes);  // write description
+                            BmhParser::Resolvable::kInclude);  // write description
                     this->lf(1);
                 }
                 fMethod = nullptr;
+                fNames = fNames->fParent;
             }
             } break;
         case MarkType::kPrivate:
diff --git a/tools/bookmaker/parserCommon.cpp b/tools/bookmaker/parserCommon.cpp
index 5500a1b..c3bab1a 100644
--- a/tools/bookmaker/parserCommon.cpp
+++ b/tools/bookmaker/parserCommon.cpp
@@ -9,11 +9,6 @@
 #include "SkOSFile.h"
 #include "SkOSPath.h"
 
-static void debug_out(int len, const char* data) {
-    // convenient place to intercept arbitrary output
-    SkDebugf("%.*s", len, data);
-}
-
 void ParserCommon::checkLineLength(size_t len, const char* str) {
     if (!fWritingIncludes) {
         return;
@@ -36,6 +31,21 @@
     }
 }
 
+// change Xxx_Xxx to xxx xxx
+string ParserCommon::ConvertRef(const string str, bool first) {
+    string substitute;
+    for (char c : str) {
+        if ('_' == c) {
+            c = ' ';
+        } else if (isupper(c) && !first) {
+            c = tolower(c);
+        }
+        substitute += c;
+        first = false;
+    }
+    return substitute;
+}
+
 void ParserCommon::CopyToFile(string oldFile, string newFile) {
     int bufferSize;
     char* buffer = ParserCommon::ReadToBuffer(newFile, &bufferSize);
@@ -49,7 +59,16 @@
     remove(newFile.c_str());
 }
 
+string ParserCommon::HtmlFileName(string bmhFileName) {
+    SkASSERT("docs" == bmhFileName.substr(0, 4));
+    SkASSERT('\\' == bmhFileName[4] || '/' == bmhFileName[4]);
+    SkASSERT(".bmh" == bmhFileName.substr(bmhFileName.length() - 4));
+    string result = bmhFileName.substr(5, bmhFileName.length() - 4 - 5);
+    return result;
+}
+
 bool ParserCommon::parseFile(const char* fileOrPath, const char* suffix, OneFile oneFile) {
+    fRawFilePathDir = string(fileOrPath);
     if (!sk_isdir(fileOrPath)) {
         if (!this->parseFromFile(fileOrPath)) {
             SkDebugf("failed to parse %s\n", fileOrPath);
@@ -80,6 +99,7 @@
 }
 
 bool ParserCommon::parseStatus(const char* statusFile, const char* suffix, StatusFilter filter) {
+    fRawFilePathDir = string(statusFile);
     StatusIter iter(statusFile, suffix, filter);
     if (iter.empty()) {
         return false;
@@ -198,10 +218,7 @@
         int len = lineEnd ? (int) (lineEnd - data) : size;
         this->writePending();
         this->indentToColumn(indent);
-        if (fDebugOut) {
-            debug_out(len, data);
-        }
-        fprintf(fOut, "%.*s", len, data);
+        FPRINTF("%.*s", len, data);
         checkLineLength(len, data);
         size -= len;
         data += len;
@@ -241,10 +258,7 @@
         fPendingSpace = 0;
     }
     this->writePending();
-    if (fDebugOut) {
-        debug_out(size, data);
-    }
-    fprintf(fOut, "%.*s", size, data);
+    FPRINTF("%.*s", size, data);
     checkLineLength(size, data);
     fWroteSomething = true;
     int added = 0;
@@ -311,10 +325,7 @@
         fPendingSpace = 0;
     }
     this->writePending();
-    if (fDebugOut) {
-        debug_out((int) strlen(str), str);
-    }
-    fprintf(fOut, "%s", str);
+    FPRINTF("%s", str);
     checkLineLength(strlen(str), str);
     fColumn += len;
     fSpaces = 0;