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;
+        }
+    }
+}