work in imageinfo and phrase substitution

This adds the ability to define long phrases
in one place and refer to those phrases in
many places.

Bookmaker has new syntax to support phrase substitution.
When it encounters

#some_phrase_reference#

It substitutes the body of

#PhraseDef some_phrase_reference
text to substitute when encountering the phrase
##

The phrase label must start with a lowercase letter,
and be bracketed by single hash marks, without spaces
between the label and the hash marks.

Docs-Preview: https://skia.org/?cl=111224
TBR=caryclark@google.com
Bug: skia:6898
Change-Id: I12c57d916ccedbd86b421377d117399150ada72a
Reviewed-on: https://skia-review.googlesource.com/111224
Reviewed-by: Cary Clark <caryclark@skia.org>
Commit-Queue: Cary Clark <caryclark@skia.org>
diff --git a/tools/bookmaker/bookmaker.cpp b/tools/bookmaker/bookmaker.cpp
index 02dbbc1..91e4cc2 100644
--- a/tools/bookmaker/bookmaker.cpp
+++ b/tools/bookmaker/bookmaker.cpp
@@ -6,6 +6,7 @@
  */
 
 #include "bookmaker.h"
+#include "SkOSPath.h"
 
 #ifdef SK_BUILD_FOR_WIN
 #include <Windows.h>
@@ -189,7 +190,7 @@
                 if (!typeNameBuilder.size()) {
                     return this->reportError<bool>("unnamed topic");
                 }
-                fTopics.emplace_front(markType, defStart, fLineCount, fParent);
+                fTopics.emplace_front(markType, defStart, fLineCount, fParent, fMC);
                 RootDefinition* rootDefinition = &fTopics.front();
                 definition = rootDefinition;
                 definition->fFileName = fFileName;
@@ -277,6 +278,7 @@
         // may be one-liner
         case MarkType::kNoExample:
         case MarkType::kParam:
+        case MarkType::kPhraseDef:
         case MarkType::kReturn:
         case MarkType::kToDo:
             if (hasEnd) {
@@ -289,22 +291,37 @@
                     if (!this->popParentStack(fParent)) { // if not one liner, pop
                         return false;
                     }
-                    if (MarkType::kParam == markType || MarkType::kReturn == markType) {
+                    if (MarkType::kParam == markType || MarkType::kReturn == markType
+                            || MarkType::kPhraseDef == markType) {
                         if (!this->checkParamReturn(definition)) {
                             return false;
                         }
                     }
+                    if (MarkType::kPhraseDef == markType) {
+                        string key = definition->fName;
+                        if (fPhraseMap.end() != fPhraseMap.find(key)) {
+                            this->reportError<bool>("duplicate phrase key");
+                        }
+                        fPhraseMap[key] = definition;
+                    }
                 } else {
-                    fMarkup.emplace_front(markType, defStart, fLineCount, fParent);
+                    fMarkup.emplace_front(markType, defStart, fLineCount, fParent, fMC);
                     definition = &fMarkup.front();
                     definition->fName = typeNameBuilder[0];
                     definition->fFiddle = fParent->fFiddle;
                     definition->fContentStart = fChar;
-                    definition->fContentEnd = this->trimmedBracketEnd(fMC);
-                    this->skipToEndBracket(fMC);
+                    string endBracket;
+                    endBracket += fMC;
+                    endBracket += fMC;
+                    definition->fContentEnd = this->trimmedBracketEnd(endBracket);
+                    this->skipToEndBracket(endBracket.c_str());
                     SkAssertResult(fMC == this->next());
                     SkAssertResult(fMC == this->next());
                     definition->fTerminator = fChar;
+                    TextParser checkForChildren(definition);
+                    if (checkForChildren.strnchr(fMC, definition->fContentEnd)) {
+                        this->reportError<bool>("put ## on separate line");
+                    }
                     fParent->fChildren.push_back(definition);
                 }
                 break;
@@ -334,10 +351,11 @@
                             return this->reportError<bool>("missing example body");
                         }
                     }
-                    definition->setWrapper();
+// can't do this here; phrase refs may not have been defined yet
+//                    this->setWrapper(definition);
                 }
             } else {
-                fMarkup.emplace_front(markType, defStart, fLineCount, fParent);
+                fMarkup.emplace_front(markType, defStart, fLineCount, fParent, fMC);
                 definition = &fMarkup.front();
                 definition->fContentStart = fChar;
                 definition->fName = typeNameBuilder[0];
@@ -395,7 +413,7 @@
             } else if (!hasEnd && MarkType::kAnchor == markType) {
                 return this->reportError<bool>("anchor line must have end element last");
             }
-            fMarkup.emplace_front(markType, defStart, fLineCount, fParent);
+            fMarkup.emplace_front(markType, defStart, fLineCount, fParent, fMC);
             definition = &fMarkup.front();
             definition->fName = typeNameBuilder[0];
             definition->fFiddle = Definition::NormalizedName(typeNameBuilder[0]);
@@ -405,7 +423,7 @@
             fParent->fChildren.push_back(definition);
             if (MarkType::kAnchor == markType) {
                 this->skipToEndBracket(fMC);
-                fMarkup.emplace_front(MarkType::kLink, fChar, fLineCount, definition);
+                fMarkup.emplace_front(MarkType::kLink, fChar, fLineCount, definition, fMC);
                 SkAssertResult(fMC == this->next());
                 this->skipWhiteSpace();
                 Definition* link = &fMarkup.front();
@@ -437,7 +455,7 @@
 				if (fMC != this->next() || fMC != this->next()) {
 					return this->reportError<bool>("expected ## to delineate line");
 				}
-				fMarkup.emplace_front(MarkType::kText, start, fLineCount, definition);
+				fMarkup.emplace_front(MarkType::kText, start, fLineCount, definition, fMC);
 				Definition* text = &fMarkup.front();
 				text->fContentStart = start;
 				text->fContentEnd = end;
@@ -690,7 +708,8 @@
         const char* wordStart = fChar;
         this->skipToNonAlphaNum();
         if (fChar - wordStart > 0) {
-            fExternals.emplace_front(MarkType::kExternal, wordStart, fChar, fLineCount, fParent);
+            fExternals.emplace_front(MarkType::kExternal, wordStart, fChar, fLineCount, fParent,
+                    fMC);
             RootDefinition* definition = &fExternals.front();
             definition->fFileName = fFileName;
             definition->fName = string(wordStart ,fChar - wordStart);
@@ -700,10 +719,10 @@
     return true;
 }
 
-static bool dump_examples(FILE* fiddleOut, const Definition& def, bool* continuation) {
+bool BmhParser::dumpExamples(FILE* fiddleOut, Definition& def, bool* continuation) const {
     if (MarkType::kExample == def.fMarkType) {
         string result;
-        if (!def.exampleToScript(&result, Definition::ExampleOptions::kAll)) {
+        if (!this->exampleToScript(&def, BmhParser::ExampleOptions::kAll, &result)) {
             return false;
         }
         if (result.length() > 0) {
@@ -719,7 +738,7 @@
         return true;
     }
     for (auto& child : def.fChildren ) {
-        if (!dump_examples(fiddleOut, *child, continuation)) {
+        if (!this->dumpExamples(fiddleOut, *child, continuation)) {
             return false;
         }
     }
@@ -738,7 +757,7 @@
         if (topic.second->fParent) {
             continue;
         }
-        dump_examples(fiddleOut, *topic.second, &continuation);
+        this->dumpExamples(fiddleOut, *topic.second, &continuation);
     }
     fprintf(fiddleOut, "\n}\n");
     fclose(fiddleOut);
@@ -767,6 +786,262 @@
     return true;
 }
 
+static size_t count_indent(const string& text, size_t test, size_t end) {
+    size_t result = test;
+    while (test < end) {
+        if (' ' != text[test]) {
+            break;
+        }
+        ++test;
+    }
+    return test - result;
+}
+
+static void add_code(const string& text, int pos, int end,
+    size_t outIndent, size_t textIndent, string& example) {
+    do {
+        // fix this to move whole paragraph in, out, but preserve doc indent
+        int nextIndent = count_indent(text, pos, end);
+        size_t len = text.find('\n', pos);
+        if (string::npos == len) {
+            len = end;
+        }
+        if ((size_t) (pos + nextIndent) < len) {
+            size_t indent = outIndent + nextIndent;
+            SkASSERT(indent >= textIndent);
+            indent -= textIndent;
+            for (size_t index = 0; index < indent; ++index) {
+                example += ' ';
+            }
+            pos += nextIndent;
+            while ((size_t) pos < len) {
+                example += '"' == text[pos] ? "\\\"" :
+                    '\\' == text[pos] ? "\\\\" :
+                    text.substr(pos, 1);
+                ++pos;
+            }
+            example += "\\n";
+        } else {
+            pos += nextIndent;
+        }
+        if ('\n' == text[pos]) {
+            ++pos;
+        }
+    } while (pos < end);
+}
+
+bool BmhParser::exampleToScript(Definition* def, ExampleOptions exampleOptions,
+        string* result) const {
+    bool hasFiddle = true;
+    const Definition* platform = def->hasChild(MarkType::kPlatform);
+    if (platform) {
+        TextParser platParse(platform);
+        hasFiddle = !platParse.strnstr("!fiddle", platParse.fEnd);
+    }
+    if (!hasFiddle) {
+        *result = "";
+        return true;
+    }
+    string text = this->extractText(def, TrimExtract::kNo);
+    bool textOut = string::npos != text.find("SkDebugf(")
+        || string::npos != text.find("dump(")
+        || string::npos != text.find("dumpHex(");
+    string heightStr = "256";
+    string widthStr = "256";
+    string normalizedName(def->fFiddle);
+    string code;
+    string imageStr = "0";
+    string srgbStr = "false";
+    string durationStr = "0";
+    for (auto iter : def->fChildren) {
+        switch (iter->fMarkType) {
+        case MarkType::kDuration:
+            durationStr = string(iter->fContentStart, iter->fContentEnd - iter->fContentStart);
+            break;
+        case MarkType::kHeight:
+            heightStr = string(iter->fContentStart, iter->fContentEnd - iter->fContentStart);
+            break;
+        case MarkType::kWidth:
+            widthStr = string(iter->fContentStart, iter->fContentEnd - iter->fContentStart);
+            break;
+        case MarkType::kDescription:
+            // ignore for now
+            break;
+        case MarkType::kFunction: {
+            // emit this, but don't wrap this in draw()
+            string funcText = this->extractText(&*iter, TrimExtract::kNo);
+            size_t pos = 0;
+            while (pos < funcText.length() && ' ' > funcText[pos]) {
+                ++pos;
+            }
+            size_t indent = count_indent(funcText, pos, funcText.length());
+            add_code(funcText, pos, funcText.length(), 0, indent, code);
+            code += "\\n";
+        } break;
+        case MarkType::kComment:
+            break;
+        case MarkType::kImage:
+            imageStr = string(iter->fContentStart, iter->fContentEnd - iter->fContentStart);
+            break;
+        case MarkType::kToDo:
+            break;
+        case MarkType::kBug:
+        case MarkType::kMarkChar:
+        case MarkType::kPlatform:
+        case MarkType::kPhraseRef:
+            // ignore for now
+            break;
+        case MarkType::kSet:
+            if ("sRGB" == string(iter->fContentStart,
+                iter->fContentEnd - iter->fContentStart)) {
+                srgbStr = "true";
+            } else {
+                SkASSERT(0);   // more work to do
+                return false;
+            }
+            break;
+        case MarkType::kStdOut:
+            textOut = true;
+            break;
+        default:
+            SkASSERT(0);  // more coding to do
+        }
+    }
+    string animatedStr = "0" != durationStr ? "true" : "false";
+    string textOutStr = textOut ? "true" : "false";
+    size_t pos = 0;
+    while (pos < text.length() && ' ' > text[pos]) {
+        ++pos;
+    }
+    size_t end = text.length();
+    size_t outIndent = 0;
+    size_t textIndent = count_indent(text, pos, end);
+    if ("" == def->fWrapper) {
+        this->setWrapper(def);
+    }
+    if (def->fWrapper.length() > 0) {
+        code += def->fWrapper;
+        code += "\\n";
+        outIndent = 4;
+    }
+    add_code(text, pos, end, outIndent, textIndent, code);
+    if (def->fWrapper.length() > 0) {
+        code += "}";
+    }
+    string example = "\"" + normalizedName + "\": {\n";
+    size_t nameStart = def->fFileName.find(SkOSPath::SEPARATOR, 0);
+    SkASSERT(string::npos != nameStart);
+    string baseFile = def->fFileName.substr(nameStart + 1, def->fFileName.length() - nameStart - 5);
+    if (ExampleOptions::kText == exampleOptions) {
+        example += "    \"code\": \"" + code + "\",\n";
+        example += "    \"hash\": \"" + def->fHash + "\",\n";
+        example += "    \"file\": \"" + baseFile + "\",\n";
+        example += "    \"name\": \"" + def->fName + "\",";
+    } else {
+        example += "    \"code\": \"" + code + "\",\n";
+        if (ExampleOptions::kPng == exampleOptions) {
+            example += "    \"width\": " + widthStr + ",\n";
+            example += "    \"height\": " + heightStr + ",\n";
+            example += "    \"hash\": \"" + def->fHash + "\",\n";
+            example += "    \"file\": \"" + baseFile + "\",\n";
+            example += "    \"name\": \"" + def->fName + "\"\n";
+            example += "}";
+        } else {
+            example += "    \"options\": {\n";
+            example += "        \"width\": " + widthStr + ",\n";
+            example += "        \"height\": " + heightStr + ",\n";
+            example += "        \"source\": " + imageStr + ",\n";
+            example += "        \"srgb\": " + srgbStr + ",\n";
+            example += "        \"f16\": false,\n";
+            example += "        \"textOnly\": " + textOutStr + ",\n";
+            example += "        \"animated\": " + animatedStr + ",\n";
+            example += "        \"duration\": " + durationStr + "\n";
+            example += "    },\n";
+            example += "    \"fast\": true";
+        }
+    }
+    *result = example;
+    return true;
+}
+
+string BmhParser::extractText(const Definition* def, TrimExtract trimExtract) const {
+    string result;
+    TextParser parser(def);
+    auto childIter = def->fChildren.begin();
+    while (!parser.eof()) {
+        const char* end = def->fChildren.end() == childIter ? parser.fEnd : (*childIter)->fStart;
+        string fragment(parser.fChar, end - parser.fChar);
+        trim_end(fragment);
+        if (TrimExtract::kYes == trimExtract) {
+            trim_start(fragment);
+            if (result.length()) {
+                result += '\n';
+                result += '\n';
+            }
+        }
+        if (TrimExtract::kYes == trimExtract || has_nonwhitespace(fragment)) {
+            result += fragment;
+        }
+        parser.skipTo(end);
+        if (def->fChildren.end() != childIter) {
+            Definition* child = *childIter;
+            if (MarkType::kPhraseRef == child->fMarkType) {
+                auto phraseIter = fPhraseMap.find(child->fName);
+                if (fPhraseMap.end() == phraseIter) {
+                    return def->reportError<string>("missing phrase definition");
+                }
+                Definition* phrase = phraseIter->second;
+                // count indent of last line in result
+                size_t lastLF = result.rfind('\n');
+                size_t startPos = string::npos == lastLF ? 0 : lastLF;
+                size_t lastLen = result.length() - startPos;
+                size_t indent = count_indent(result, startPos, result.length()) + 4;
+                string phraseStr = this->extractText(phrase, TrimExtract::kNo);
+                startPos = 0;
+                bool firstTime = true;
+                size_t endPos;
+                do {
+                    endPos = phraseStr.find('\n', startPos);
+                    size_t len = (string::npos != endPos ? endPos : phraseStr.length()) - startPos;
+                    if (firstTime && lastLen + len + 1 < 100) {  // FIXME: make 100 global const or something
+                        result += ' ';
+                    } else {
+                        result += '\n';
+                        result += string(indent, ' ');
+                    }
+                    firstTime = false;
+                    string tmp = phraseStr.substr(startPos, len);
+                    result += tmp;
+                    startPos = endPos + 1;
+                } while (string::npos != endPos);
+                result += '\n';
+            }
+            parser.skipTo(child->fTerminator);
+            std::advance(childIter, 1);
+        }
+    }
+    return result;
+}
+
+void BmhParser::setWrapper(Definition* def) const {
+    const char drawWrapper[] = "void draw(SkCanvas* canvas) {";
+    const char drawNoCanvas[] = "void draw(SkCanvas* ) {";
+    string text = this->extractText(def, TrimExtract::kNo);
+    size_t nonSpace = 0;
+    while (nonSpace < text.length() && ' ' >= text[nonSpace]) {
+        ++nonSpace;
+    }
+    bool hasFunc = !text.compare(nonSpace, sizeof(drawWrapper) - 1, drawWrapper);
+    bool noCanvas = !text.compare(nonSpace, sizeof(drawNoCanvas) - 1, drawNoCanvas);
+    bool hasCanvas = string::npos != text.find("SkCanvas canvas");
+    SkASSERT(!hasFunc || !noCanvas);
+    bool preprocessor = text[0] == '#';
+    bool wrapCode = !hasFunc && !noCanvas && !preprocessor;
+    if (wrapCode) {
+        def->fWrapper = hasCanvas ? string(drawNoCanvas) : string(drawWrapper);
+    }
+}
+
 // FIXME: some examples may produce different output on different platforms
 // if the text output can be different, think of how to author that
 
@@ -812,7 +1087,7 @@
                     if (' ' >= fMC) {
                         return this->reportError<bool>("illegal markup character");
                     }
-                    fMarkup.emplace_front(MarkType::kMarkChar, fChar - 1, fLineCount, fParent);
+                    fMarkup.emplace_front(MarkType::kMarkChar, fChar - 4, fLineCount, fParent, fMC);
                     Definition* markChar = &fMarkup.front();
                     markChar->fContentStart = fChar - 1;
                     this->skipToEndBracket('\n');
@@ -865,7 +1140,7 @@
                                 }
                             } else {  // one line comment
                                 fMarkup.emplace_front(MarkType::kComment, fChar - 1, fLineCount,
-                                        fParent);
+                                        fParent, fMC);
                                 Definition* comment = &fMarkup.front();
                                 comment->fContentStart = fChar - 1;
                                 this->skipToEndBracket('\n');
@@ -890,7 +1165,7 @@
                 } else if (TableState::kNone == fTableState) {
                     // fixme? no nested tables for now
                     fColStart = fChar - 1;
-                    fMarkup.emplace_front(MarkType::kRow, fColStart, fLineCount, fParent);
+                    fMarkup.emplace_front(MarkType::kRow, fColStart, fLineCount, fParent, fMC);
                     fRow = &fMarkup.front();
                     fRow->fName = fParent->fName;
                     this->skipWhiteSpace();
@@ -899,7 +1174,7 @@
                     fTableState = TableState::kColumnStart;
                 }
                 if (TableState::kColumnStart == fTableState) {
-                    fMarkup.emplace_front(MarkType::kColumn, fColStart, fLineCount, fParent);
+                    fMarkup.emplace_front(MarkType::kColumn, fColStart, fLineCount, fParent, fMC);
                     fWorkingColumn = &fMarkup.front();
                     fWorkingColumn->fName = fParent->fName;
                     fWorkingColumn->fContentStart = fChar;
@@ -907,6 +1182,28 @@
                     fTableState = TableState::kColumnEnd;
                     continue;
                 }
+            } else if (this->peek() >= 'a' && this->peek() <= 'z') {
+                // expect zero or more letters and underscores (no spaces) then hash
+                const char* phraseNameStart = fChar;
+                this->skipPhraseName();
+                string phraseKey = string(phraseNameStart, fChar - phraseNameStart);
+                if (fMC != this->next()) {
+                    return this->reportError<bool>("expect # after phrase-name");
+                }
+                const char* start = phraseNameStart;
+                SkASSERT('#' == start[-1]);
+                --start;
+                if (start > fStart && ' ' >= start[-1]) {
+                    --start;  // preserve whether to add whitespace before substitution
+                }
+                fMarkup.emplace_front(MarkType::kPhraseRef, start, fLineCount, fParent, fMC);
+                Definition* markChar = &fMarkup.front();
+                markChar->fContentStart = fChar;
+                this->skipToEndBracket('\n');
+                markChar->fContentEnd = fChar;
+                markChar->fTerminator = fChar;
+                markChar->fName = phraseKey;
+                fParent->fChildren.push_back(markChar);
             }
         }
         char nextChar = this->next();
@@ -1316,6 +1613,45 @@
     return parent;
 }
 
+const char* BmhParser::checkForFullTerminal(const char* end, const Definition* definition) const {
+    const char* start = end;
+    while ('\n' != start[0] && start > fStart) {
+        --start;
+    }
+    SkASSERT (start < end);
+    // if end is preceeeded by \n#MarkType ## backup to there
+    TextParser parser(fFileName, start, fChar, fLineCount);
+    parser.skipWhiteSpace();
+    if (parser.eof() || fMC != parser.next()) {
+        return end;
+    }
+    const char* markName = fMaps[(int) definition->fMarkType].fName;
+    if (!parser.skipExact(markName)) {
+        return end;
+    }
+    parser.skipWhiteSpace();
+    const char* nameStart = parser.fChar;
+    if (isupper(nameStart[0])) {
+        parser.skipToWhiteSpace();
+        if (parser.eof()) {
+            return end;
+        }
+        string defName = string(nameStart, parser.fChar - nameStart);
+        size_t defNamePos = definition->fName.rfind(defName);
+        if (definition->fName.length() != defNamePos + defName.length()) {
+            return end;
+        }
+    }
+    parser.skipWhiteSpace();
+    if (fMC != parser.next()) {
+        return end;
+    }
+    if (!parser.eof() && fMC != parser.next()) {
+        return end;
+    }
+    return start;
+}
+
 bool BmhParser::popParentStack(Definition* definition) {
     if (!fParent) {
         return this->reportError<bool>("missing parent");
@@ -1329,7 +1665,19 @@
     if (definition->fContentEnd) {
         return this->reportError<bool>("definition already ended");
     }
-    definition->fContentEnd = fLine - 1;
+    // more to figure out to handle table columns, at minimum
+    const char* end = fChar;
+    if (fMC != end[0]) {
+        while (end > definition->fContentStart && ' ' >= end[-1]) {
+            --end;
+        }
+        SkASSERT(&end[-1] >= definition->fContentStart && fMC == end[-1]
+                && (MarkType::kColumn == definition->fMarkType
+                || (&end[-2] >= definition->fContentStart && fMC == end[-2])));
+        end -= 2;
+    }
+    end = checkForFullTerminal(end, definition);
+    definition->fContentEnd = end;
     definition->fTerminator = fChar;
     fParent = definition->fParent;
     if (!fParent || (MarkType::kTopic == fParent->fMarkType && !fParent->fParent)) {
@@ -1629,7 +1977,8 @@
             builder = this->typedefName();
             break;
         case MarkType::kParam:
-           // fixme: expect camelCase
+        case MarkType::kPhraseDef:
+            // fixme: expect camelCase for param
             builder = this->word("", "");
             this->skipSpace();
             *checkEnd = false;
diff --git a/tools/bookmaker/bookmaker.h b/tools/bookmaker/bookmaker.h
index 7bb743c..db0d992 100644
--- a/tools/bookmaker/bookmaker.h
+++ b/tools/bookmaker/bookmaker.h
@@ -120,6 +120,8 @@
     kNoExample,
     kOutdent,
     kParam,
+    kPhraseDef,
+    kPhraseRef,
     kPlatform,
     kPopulate,
     kPrivate,
@@ -504,6 +506,12 @@
         }
     }
 
+    void skipPhraseName() {
+        while (fChar < fEnd && (islower(fChar[0]) || '_' == fChar[0])) {
+            fChar++;
+        }
+    }
+
     void skipToSpace() {
         while (fChar < fEnd && ' ' != fChar[0]) {
             fChar++;
@@ -637,6 +645,17 @@
         return fChar + index;
     }
 
+    const char* trimmedBracketEnd(string bracket) const {
+        size_t max = (size_t) (this->lineLength());
+        string line(fChar, max);
+        size_t index = line.find(bracket);
+        SkASSERT(index < max);
+        while (index > 0 && ' ' >= fChar[index - 1]) {
+            --index;
+        }
+        return fChar + index;
+    }
+
     const char* trimmedLineEnd() const {
         const char* result = this->lineEnd();
         while (result > fChar && ' ' >= result[-1]) {
@@ -730,17 +749,6 @@
         kFileType,
     };
 
-    enum class TrimExtract {
-        kNo,
-        kYes
-    };
-
-    enum class ExampleOptions {
-        kText,
-        kPng,
-        kAll
-    };
-
     enum class MethodType {
         kNone,
         kConstructor,
@@ -775,13 +783,14 @@
 
     Definition() {}
 
-    Definition(const char* start, const char* end, int line, Definition* parent)
+    Definition(const char* start, const char* end, int line, Definition* parent, char mc)
         : fStart(start)
         , fContentStart(start)
         , fContentEnd(end)
         , fParent(parent)
         , fLineCount(line)
-        , fType(Type::kWord) {
+        , fType(Type::kWord)
+        , fMC(mc) {
         if (parent) {
             SkASSERT(parent->fFileName.length() > 0);
             fFileName = parent->fFileName;
@@ -789,31 +798,31 @@
         this->setParentIndex();
     }
 
-    Definition(MarkType markType, const char* start, int line, Definition* parent)
-        : Definition(markType, start, nullptr, line, parent) {
+    Definition(MarkType markType, const char* start, int line, Definition* parent, char mc)
+        : Definition(markType, start, nullptr, line, parent, mc) {
     }
 
-    Definition(MarkType markType, const char* start, const char* end, int line, Definition* parent)
-        : Definition(start, end, line, parent) {
+    Definition(MarkType markType, const char* start, const char* end, int line, Definition* parent, char mc)
+        : Definition(start, end, line, parent, mc) {
         fMarkType = markType;
         fType = Type::kMark;
     }
 
-    Definition(Bracket bracket, const char* start, int lineCount, Definition* parent)
-        : Definition(start, nullptr, lineCount, parent) {
+    Definition(Bracket bracket, const char* start, int lineCount, Definition* parent, char mc)
+        : Definition(start, nullptr, lineCount, parent, mc) {
         fBracket = bracket;
         fType = Type::kBracket;
     }
 
     Definition(KeyWord keyWord, const char* start, const char* end, int lineCount,
-            Definition* parent)
-        : Definition(start, end, lineCount, parent) {
+            Definition* parent, char mc)
+        : Definition(start, end, lineCount, parent, mc) {
         fKeyWord = keyWord;
         fType = Type::kKeyWord;
     }
 
-    Definition(Punctuation punctuation, const char* start, int lineCount, Definition* parent)
-        : Definition(start, nullptr, lineCount, parent) {
+    Definition(Punctuation punctuation, const char* start, int lineCount, Definition* parent, char mc)
+        : Definition(start, nullptr, lineCount, parent, mc) {
         fPunctuation = punctuation;
         fType = Type::kPunctuation;
     }
@@ -845,8 +854,6 @@
         return nullptr;
     }
 
-    bool exampleToScript(string* result, ExampleOptions ) const;
-    string extractText(TrimExtract trimExtract) const;
     string fiddleName() const;
     const Definition* findClone(string match) const;
     string formatFunction(Format format) const;
@@ -901,8 +908,6 @@
         fParentIndex = fParent ? (int) fParent->fTokens.size() : -1;
     }
 
-    void setWrapper();
-
     const Definition* topicParent() const {
         Definition* test = fParent;
         while (test) {
@@ -936,6 +941,7 @@
     MethodType fMethodType = MethodType::kNone;
     Operator fOperator = Operator::kUnknown;
     Type fType = Type::kNone;
+    char fMC = '#';
     bool fClone = false;
     bool fCloned = false;
     bool fDeprecated = false;
@@ -958,12 +964,12 @@
     RootDefinition() {
     }
 
-    RootDefinition(MarkType markType, const char* start, int line, Definition* parent)
-            : Definition(markType, start, line, parent) {
+    RootDefinition(MarkType markType, const char* start, int line, Definition* parent, char mc)
+            : Definition(markType, start, line, parent, mc) {
     }
 
     RootDefinition(MarkType markType, const char* start, const char* end, int line,
-            Definition* parent) : Definition(markType, start, end,  line, parent) {
+            Definition* parent, char mc) : Definition(markType, start, end,  line, parent, mc) {
     }
 
     ~RootDefinition() override {
@@ -1205,6 +1211,12 @@
 		kClone,   // resolved, output, with references to clones as well
     };
 
+    enum class ExampleOptions {
+        kText,
+        kPng,
+        kAll
+    };
+
     enum class Exemplary {
         kNo,
         kYes,
@@ -1222,6 +1234,11 @@
         kYes,
     };
 
+    enum class TrimExtract {
+        kNo,
+        kYes
+    };
+
 #define M(mt) (1LL << (int) MarkType::k##mt)
 #define M_D M(Description)
 #define M_CS M(Class) | M(Struct)
@@ -1287,6 +1304,8 @@
 , { "NoExample",   nullptr,      MarkType::kNoExample,    R_O, E_N, M_CSST | M_E | M(Method) }
 , { "Outdent",     nullptr,      MarkType::kOutdent,      R_N, E_N, M(Code) }
 , { "Param",       nullptr,      MarkType::kParam,        R_Y, E_N, M(Method) }
+, { "PhraseDef",   nullptr,      MarkType::kPhraseDef,    R_Y, E_N, M(Subtopic) }
+, { "",            nullptr,      MarkType::kPhraseRef,    R_Y, E_N, 0 }
 , { "Platform",    nullptr,      MarkType::kPlatform,     R_N, E_N, M(Example) | M(NoExample) }
 , { "Populate",    nullptr,      MarkType::kPopulate,     R_N, E_N, M(Subtopic) }
 , { "Private",     nullptr,      MarkType::kPrivate,      R_N, E_N, 0 }
@@ -1333,13 +1352,17 @@
             const vector<string>& typeNameBuilder, HasTag hasTag);
     bool checkEndMarker(MarkType markType, string name) const;
     bool checkExamples() const;
+    const char* checkForFullTerminal(const char* end, const Definition* ) const;
     bool checkParamReturn(const Definition* definition) const;
+    bool dumpExamples(FILE* fiddleOut, Definition& def, bool* continuation) const;
     bool dumpExamples(const char* fiddleJsonFileName) const;
     bool childOf(MarkType markType) const;
     string className(MarkType markType);
     bool collectExternals();
     int endHashCount() const;
     bool endTableColumn(const char* end, const char* terminator);
+    bool exampleToScript(Definition*, ExampleOptions, string* result ) const;
+    string extractText(const Definition* , TrimExtract ) const;
 
     RootDefinition* findBmhObject(MarkType markType, const string& typeName) const {
         auto map = fMaps[(int) markType].fBmh;
@@ -1383,6 +1406,7 @@
         fCheckMethods = false;
     }
 
+    void setWrapper(Definition* def) const;
     bool skipNoName();
     bool skipToDefinitionEnd(MarkType markType);
 	bool skipToString();
@@ -1418,6 +1442,7 @@
     unordered_map<string, RootDefinition> fTypedefMap;
     unordered_map<string, Definition*> fTopicMap;
     unordered_map<string, Definition*> fAliasMap;
+    unordered_map<string, Definition*> fPhraseMap;
     RootDefinition* fRoot;
     Definition* fWorkingColumn;
     Definition* fRow;
@@ -1485,6 +1510,8 @@
         , { nullptr,        MarkType::kNoExample }
         , { nullptr,        MarkType::kOutdent }
         , { nullptr,        MarkType::kParam }
+        , { nullptr,        MarkType::kPhraseDef }
+        , { nullptr,        MarkType::kPhraseRef }
         , { nullptr,        MarkType::kPlatform }
         , { nullptr,        MarkType::kPopulate }
         , { nullptr,        MarkType::kPrivate }
@@ -1517,11 +1544,11 @@
     void addKeyword(KeyWord keyWord);
 
     void addPunctuation(Punctuation punctuation) {
-        fParent->fTokens.emplace_back(punctuation, fChar, fLineCount, fParent);
+        fParent->fTokens.emplace_back(punctuation, fChar, fLineCount, fParent, '\0');
     }
 
     void addWord() {
-        fParent->fTokens.emplace_back(fIncludeWord, fChar, fLineCount, fParent);
+        fParent->fTokens.emplace_back(fIncludeWord, fChar, fLineCount, fParent, '\0');
         fIncludeWord = nullptr;
     }
 
@@ -1602,7 +1629,7 @@
 
     void pushBracket(Bracket bracket) {
         this->setBracketShortCuts(bracket);
-        fParent->fTokens.emplace_back(bracket, fChar, fLineCount, fParent);
+        fParent->fTokens.emplace_back(bracket, fChar, fLineCount, fParent, '\0');
         Definition* container = &fParent->fTokens.back();
         this->addDefinition(container);
     }
@@ -1889,7 +1916,7 @@
 
 	void constOut(const Definition* memberStart, const Definition& child,
 		const Definition* bmhConst);
-    void descriptionOut(const Definition* def, SkipFirstLine );
+    void descriptionOut(const Definition* def, SkipFirstLine , Phrase );
     void enumHeaderOut(const RootDefinition* root, const Definition& child);
     void enumMembersOut(const RootDefinition* root, Definition& child);
     void enumSizeItems(const Definition& child);
diff --git a/tools/bookmaker/cataloger.cpp b/tools/bookmaker/cataloger.cpp
index b3f578d..a564d3e 100644
--- a/tools/bookmaker/cataloger.cpp
+++ b/tools/bookmaker/cataloger.cpp
@@ -103,7 +103,7 @@
 
 bool Catalog::pngOut(Definition* example) {
     string result;
-    if (!example->exampleToScript(&result, Definition::ExampleOptions::kPng)) {
+    if (!fBmhParser->exampleToScript(example, BmhParser::ExampleOptions::kPng, &result)) {
         return false;
     }
     if (result.length() > 0) {
@@ -121,7 +121,7 @@
 bool Catalog::textOut(Definition* def, const char* stdOutStart,
     const char* stdOutEnd) {
     string result;
-    if (!def->exampleToScript(&result, Definition::ExampleOptions::kText)) {
+    if (!fBmhParser->exampleToScript(def, BmhParser::ExampleOptions::kText, &result)) {
         return false;
     }
     if (result.length() > 0) {
diff --git a/tools/bookmaker/definition.cpp b/tools/bookmaker/definition.cpp
index 68e001a..14f6daa 100644
--- a/tools/bookmaker/definition.cpp
+++ b/tools/bookmaker/definition.cpp
@@ -6,51 +6,6 @@
  */
 
 #include "bookmaker.h"
-#include "SkOSPath.h"
-
-static size_t count_indent(const string& text, size_t test, size_t end) {
-    size_t result = test;
-    while (test < end) {
-        if (' ' != text[test]) {
-            break;
-        }
-        ++test;
-    }
-    return test - result;
-}
-
-static void add_code(const string& text, int pos, int end,
-        size_t outIndent, size_t textIndent, string& example) {
-    do {
-         // fix this to move whole paragraph in, out, but preserve doc indent
-        int nextIndent = count_indent(text, pos, end);
-        size_t len = text.find('\n', pos);
-        if (string::npos == len) {
-            len = end;
-        }
-        if ((size_t) (pos + nextIndent) < len) {
-            size_t indent = outIndent + nextIndent;
-            SkASSERT(indent >= textIndent);
-            indent -= textIndent;
-            for (size_t index = 0; index < indent; ++index) {
-                example += ' ';
-            }
-            pos += nextIndent;
-            while ((size_t) pos < len) {
-                example += '"' == text[pos] ? "\\\"" :
-                    '\\' == text[pos] ? "\\\\" :
-                    text.substr(pos, 1);
-                ++pos;
-            }
-            example += "\\n";
-        } else {
-            pos += nextIndent;
-        }
-        if ('\n' == text[pos]) {
-            ++pos;
-        }
-    } while (pos < end);
-}
 
 #ifdef CONST
 #undef CONST
@@ -487,215 +442,6 @@
     fFiddle = Definition::NormalizedName(result);
 }
 
-void Definition::setWrapper() {
-    const char drawWrapper[] = "void draw(SkCanvas* canvas) {";
-    const char drawNoCanvas[] = "void draw(SkCanvas* ) {";
-    string text = this->extractText(Definition::TrimExtract::kNo);
-    size_t nonSpace = 0;
-    while (nonSpace < text.length() && ' ' >= text[nonSpace]) {
-        ++nonSpace;
-    }
-    bool hasFunc = !text.compare(nonSpace, sizeof(drawWrapper) - 1, drawWrapper);
-    bool noCanvas = !text.compare(nonSpace, sizeof(drawNoCanvas) - 1, drawNoCanvas);
-    bool hasCanvas = string::npos != text.find("SkCanvas canvas");
-    SkASSERT(!hasFunc || !noCanvas);
-    bool preprocessor = text[0] == '#';
-    bool wrapCode = !hasFunc && !noCanvas && !preprocessor;
-    if (wrapCode) {
-        fWrapper = hasCanvas ? string(drawNoCanvas) : string(drawWrapper);
-    }
-}
-
-bool Definition::exampleToScript(string* result, ExampleOptions exampleOptions) const {
-    bool hasFiddle = true;
-    const Definition* platform = this->hasChild(MarkType::kPlatform);
-    if (platform) {
-        TextParser platParse(platform);
-        hasFiddle = !platParse.strnstr("!fiddle", platParse.fEnd);
-    }
-    if (!hasFiddle) {
-        *result = "";
-        return true;
-    }
-    string text = this->extractText(Definition::TrimExtract::kNo);
-    bool textOut = string::npos != text.find("SkDebugf(")
-            || string::npos != text.find("dump(")
-            || string::npos != text.find("dumpHex(");
-    string heightStr = "256";
-    string widthStr = "256";
-    string normalizedName(fFiddle);
-    string code;
-    string imageStr = "0";
-    string srgbStr = "false";
-    string durationStr = "0";
-    for (auto const& iter : fChildren) {
-        switch (iter->fMarkType) {
-            case MarkType::kDuration:
-                durationStr = string(iter->fContentStart, iter->fContentEnd - iter->fContentStart);
-                break;
-            case MarkType::kHeight:
-                heightStr = string(iter->fContentStart, iter->fContentEnd - iter->fContentStart);
-                break;
-            case MarkType::kWidth:
-                widthStr = string(iter->fContentStart, iter->fContentEnd - iter->fContentStart);
-                break;
-            case MarkType::kDescription:
-                // ignore for now
-                break;
-            case MarkType::kFunction: {
-                // emit this, but don't wrap this in draw()
-                string funcText(iter->fContentStart, iter->fContentEnd - iter->fContentStart);
-                size_t pos = 0;
-                while (pos < funcText.length() && ' ' > funcText[pos]) {
-                    ++pos;
-                }
-                size_t indent = count_indent(funcText, pos, funcText.length());
-                add_code(funcText, pos, funcText.length(), 0, indent, code);
-                code += "\\n";
-                } break;
-            case MarkType::kComment:
-                break;
-            case MarkType::kImage:
-                imageStr = string(iter->fContentStart, iter->fContentEnd - iter->fContentStart);
-                break;
-            case MarkType::kToDo:
-                break;
-            case MarkType::kBug:
-            case MarkType::kMarkChar:
-            case MarkType::kPlatform:
-                // ignore for now
-                break;
-            case MarkType::kSet:
-                if ("sRGB" == string(iter->fContentStart,
-                                     iter->fContentEnd - iter->fContentStart)) {
-                    srgbStr = "true";
-                } else {
-                    SkASSERT(0);   // more work to do
-                    return false;
-                }
-                break;
-            case MarkType::kStdOut:
-                textOut = true;
-                break;
-            default:
-                SkASSERT(0);  // more coding to do
-        }
-    }
-    string animatedStr = "0" != durationStr ? "true" : "false";
-    string textOutStr = textOut ? "true" : "false";
-    size_t pos = 0;
-    while (pos < text.length() && ' ' > text[pos]) {
-        ++pos;
-    }
-    size_t end = text.length();
-    size_t outIndent = 0;
-    size_t textIndent = count_indent(text, pos, end);
-    if (fWrapper.length() > 0) {
-        code += fWrapper;
-        code += "\\n";
-        outIndent = 4;
-    }
-    add_code(text, pos, end, outIndent, textIndent, code);
-    if (fWrapper.length() > 0) {
-        code += "}";
-    }
-    string example = "\"" + normalizedName + "\": {\n";
-    size_t nameStart = fFileName.find(SkOSPath::SEPARATOR, 0);
-    SkASSERT(string::npos != nameStart);
-    string baseFile = fFileName.substr(nameStart + 1, fFileName.length() - nameStart - 5);
-    if (ExampleOptions::kText == exampleOptions) {
-        example += "    \"code\": \"" + code + "\",\n";
-        example += "    \"hash\": \"" + fHash + "\",\n";
-        example += "    \"file\": \"" + baseFile + "\",\n";
-        example += "    \"name\": \"" + fName + "\",";
-    } else {
-        example += "    \"code\": \"" + code + "\",\n";
-        if (ExampleOptions::kPng == exampleOptions) {
-            example += "    \"width\": " + widthStr + ",\n";
-            example += "    \"height\": " + heightStr + ",\n";
-            example += "    \"hash\": \"" + fHash + "\",\n";
-            example += "    \"file\": \"" + baseFile + "\",\n";
-            example += "    \"name\": \"" + fName + "\"\n";
-            example += "}";
-       } else {
-            example += "    \"options\": {\n";
-            example += "        \"width\": " + widthStr + ",\n";
-            example += "        \"height\": " + heightStr + ",\n";
-            example += "        \"source\": " + imageStr + ",\n";
-            example += "        \"srgb\": " + srgbStr + ",\n";
-            example += "        \"f16\": false,\n";
-            example += "        \"textOnly\": " + textOutStr + ",\n";
-            example += "        \"animated\": " + animatedStr + ",\n";
-            example += "        \"duration\": " + durationStr + "\n";
-            example += "    },\n";
-            example += "    \"fast\": true";
-        }
-    }
-    *result = example;
-    return true;
-}
-
-string Definition::extractText(TrimExtract trimExtract) const {
-    string result;
-    TextParser parser(fFileName, fContentStart, fContentEnd, fLineCount);
-    int childIndex = 0;
-    char mc = '#';
-    while (parser.fChar < parser.fEnd) {
-        if (TrimExtract::kYes == trimExtract && !parser.skipWhiteSpace()) {
-            break;
-        }
-        if (parser.next() == mc) {
-            if (parser.next() == mc) {
-                if (parser.next() == mc) {
-                    mc = parser.next();
-                }
-            } else {
-                // fixme : more work to do if # style comment is in text
-                // if in method definition, could be alternate method name
-                --parser.fChar;
-                if (' ' < parser.fChar[0]) {
-                    if (islower(parser.fChar[0])) {
-                        result += '\n';
-                        parser.skipLine();
-                    } else {
-                        SkASSERT(isupper(parser.fChar[0]));
-                        parser.skipTo(fChildren[childIndex]->fTerminator);
-                        if (mc == parser.fChar[0] && mc == parser.fChar[1]) {
-                            parser.next();
-                            parser.next();
-                        }
-                        childIndex++;
-                    }
-                } else {
-                    parser.skipLine();
-                }
-                continue;
-            }
-        } else {
-            --parser.fChar;
-        }
-        const char* end = parser.fEnd;
-        const char* mark = parser.strnchr(mc, end);
-        if (mark) {
-            end = mark;
-        }
-        string fragment(parser.fChar, end - parser.fChar);
-        trim_end(fragment);
-        if (TrimExtract::kYes == trimExtract) {
-            trim_start(fragment);
-            if (result.length()) {
-                result += '\n';
-                result += '\n';
-            }
-        }
-        if (TrimExtract::kYes == trimExtract || has_nonwhitespace(fragment)) {
-            result += fragment;
-        }
-        parser.skipTo(end);
-    }
-    return result;
-}
-
 static void space_pad(string* str) {
     size_t len = str->length();
     if (len == 0) {
diff --git a/tools/bookmaker/includeParser.cpp b/tools/bookmaker/includeParser.cpp
index 385b9c9..07189dc 100644
--- a/tools/bookmaker/includeParser.cpp
+++ b/tools/bookmaker/includeParser.cpp
@@ -87,7 +87,7 @@
 }
 
 void IncludeParser::addKeyword(KeyWord keyWord) {
-    fParent->fTokens.emplace_back(keyWord, fIncludeWord, fChar, fLineCount, fParent);
+    fParent->fTokens.emplace_back(keyWord, fIncludeWord, fChar, fLineCount, fParent, '\0');
     fIncludeWord = nullptr;
     if (KeyProperty::kObject == kKeyWords[(int) keyWord].fProperty) {
         Definition* def = &fParent->fTokens.back();
@@ -1295,7 +1295,7 @@
         }
         const char* lineEnd = parser.trimmedLineEnd();
         markupDef->fTokens.emplace_back(MarkType::kComment, parser.fChar, lineEnd,
-                parser.fLineCount, parent);
+                parser.fLineCount, parent, '\0');
         parser.skipToEndBracket('\n');
     }
     return true;
@@ -1338,7 +1338,7 @@
         markupChild->fLineCount = child->fLineCount;
     } else {
         markupDef->fTokens.emplace_back(MarkType::kEnum, child->fContentStart, child->fContentEnd,
-            child->fLineCount, markupDef);
+            child->fLineCount, markupDef, '\0');
         markupChild = &markupDef->fTokens.back();
     }
     SkASSERT(KeyWord::kNone == markupChild->fKeyWord);
@@ -1385,7 +1385,7 @@
                 parser.skipToLineStart();
             }
             markupChild->fTokens.emplace_back(MarkType::kComment, start, end, parser.fLineCount,
-                    markupChild);
+                    markupChild, '\0');
             comment = &markupChild->fTokens.back();
             comment->fTerminator = end;
             if (!this->parseComment(parser.fFileName, start, end, parser.fLineCount, comment)) {
@@ -1439,12 +1439,12 @@
             SkASSERT(!parser.eof());
             const char* commentEnd = parser.fChar;
             markupChild->fTokens.emplace_back(MarkType::kComment, commentStart, commentEnd,
-                    parser.fLineCount, markupChild);
+                    parser.fLineCount, markupChild, '\0');
             comment = &markupChild->fTokens.back();
             comment->fTerminator = commentEnd;
         }
         markupChild->fTokens.emplace_back(MarkType::kMember, dataStart, dataEnd, parser.fLineCount,
-                markupChild);
+                markupChild, '\0');
         Definition* member = &markupChild->fTokens.back();
         member->fName = memberName;
         if (comment) {
@@ -1463,7 +1463,7 @@
         }
         SkASSERT(KeyWord::kStatic == outsideMember->fKeyWord);
         markupChild->fTokens.emplace_back(MarkType::kMember, outsideMember->fContentStart,
-                outsideMember->fContentEnd, outsideMember->fLineCount, markupChild);
+                outsideMember->fContentEnd, outsideMember->fLineCount, markupChild, '\0');
         Definition* member = &markupChild->fTokens.back();
         member->fName = outsideMember->fName;
         // FIXME: ? add comment as well ?
@@ -1503,7 +1503,7 @@
 bool IncludeParser::parseMember(Definition* child, Definition* markupDef) {
     const char* typeStart = child->fChildren[0]->fContentStart;
     markupDef->fTokens.emplace_back(MarkType::kMember, typeStart, child->fContentStart,
-        child->fLineCount, markupDef);
+        child->fLineCount, markupDef, '\0');
     Definition* markupChild = &markupDef->fTokens.back();
     TextParser nameParser(child);
     nameParser.skipToNonAlphaNum();
@@ -1534,7 +1534,7 @@
                     }
                 }
                 markupDef->fTokens.emplace_back(MarkType::kComment, start, end, child->fLineCount,
-                        markupDef);
+                        markupDef, '\0');
                 Definition* commentChild = &markupDef->fTokens.back();
                 markupChild->fChildren.emplace_back(commentChild);
                 parser.skipTo(end);
@@ -1676,7 +1676,7 @@
         return true;
     }
     markupDef->fTokens.emplace_back(MarkType::kMethod, start, end, tokenIter->fLineCount,
-            markupDef);
+            markupDef, '\0');
     Definition* markupChild = &markupDef->fTokens.back();
     // do find instead -- I wonder if there is a way to prevent this in c++
     IClassDefinition& classDef = fIClassMap[markupDef->fName];
@@ -1873,7 +1873,7 @@
         return true;
     }
     markupDef->fTokens.emplace_back(MarkType::kTypedef, child->fContentStart, child->fContentEnd,
-        child->fLineCount, markupDef);
+        child->fLineCount, markupDef, '\0');
     Definition* markupChild = &markupDef->fTokens.back();
     markupChild->fName = nameStr;
     markupChild->fTerminator = markupChild->fContentEnd;
diff --git a/tools/bookmaker/includeWriter.cpp b/tools/bookmaker/includeWriter.cpp
index 328e941..83895ea 100644
--- a/tools/bookmaker/includeWriter.cpp
+++ b/tools/bookmaker/includeWriter.cpp
@@ -15,13 +15,14 @@
     this->lf(2);
     this->writeCommentHeader();
     fIndent += 4;
-    this->descriptionOut(bmhConst, SkipFirstLine::kYes);
+    this->descriptionOut(bmhConst, SkipFirstLine::kYes, Phrase::kNo);
     fIndent -= 4;
     this->writeCommentTrailer();
     fStart = memberStart->fContentStart;
 }
 
-void IncludeWriter::descriptionOut(const Definition* def, SkipFirstLine skipFirstLine) {
+void IncludeWriter::descriptionOut(const Definition* def, SkipFirstLine skipFirstLine,
+            Phrase phrase) {
     const char* commentStart = def->fContentStart;
     if (SkipFirstLine::kYes == skipFirstLine) {
         TextParser parser(def);
@@ -99,7 +100,11 @@
                 commentLen = (int) (prop->fContentEnd - commentStart);
                 if (commentLen > 0) {
                     this->writeBlockIndent(commentLen, commentStart);
-                    if ('\n' != commentStart[commentLen - 1] && '\n' == commentStart[commentLen]) {
+                    const char* end = commentStart + commentLen;
+                    while (end > commentStart && ' ' == end[-1]) {
+                        --end;
+                    }
+                    if (end > commentStart && '\n' == end[-1]) {
                         this->lfcr();
                     }
                 }
@@ -175,7 +180,7 @@
                         SkASSERT(MarkType::kColumn == column->fMarkType);
                         this->writeString("-");
                         this->writeSpace();
-                        this->descriptionOut(column, SkipFirstLine::kNo);
+                        this->descriptionOut(column, SkipFirstLine::kNo, Phrase::kNo);
                         this->lf(1);
                     }
                 }
@@ -185,6 +190,22 @@
                     this->lf(2);
                 }
                 break;
+            case MarkType::kPhraseRef: {
+                commentLen = prop->fStart - commentStart;
+                if (commentLen > 0) {
+                    this->rewriteBlock(commentLen, commentStart, Phrase::kNo);
+                    // ince we don't do line wrapping, always insert LF before phrase
+                    this->lfcr();   // TODO: remove this once rewriteBlock rewraps paragraphs
+                }
+                auto iter = fBmhParser->fPhraseMap.find(prop->fName);
+                if (fBmhParser->fPhraseMap.end() == iter) {
+                    return this->reportError<void>("missing phrase definition");
+                }
+                Definition* phraseDef = iter->second;
+                this->rewriteBlock(phraseDef->length(), phraseDef->fContentStart, Phrase::kYes);
+                commentStart = prop->fContentStart;
+                commentLen = (int) (def->fContentEnd - commentStart);
+                } break;
             default:
                 commentLen = (int) (prop->fStart - commentStart);
                 breakOut = true;
@@ -195,7 +216,7 @@
     }
     SkASSERT(wroteCode || (commentLen > 0 && commentLen < 1500) || def->fDeprecated);
     if (commentLen > 0) {
-        this->rewriteBlock(commentLen, commentStart, Phrase::kNo);
+        this->rewriteBlock(commentLen, commentStart, phrase);
     }
 }
 
@@ -654,7 +675,7 @@
     }
     this->writeCommentHeader();
     fIndent += 4;
-    this->descriptionOut(method, SkipFirstLine::kNo);
+    this->descriptionOut(method, SkipFirstLine::kNo, Phrase::kNo);
     // compute indention column
     size_t column = 0;
     bool hasParmReturn = false;
@@ -671,8 +692,6 @@
         column += fIndent + sizeof("@return ");
         int saveIndent = fIndent;
         for (auto methodPart : method->fChildren) {
-            const char* partStart = methodPart->fContentStart;
-            const char* partEnd = methodPart->fContentEnd;
             if (MarkType::kParam == methodPart->fMarkType) {
                 this->writeString("@param");
                 this->writeSpace();
@@ -682,18 +701,24 @@
             } else {
                 continue;
             }
+            this->indentToColumn(column);
+            fIndent = column;
+#if 0
+            const char* partStart = methodPart->fContentStart;
+            const char* partEnd = methodPart->fContentEnd;
             while ('\n' == partEnd[-1]) {
                 --partEnd;
             }
             while ('#' == partEnd[-1]) { // FIXME: so wrong; should not be before fContentEnd
                 --partEnd;
             }
-            this->indentToColumn(column);
             int partLen = (int) (partEnd - partStart);
             // FIXME : detect this earlier; assert if #Return is empty
             SkASSERT(partLen > 0 && partLen < 300);  // may assert if param desc is especially long
-            fIndent = column;
             this->rewriteBlock(partLen, partStart, Phrase::kYes);
+#else
+            this->descriptionOut(methodPart, SkipFirstLine::kNo, Phrase::kYes);
+#endif
             fIndent = saveIndent;
             this->lfcr();
         }
@@ -820,7 +845,7 @@
         this->indentToColumn(fStructCommentTab);
         this->writeString("//!<");
         this->writeSpace();
-        string extract = commentBlock->extractText(Definition::TrimExtract::kYes);
+        string extract = fBmhParser->extractText(commentBlock, BmhParser::TrimExtract::kYes);
         this->rewriteBlock(extract.length(), &extract.front(), Phrase::kNo);
     }
     this->lf(2);
@@ -833,7 +858,7 @@
         if (MarkType::kMember != memberDef->fMarkType) {
             continue;
         }
-        string extract = memberDef->extractText(Definition::TrimExtract::kYes);
+        string extract = fBmhParser->extractText(memberDef, BmhParser::TrimExtract::kYes);
         bool multiline = string::npos != extract.find('\n');
         if (multiline) {
             memberDef->fShort = false;
diff --git a/tools/bookmaker/mdOut.cpp b/tools/bookmaker/mdOut.cpp
index f74853b..ff1bff2 100644
--- a/tools/bookmaker/mdOut.cpp
+++ b/tools/bookmaker/mdOut.cpp
@@ -166,7 +166,9 @@
             // look for Sk / sk / SK ..
         if (!ref.compare(0, 2, "Sk") && ref != "Skew" && ref != "Skews" &&
               ref != "Skip" && ref != "Skips") {
-            t.reportError("missed Sk prefixed");
+            if (BmhParser::Resolvable::kOut != resolvable) {
+                t.reportError("missed Sk prefixed");
+            }
             return result;
         }
         if (!ref.compare(0, 2, "SK")) {
@@ -744,7 +746,9 @@
     const char* textStart = def->fContentStart;
     if (MarkType::kParam != def->fMarkType && MarkType::kConst != def->fMarkType &&
             (!def->fParent || MarkType::kConst != def->fParent->fMarkType) &&
-            TableState::kNone != fTableState) {
+            TableState::kNone != fTableState &&
+            (MarkType::kPhraseRef != def->fMarkType || !def->fParent ||
+            MarkType::kParam != def->fParent->fMarkType)) {
         this->writePending();
         FPRINTF("</table>");
         this->lf(2);
@@ -1088,6 +1092,22 @@
             break;
         case MarkType::kWidth:
             break;
+        case MarkType::kPhraseDef:
+            break;
+        case MarkType::kPhraseRef:
+            if (fBmhParser.fPhraseMap.end() == fBmhParser.fPhraseMap.find(def->fName)) {
+                def->reportError<void>("missing phrase definition");
+            } else {
+                if (fColumn && ' ' >= def->fStart[0]) {
+                    this->writeSpace();
+                }
+                Definition* phraseRef = fBmhParser.fPhraseMap.find(def->fName)->second;
+                this->childrenOut(phraseRef, phraseRef->fContentStart);
+                if (' ' >= def->fContentStart[0]) {
+                    this->writeSpace();
+                }
+            }
+            break;
         default:
             SkDebugf("fatal error: MarkType::k%s unhandled in %s()\n",
                     fBmhParser.fMaps[(int) def->fMarkType].fName, __func__);
@@ -1195,6 +1215,8 @@
         case MarkType::kTable:
             this->lf(2);
             break;
+        case MarkType::kPhraseDef:
+            break;
         case MarkType::kPrivate:
             break;
         default: