diff options
author | 2017-07-28 11:04:54 -0400 | |
---|---|---|
committer | 2017-07-28 15:30:38 +0000 | |
commit | 8032b983faaa8c76f81bf3cf028e9c64f4635478 (patch) | |
tree | ed3be061ff02a99dab1b3e443d48b7f5c906417e /tools/bookmaker/bookmaker.cpp | |
parent | acaa607328fb0dfac0894d4a2fcdead520e696b3 (diff) |
bookmaker initial checkin
bookmaker is a tool that generates documentation
backends from a canonical markup. Documentation for
bookmaker itself is evolving at docs/usingBookmaker.bmh,
which is visible online at skia.org/user/api/bmh_usingBookmaker
Change-Id: Ic76ddf29134895b5c2ebfbc84603e40ff08caf09
Reviewed-on: https://skia-review.googlesource.com/28000
Commit-Queue: Cary Clark <caryclark@google.com>
Reviewed-by: Cary Clark <caryclark@google.com>
Diffstat (limited to 'tools/bookmaker/bookmaker.cpp')
-rw-r--r-- | tools/bookmaker/bookmaker.cpp | 2198 |
1 files changed, 2198 insertions, 0 deletions
diff --git a/tools/bookmaker/bookmaker.cpp b/tools/bookmaker/bookmaker.cpp new file mode 100644 index 0000000000..3b67663000 --- /dev/null +++ b/tools/bookmaker/bookmaker.cpp @@ -0,0 +1,2198 @@ +/* + * Copyright 2017 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "bookmaker.h" + +#include "SkCommandLineFlags.h" +#include "SkOSFile.h" +#include "SkOSPath.h" + + +/* recipe for generating timestamps for existing doxygen comments +find include/core -type f -name '*.h' -print -exec git blame {} \; > ~/all.blame.txt + +space table better for Constants +should Return be on same line as 'Return Value'? +remove anonymous header, e.g. Enum SkPaint::::anonymous_2 +Text Encoding anchors in paragraph are echoed instead of being linked to anchor names + also should not point to 'undocumented' since they are resolvable links +#Member lost all formatting +inconsistent use of capitalization in #Param +#List needs '# content ##', formatting +consts like enum members need fully qualfied refs to make a valid link +enum comments should be disallowed unless after #Enum and before first #Const + ... or, should look for enum comments in other places + +// in includeWriter.cpp +lf preceding #A is ignored + +Text_Size should become SkPaint's text size if root is not Paint? +100 column limit done manually -- either error or rewrap + +SkPaint.bmh line 22: +Insert 'the' after 'regardless of' ? +somewhat intentional. Imagine SkPaint::kXXX is 'Joe'. Then it shouldn't read 'regardless +of the Joe setting.' To make that work as a proper pronoun, maybe it should read: +'regardless of SkPaint's kAntiAlias_Flag setting or 'regardless of SkPaint's anti-alias setting'. +It's the way it is so that SkPaint::kAntiAlias_Flag can be a link to the definition. +Its awkwardness is compounded because this description is technically outside of 'class SkPaint' +so a reference to kAntiAlias_Flag by itself doesn't know that it is defined inside SkPaint, +but that's a detail I could work around. + +SkPaint.bmh line 319, 400, 444 +more complications I haven't figured out. I don't know when or how to pluralize +references. This should be "objects' reference counts" probably, but then +I lose the link to SkRefCnt. + +SkPaint.bmh line 2074 +arcs at front of sentence not capitalized + +SkPaint.bmh line 2639 +I'd argue that 'fill path' is OK, in that is it the path that will fill, not the path +that has already been filled. I see the awkwardness though, and will add it to my bug list. + +check for function name in its own description + +multiple line #Param / #Return only copies first line? + +rework underlinethickness / strikeout thickness + +getTextIntercepts lost underline comment + */ + +static string normalized_name(string name) { + string normalizedName = name; + std::replace(normalizedName.begin(), normalizedName.end(), '-', '_'); + do { + size_t doubleColon = normalizedName.find("::", 0); + if (string::npos == doubleColon) { + break; + } + normalizedName = normalizedName.substr(0, doubleColon) + + '_' + normalizedName.substr(doubleColon + 2); + } while (true); + return normalizedName; +} + +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); +} + +// fixme: this will need to be more complicated to handle all of Skia +// for now, just handle paint -- maybe fiddle will loosen naming restrictions +void Definition::setCanonicalFiddle() { + fMethodType = Definition::MethodType::kNone; + size_t doubleColons = fName.find("::", 0); + SkASSERT(string::npos != doubleColons); + string result = fName.substr(0, doubleColons) + "_"; + doubleColons += 2; + if (string::npos != fName.find('~', doubleColons)) { + fMethodType = Definition::MethodType::kDestructor; + result += "destructor"; + } else { + bool isMove = string::npos != fName.find("&&", doubleColons); + const char operatorStr[] = "operator"; + size_t opPos = fName.find(operatorStr, doubleColons); + if (string::npos != opPos) { + fMethodType = Definition::MethodType::kOperator; + opPos += sizeof(operatorStr) - 1; + if ('!' == fName[opPos]) { + SkASSERT('=' == fName[opPos + 1]); + result += "not_equal_operator"; + } else if ('=' == fName[opPos]) { + if ('(' == fName[opPos + 1]) { + result += isMove ? "move_" : "copy_"; + result += "assignment_operator"; + } else { + SkASSERT('=' == fName[opPos + 1]); + result += "equal_operator"; + } + } else { + SkASSERT(0); // todo: incomplete + } + } else if (string::npos != fName.find("()", doubleColons)) { + if (isupper(fName[doubleColons])) { + fMethodType = Definition::MethodType::kConstructor; + result += "empty_constructor"; + } else { + result += fName.substr(doubleColons, fName.length() - doubleColons - 2); + } + } else { + size_t comma = fName.find(',', doubleColons); + size_t openParen = fName.find('(', doubleColons); + if (string::npos == comma && string::npos != openParen) { + fMethodType = Definition::MethodType::kConstructor; + result += isMove ? "move_" : "copy_"; + result += "constructor"; + } else if (string::npos == openParen) { + result += fName.substr(doubleColons); + } else { + fMethodType = Definition::MethodType::kConstructor; + // name them by their param types, e.g. SkCanvas__int_int_const_SkSurfaceProps_star + SkASSERT(string::npos != openParen); + // TODO: move forward until parens are balanced and terminator =,) + TextParser params("", &fName[openParen] + 1, &*fName.end(), 0); + bool underline = false; + while (!params.eof()) { +// SkDEBUGCODE(const char* end = params.anyOf("(),=")); // unused for now +// SkASSERT(end[0] != '('); // fixme: put off handling nested parentheseses + if (params.startsWith("const") || params.startsWith("int") + || params.startsWith("Sk")) { + const char* wordStart = params.fChar; + params.skipToNonAlphaNum(); + if (underline) { + result += '_'; + } else { + underline = true; + } + result += string(wordStart, params.fChar - wordStart); + } else { + params.skipToNonAlphaNum(); + } + if (!params.eof() && '*' == params.peek()) { + if (underline) { + result += '_'; + } else { + underline = true; + } + result += "star"; + params.next(); + params.skipSpace(); + } + params.skipToAlpha(); + } + } + } + } + fFiddle = normalized_name(result); +} + +bool Definition::exampleToScript(string* result) 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); + const char drawWrapper[] = "void draw(SkCanvas* canvas) {"; + const char drawNoCanvas[] = "void draw(SkCanvas* ) {"; + 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 textOut = string::npos != text.find("SkDebugf(") + || string::npos != text.find("dump(") + || string::npos != text.find("dumpHex("); + string heightStr = "256"; + string widthStr = "256"; + bool preprocessor = text[0] == '#'; + string normalizedName(fFiddle); + string code; + string imageStr = "0"; + for (auto const& iter : fChildren) { + switch (iter->fMarkType) { + case MarkType::kError: + result->clear(); + return true; + 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 - 1); + 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::kMarkChar: + case MarkType::kPlatform: + // ignore for now + break; + case MarkType::kStdOut: + textOut = true; + break; + default: + SkASSERT(0); // more coding to do + } + } + 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); + bool wrapCode = !hasFunc && !noCanvas && !preprocessor; + if (wrapCode) { + code += hasCanvas ? drawNoCanvas : drawWrapper; + code += "\\n"; + outIndent = 4; + } + add_code(text, pos, end, outIndent, textIndent, code); + if (wrapCode) { + code += "}"; + } + string example = "\"" + normalizedName + "\": {\n"; + example += " \"code\": \"" + code + "\",\n"; + example += " \"options\": {\n"; + example += " \"width\": " + widthStr + ",\n"; + example += " \"height\": " + heightStr + ",\n"; + example += " \"source\": " + imageStr + ",\n"; + example += " \"srgb\": false,\n"; + example += " \"f16\": false,\n"; + example += " \"textOnly\": " + textOutStr + ",\n"; + example += " \"animated\": false,\n"; + example += " \"duration\": 0\n"; + example += " },\n"; + example += " \"fast\": true\n"; + example += "}"; + *result = example; + return true; +} + +static void space_pad(string* str) { + size_t len = str->length(); + if (len == 0) { + return; + } + char last = (*str)[len - 1]; + if ('~' == last || ' ' >= last) { + return; + } + *str += ' '; +} + +//start here; +// see if it possible to abstract this a little bit so it can +// additionally be used to find params and return in method prototype that +// does not have corresponding doxygen comments +bool Definition::checkMethod() const { + SkASSERT(MarkType::kMethod == fMarkType); + // if method returns a value, look for a return child + // for each parameter, look for a corresponding child + const char* end = fContentStart; + while (end > fStart && ' ' >= end[-1]) { + --end; + } + TextParser methodParser(fFileName, fStart, end, fLineCount); + methodParser.skipWhiteSpace(); + SkASSERT(methodParser.startsWith("#Method")); + methodParser.skipName("#Method"); + methodParser.skipSpace(); + string name = this->methodName(); + if (MethodType::kNone == fMethodType && "()" == name.substr(name.length() - 2)) { + name = name.substr(0, name.length() - 2); + } + bool expectReturn = this->methodHasReturn(name, &methodParser); + bool foundReturn = false; + bool foundException = false; + for (auto& child : fChildren) { + foundException |= MarkType::kDeprecated == child->fMarkType + || MarkType::kExperimental == child->fMarkType; + if (MarkType::kReturn != child->fMarkType) { + if (MarkType::kParam == child->fMarkType) { + child->fVisited = false; + } + continue; + } + if (!expectReturn) { + return methodParser.reportError<bool>("no #Return expected"); + } + if (foundReturn) { + return methodParser.reportError<bool>("multiple #Return markers"); + } + foundReturn = true; + } + if (expectReturn && !foundReturn && !foundException) { + return methodParser.reportError<bool>("missing #Return marker"); + } + const char* paren = methodParser.strnchr('(', methodParser.fEnd); + if (!paren) { + return methodParser.reportError<bool>("missing #Method function definition"); + } + const char* nextEnd = paren; + do { + string paramName; + methodParser.fChar = nextEnd + 1; + methodParser.skipSpace(); + if (!this->nextMethodParam(&methodParser, &nextEnd, ¶mName)) { + continue; + } + bool foundParam = false; + for (auto& child : fChildren) { + if (MarkType::kParam != child->fMarkType) { + continue; + } + if (paramName != child->fName) { + continue; + } + if (child->fVisited) { + return methodParser.reportError<bool>("multiple #Method param with same name"); + } + child->fVisited = true; + if (foundParam) { + TextParser paramError(child); + return methodParser.reportError<bool>("multiple #Param with same name"); + } + foundParam = true; + + } + if (!foundParam && !foundException) { + return methodParser.reportError<bool>("no #Param found"); + } + if (')' == nextEnd[0]) { + break; + } + } while (')' != nextEnd[0]); + for (auto& child : fChildren) { + if (MarkType::kParam != child->fMarkType) { + continue; + } + if (!child->fVisited) { + TextParser paramError(child); + return paramError.reportError<bool>("#Param without param in #Method"); + } + } + return true; +} + +bool Definition::crossCheck(const char* tokenID, const Definition& includeToken) const { + const char* defStart = fStart; + SkASSERT('#' == defStart[0]); // FIXME: needs to be per definition + ++defStart; + SkASSERT(!strncmp(defStart, tokenID, strlen(tokenID))); + defStart += strlen(tokenID); + return crossCheckInside(defStart, fContentStart, includeToken); +} + +bool Definition::crossCheck(const Definition& includeToken) const { + return crossCheckInside(fContentStart, fContentEnd, includeToken); +} + +bool Definition::crossCheckInside(const char* start, const char* end, + const Definition& includeToken) const { + TextParser def(fFileName, start, end, fLineCount); + TextParser inc("", includeToken.fContentStart, includeToken.fContentEnd, 0); + if (inc.startsWith("SK_API")) { + inc.skipWord("SK_API"); + } + if (inc.startsWith("friend")) { + inc.skipWord("friend"); + } + do { + bool defEof; + bool incEof; + do { + defEof = def.eof() || !def.skipWhiteSpace(); + incEof = inc.eof() || !inc.skipWhiteSpace(); + if (!incEof && '/' == inc.peek() && (defEof || '/' != def.peek())) { + inc.next(); + if ('*' == inc.peek()) { + inc.skipToEndBracket("*/"); + inc.next(); + } else if ('/' == inc.peek()) { + inc.skipToEndBracket('\n'); + } + } else if (!incEof && '#' == inc.peek() && (defEof || '#' != def.peek())) { + inc.next(); + SkASSERT(inc.startsWith("if")); + inc.skipToEndBracket("#"); + SkASSERT(inc.startsWith("#endif")); + inc.skipToEndBracket("\n"); + } else { + break; + } + inc.next(); + } while (true); + if (defEof || incEof) { + return defEof == incEof || (!defEof && ';' == def.peek()); + } + char defCh; + do { + defCh = def.next(); + char incCh = inc.next(); + if (' ' >= defCh && ' ' >= incCh) { + break; + } + if (defCh != incCh) { + return false; + } + if (';' == defCh) { + return true; + } + } while (!def.eof() && !inc.eof()); + } while (true); + return false; +} + +string Definition::formatFunction() const { + const char* end = fContentStart; + while (end > fStart && ' ' >= end[-1]) { + --end; + } + TextParser methodParser(fFileName, fStart, end, fLineCount); + methodParser.skipWhiteSpace(); + SkASSERT(methodParser.startsWith("#Method")); + methodParser.skipName("#Method"); + methodParser.skipSpace(); + const char* lastStart = methodParser.fChar; + const int limit = 80; // todo: allow this to be set by caller or in global or something + string methodStr; + string name = this->methodName(); + const char* nameInParser = methodParser.strnstr(name.c_str(), methodParser.fEnd); + methodParser.skipTo(nameInParser); + const char* lastEnd = methodParser.fChar; + const char* paren = methodParser.strnchr('(', methodParser.fEnd); + size_t indent; + if (paren) { + indent = (size_t) (paren - lastStart) + 1; + } else { + indent = (size_t) (lastEnd - lastStart); + } + int written = 0; + do { + const char* nextStart = lastEnd; + SkASSERT(written < limit); + const char* delimiter = methodParser.anyOf(",)"); + const char* nextEnd = delimiter ? delimiter : methodParser.fEnd; + if (delimiter) { + while (nextStart < nextEnd && ' ' >= nextStart[0]) { + ++nextStart; + } + } + while (nextEnd > nextStart && ' ' >= nextEnd[-1]) { + --nextEnd; + } + if (delimiter) { + nextEnd += 1; + delimiter += 1; + } + if (lastEnd > lastStart) { + if (lastStart[0] != ' ') { + space_pad(&methodStr); + } + methodStr += string(lastStart, (size_t) (lastEnd - lastStart)); + written += (size_t) (lastEnd - lastStart); + } + if (delimiter) { + if (nextEnd - nextStart >= (ptrdiff_t) (limit - written)) { + written = indent; + methodStr += '\n'; + methodStr += string(indent, ' '); + } + methodParser.skipTo(delimiter); + } + lastStart = nextStart; + lastEnd = nextEnd; + } while (lastStart < lastEnd); + return methodStr; +} + +string Definition::fiddleName() const { + string result; + size_t start = 0; + string parent; + const Definition* parentDef = this; + while ((parentDef = parentDef->fParent)) { + if (MarkType::kClass == parentDef->fMarkType || MarkType::kStruct == parentDef->fMarkType) { + parent = parentDef->fFiddle; + break; + } + } + if (parent.length() && 0 == fFiddle.compare(0, parent.length(), parent)) { + start = parent.length(); + while (start < fFiddle.length() && '_' == fFiddle[start]) { + ++start; + } + } + size_t end = fFiddle.find_first_of('(', start); + return fFiddle.substr(start, end - start); +} + +const Definition* Definition::hasChild(MarkType markType) const { + for (auto iter : fChildren) { + if (markType == iter->fMarkType) { + return iter; + } + } + return nullptr; +} + +const Definition* Definition::hasParam(const string& ref) const { + SkASSERT(MarkType::kMethod == fMarkType); + for (auto iter : fChildren) { + if (MarkType::kParam != iter->fMarkType) { + continue; + } + if (iter->fName == ref) { + return &*iter; + } + + } + return nullptr; +} + +bool Definition::methodHasReturn(const string& name, TextParser* methodParser) const { + const char* lastStart = methodParser->fChar; + const char* nameInParser = methodParser->strnstr(name.c_str(), methodParser->fEnd); + methodParser->skipTo(nameInParser); + const char* lastEnd = methodParser->fChar; + const char* returnEnd = lastEnd; + while (returnEnd > lastStart && ' ' == returnEnd[-1]) { + --returnEnd; + } + bool expectReturn = 4 != returnEnd - lastStart || strncmp("void", lastStart, 4); + if (MethodType::kNone != fMethodType && !expectReturn) { + return methodParser->reportError<bool>("unexpected void"); + } + switch (fMethodType) { + case MethodType::kNone: + case MethodType::kOperator: + // either is fine + break; + case MethodType::kConstructor: + expectReturn = true; + break; + case MethodType::kDestructor: + expectReturn = false; + break; + } + return expectReturn; +} + +string Definition::methodName() const { + string result; + size_t start = 0; + string parent; + const Definition* parentDef = this; + while ((parentDef = parentDef->fParent)) { + if (MarkType::kClass == parentDef->fMarkType || MarkType::kStruct == parentDef->fMarkType) { + parent = parentDef->fName; + break; + } + } + if (parent.length() && 0 == fName.compare(0, parent.length(), parent)) { + start = parent.length(); + while (start < fName.length() && ':' == fName[start]) { + ++start; + } + } + if (fClone) { + int lastUnder = fName.rfind('_'); + return fName.substr(start, (size_t) (lastUnder - start)); + } + size_t end = fName.find_first_of('(', start); + if (string::npos == end) { + return fName.substr(start); + } + return fName.substr(start, end - start); +} + +bool Definition::nextMethodParam(TextParser* methodParser, const char** nextEndPtr, + string* paramName) const { + *nextEndPtr = methodParser->anyOf(",)"); + const char* nextEnd = *nextEndPtr; + if (!nextEnd) { + return methodParser->reportError<bool>("#Method function missing close paren"); + } + const char* paramEnd = nextEnd; + const char* assign = methodParser->strnstr(" = ", paramEnd); + if (assign) { + paramEnd = assign; + } + const char* closeBracket = methodParser->strnstr("]", paramEnd); + if (closeBracket) { + const char* openBracket = methodParser->strnstr("[", paramEnd); + if (openBracket && openBracket < closeBracket) { + while (openBracket < --closeBracket && isdigit(closeBracket[0])) + ; + if (openBracket == closeBracket) { + paramEnd = openBracket; + } + } + } + while (paramEnd > methodParser->fChar && ' ' == paramEnd[-1]) { + --paramEnd; + } + const char* paramStart = paramEnd; + while (paramStart > methodParser->fChar && isalnum(paramStart[-1])) { + --paramStart; + } + if (paramStart > methodParser->fChar && paramStart >= paramEnd) { + return methodParser->reportError<bool>("#Method missing param name"); + } + *paramName = string(paramStart, paramEnd - paramStart); + if (!paramName->length()) { + if (')' != nextEnd[0]) { + return methodParser->reportError<bool>("#Method malformed param"); + } + return false; + } + return true; +} + + bool ParserCommon::parseFile(const char* fileOrPath, const char* suffix) { + if (!sk_isdir(fileOrPath)) { + if (!this->parseFromFile(fileOrPath)) { + SkDebugf("failed to parse %s\n", fileOrPath); + return false; + } + } else { + SkOSFile::Iter it(fileOrPath, suffix); + for (SkString file; it.next(&file); ) { + SkString p = SkOSPath::Join(fileOrPath, file.c_str()); + const char* hunk = p.c_str(); + if (!SkStrEndsWith(hunk, suffix)) { + continue; + } + if (!this->parseFromFile(hunk)) { + SkDebugf("failed to parse %s\n", hunk); + return false; + } + } + } + return true; +} + +bool Definition::paramsMatch(const string& match, const string& name) const { + TextParser def(fFileName, fStart, fContentStart, fLineCount); + const char* dName = def.strnstr(name.c_str(), fContentStart); + if (!dName) { + return false; + } + def.skipTo(dName); + TextParser m(fFileName, &match.front(), &match.back() + 1, fLineCount); + const char* mName = m.strnstr(name.c_str(), m.fEnd); + if (!mName) { + return false; + } + m.skipTo(mName); + while (!def.eof() && ')' != def.peek() && !m.eof() && ')' != m.peek()) { + const char* ds = def.fChar; + const char* ms = m.fChar; + const char* de = def.anyOf(") \n"); + const char* me = m.anyOf(") \n"); + def.skipTo(de); + m.skipTo(me); + if (def.fChar - ds != m.fChar - ms) { + return false; + } + if (strncmp(ds, ms, (int) (def.fChar - ds))) { + return false; + } + def.skipWhiteSpace(); + m.skipWhiteSpace(); + } + return !def.eof() && ')' == def.peek() && !m.eof() && ')' == m.peek(); +} + +void RootDefinition::clearVisited() { + fVisited = false; + for (auto& leaf : fLeaves) { + leaf.second.fVisited = false; + } + for (auto& branch : fBranches) { + branch.second->clearVisited(); + } +} + +bool RootDefinition::dumpUnVisited() { + bool allStructElementsFound = true; + for (auto& leaf : fLeaves) { + if (!leaf.second.fVisited) { + // TODO: parse embedded struct in includeParser phase, then remove this condition + size_t firstColon = leaf.first.find("::"); + size_t lastColon = leaf.first.rfind("::"); + if (firstColon != lastColon) { // struct, two sets + allStructElementsFound = false; + continue; + } + SkDebugf("defined in bmh but missing in include: %s\n", leaf.first.c_str()); + } + } + for (auto& branch : fBranches) { + allStructElementsFound &= branch.second->dumpUnVisited(); + } + return allStructElementsFound; +} + +const Definition* RootDefinition::find(const string& ref) const { + const auto leafIter = fLeaves.find(ref); + if (leafIter != fLeaves.end()) { + return &leafIter->second; + } + const auto branchIter = fBranches.find(ref); + if (branchIter != fBranches.end()) { + const RootDefinition* rootDef = branchIter->second; + return rootDef; + } + const Definition* result = nullptr; + for (const auto& branch : fBranches) { + const RootDefinition* rootDef = branch.second; + result = rootDef->find(ref); + if (result) { + break; + } + } + return result; +} + +/* + class contains named struct, enum, enum-member, method, topic, subtopic + everything contained by class is uniquely named + contained names may be reused by other classes + method contains named parameters + parameters may be reused in other methods + */ + +bool BmhParser::addDefinition(const char* defStart, bool hasEnd, MarkType markType, + const vector<string>& typeNameBuilder) { + Definition* definition = nullptr; + switch (markType) { + case MarkType::kComment: + if (!this->skipToDefinitionEnd(markType)) { + return false; + } + return true; + // these types may be referred to by name + case MarkType::kClass: + case MarkType::kStruct: + case MarkType::kConst: + case MarkType::kEnum: + case MarkType::kEnumClass: + case MarkType::kMember: + case MarkType::kMethod: + case MarkType::kTypedef: { + if (!typeNameBuilder.size()) { + return this->reportError<bool>("unnamed markup"); + } + if (typeNameBuilder.size() > 1) { + return this->reportError<bool>("expected one name only"); + } + const string& name = typeNameBuilder[0]; + if (nullptr == fRoot) { + fRoot = this->findBmhObject(markType, name); + fRoot->fFileName = fFileName; + definition = fRoot; + } else { + if (nullptr == fParent) { + return this->reportError<bool>("expected parent"); + } + if (fParent == fRoot && hasEnd) { + RootDefinition* rootParent = fRoot->rootParent(); + if (rootParent) { + fRoot = rootParent; + } + definition = fParent; + } else { + if (!hasEnd && fRoot->find(name)) { + return this->reportError<bool>("duplicate symbol"); + } + if (MarkType::kStruct == markType || MarkType::kClass == markType) { + // if class or struct, build fRoot hierarchy + // and change isDefined to search all parents of fRoot + SkASSERT(!hasEnd); + RootDefinition* childRoot = new RootDefinition; + (fRoot->fBranches)[name] = childRoot; + childRoot->setRootParent(fRoot); + childRoot->fFileName = fFileName; + fRoot = childRoot; + definition = fRoot; + } else { + definition = &fRoot->fLeaves[name]; + } + } + } + if (hasEnd) { + Exemplary hasExample = Exemplary::kNo; + bool hasExcluder = false; + for (auto child : definition->fChildren) { + if (MarkType::kExample == child->fMarkType) { + hasExample = Exemplary::kYes; + } + hasExcluder |= MarkType::kPrivate == child->fMarkType + || MarkType::kDeprecated == child->fMarkType + || MarkType::kExperimental == child->fMarkType + || MarkType::kNoExample == child->fMarkType; + } + if (fMaps[(int) markType].fExemplary != hasExample + && fMaps[(int) markType].fExemplary != Exemplary::kOptional) { + if (string::npos == fFileName.find("undocumented") + && !hasExcluder) { + hasExample == Exemplary::kNo ? + this->reportWarning("missing example") : + this->reportWarning("unexpected example"); + } + + } + if (MarkType::kMethod == markType) { + if (fCheckMethods && !definition->checkMethod()) { + return false; + } + } + if (!this->popParentStack(definition)) { + return false; + } + } else { + definition->fStart = defStart; + this->skipSpace(); + definition->fFileName = fFileName; + definition->fContentStart = fChar; + definition->fLineCount = fLineCount; + definition->fClone = fCloned; + if (MarkType::kConst == markType) { + // todo: require that fChar points to def on same line as markup + // additionally add definition to class children if it is not already there + if (definition->fParent != fRoot) { +// fRoot->fChildren.push_back(definition); + } + } + definition->fName = name; + if (MarkType::kMethod == markType) { + if (string::npos != name.find(':', 0)) { + definition->setCanonicalFiddle(); + } else { + definition->fFiddle = name; + } + } else { + definition->fFiddle = normalized_name(name); + } + definition->fMarkType = markType; + this->setAsParent(definition); + } + } break; + case MarkType::kTopic: + case MarkType::kSubtopic: + SkASSERT(1 == typeNameBuilder.size()); + if (!hasEnd) { + if (!typeNameBuilder.size()) { + return this->reportError<bool>("unnamed topic"); + } + fTopics.emplace_front(markType, defStart, fLineCount, fParent); + RootDefinition* rootDefinition = &fTopics.front(); + definition = rootDefinition; + definition->fFileName = fFileName; + definition->fContentStart = fChar; + definition->fName = typeNameBuilder[0]; + Definition* parent = fParent; + while (parent && MarkType::kTopic != parent->fMarkType + && MarkType::kSubtopic != parent->fMarkType) { + parent = parent->fParent; + } + definition->fFiddle = parent ? parent->fFiddle + '_' : ""; + definition->fFiddle += normalized_name(typeNameBuilder[0]); + this->setAsParent(definition); + } + { + const string& fullTopic = hasEnd ? fParent->fFiddle : definition->fFiddle; + Definition* defPtr = fTopicMap[fullTopic]; + if (hasEnd) { + if (!definition) { + definition = defPtr; + } else if (definition != defPtr) { + return this->reportError<bool>("mismatched topic"); + } + } else { + if (nullptr != defPtr) { + return this->reportError<bool>("already declared topic"); + } + fTopicMap[fullTopic] = definition; + } + } + if (hasEnd) { + if (!this->popParentStack(definition)) { + return false; + } + } + break; + // these types are children of parents, but are not in named maps + case MarkType::kDefinedBy: { + string prefixed(fRoot->fName); + const char* start = fChar; + string name(start, this->trimmedBracketEnd(fMC, OneLine::kYes) - start); + prefixed += "::" + name; + this->skipToEndBracket(fMC); + const auto leafIter = fRoot->fLeaves.find(prefixed); + if (fRoot->fLeaves.end() != leafIter) { + this->reportError<bool>("DefinedBy already defined"); + } + definition = &fRoot->fLeaves[prefixed]; + definition->fParent = fParent; + definition->fStart = defStart; + definition->fContentStart = start; + definition->fName = name; + definition->fFiddle = normalized_name(name); + definition->fContentEnd = fChar; + this->skipToEndBracket('\n'); + definition->fTerminator = fChar; + definition->fMarkType = markType; + definition->fLineCount = fLineCount; + fParent->fChildren.push_back(definition); + } break; + case MarkType::kDescription: + case MarkType::kStdOut: + // may be one-liner + case MarkType::kBug: + case MarkType::kNoExample: + case MarkType::kParam: + case MarkType::kReturn: + case MarkType::kToDo: + if (hasEnd) { + if (markType == fParent->fMarkType) { + definition = fParent; + if (MarkType::kBug == markType || MarkType::kReturn == markType + || MarkType::kToDo == markType) { + this->skipNoName(); + } + if (!this->popParentStack(fParent)) { // if not one liner, pop + return false; + } + } else { + fMarkup.emplace_front(markType, defStart, fLineCount, fParent); + definition = &fMarkup.front(); + definition->fName = typeNameBuilder[0]; + definition->fFiddle = normalized_name(typeNameBuilder[0]); + definition->fContentStart = fChar; + definition->fContentEnd = this->trimmedBracketEnd(fMC, OneLine::kYes); + this->skipToEndBracket(fMC); + SkAssertResult(fMC == this->next()); + SkAssertResult(fMC == this->next()); + definition->fTerminator = fChar; + fParent->fChildren.push_back(definition); + } + break; + } + // not one-liners + case MarkType::kCode: + case MarkType::kDeprecated: + case MarkType::kExample: + case MarkType::kExperimental: + case MarkType::kFormula: + case MarkType::kFunction: + case MarkType::kLegend: + case MarkType::kList: + case MarkType::kPrivate: + case MarkType::kTable: + case MarkType::kTrack: + if (hasEnd) { + definition = fParent; + if (markType != fParent->fMarkType) { + return this->reportError<bool>("end element mismatch"); + } else if (!this->popParentStack(fParent)) { + return false; + } + if (MarkType::kExample == markType) { + if (definition->fChildren.size() == 0) { + TextParser emptyCheck(definition); + if (emptyCheck.eof() || !emptyCheck.skipWhiteSpace()) { + return this->reportError<bool>("missing example body"); + } + } + } + } else { + fMarkup.emplace_front(markType, defStart, fLineCount, fParent); + definition = &fMarkup.front(); + definition->fContentStart = fChar; + definition->fName = typeNameBuilder[0]; + definition->fFiddle = fParent->fFiddle; + char suffix = '\0'; + bool tryAgain; + do { + tryAgain = false; + for (const auto& child : fParent->fChildren) { + if (child->fFiddle == definition->fFiddle) { + if (MarkType::kExample != child->fMarkType) { + continue; + } + if ('\0' == suffix) { + suffix = 'a'; + } else if (++suffix > 'z') { + return reportError<bool>("too many examples"); + } + definition->fFiddle = fParent->fFiddle + '_'; + definition->fFiddle += suffix; + tryAgain = true; + break; + } + } + } while (tryAgain); + this->setAsParent(definition); + } + break; + // always treated as one-liners (can't detect misuse easily) + case MarkType::kAlias: + case MarkType::kAnchor: + case MarkType::kDefine: + case MarkType::kError: + case MarkType::kFile: + case MarkType::kHeight: + case MarkType::kImage: + case MarkType::kPlatform: + case MarkType::kSeeAlso: + case MarkType::kSubstitute: + case MarkType::kTime: + case MarkType::kVolatile: + case MarkType::kWidth: + if (hasEnd) { + return this->reportError<bool>("one liners omit end element"); + } + fMarkup.emplace_front(markType, defStart, fLineCount, fParent); + definition = &fMarkup.front(); + definition->fName = typeNameBuilder[0]; + definition->fFiddle = normalized_name(typeNameBuilder[0]); + definition->fContentStart = fChar; + definition->fContentEnd = this->trimmedBracketEnd('\n', OneLine::kYes); + definition->fTerminator = this->lineEnd() - 1; + fParent->fChildren.push_back(definition); + if (MarkType::kAnchor == markType) { + this->skipToEndBracket(fMC); + fMarkup.emplace_front(MarkType::kLink, fChar, fLineCount, definition); + SkAssertResult(fMC == this->next()); + this->skipWhiteSpace(); + Definition* link = &fMarkup.front(); + link->fContentStart = fChar; + link->fContentEnd = this->trimmedBracketEnd(fMC, OneLine::kYes); + this->skipToEndBracket(fMC); + SkAssertResult(fMC == this->next()); + SkAssertResult(fMC == this->next()); + link->fTerminator = fChar; + definition->fContentEnd = link->fContentEnd; + definition->fTerminator = fChar; + definition->fChildren.emplace_back(link); + } else if (MarkType::kAlias == markType) { + this->skipWhiteSpace(); + const char* start = fChar; + this->skipToNonAlphaNum(); + string alias(start, fChar - start); + if (fAliasMap.end() != fAliasMap.find(alias)) { + return this->reportError<bool>("duplicate alias"); + } + fAliasMap[alias] = definition; + } + break; + case MarkType::kExternal: + (void) this->collectExternals(); // FIXME: detect errors in external defs? + break; + default: + SkASSERT(0); // fixme : don't let any types be invisible + return true; + } + if (fParent) { + SkASSERT(definition); + SkASSERT(definition->fName.length() > 0); + } + return true; +} + +bool BmhParser::childOf(MarkType markType) const { + auto childError = [this](MarkType markType) -> bool { + string errStr = "expected "; + errStr += fMaps[(int) markType].fName; + errStr += " parent"; + return this->reportError<bool>(errStr.c_str()); + }; + + if (markType == fParent->fMarkType) { + return true; + } + if (this->hasEndToken()) { + if (!fParent->fParent) { + return this->reportError<bool>("expected grandparent"); + } + if (markType == fParent->fParent->fMarkType) { + return true; + } + } + return childError(markType); +} + +string BmhParser::className(MarkType markType) { + string builder; + const Definition* parent = this->parentSpace(); + if (parent && (parent != fParent || MarkType::kClass != markType)) { + builder += parent->fName; + } + const char* end = this->lineEnd(); + const char* mc = this->strnchr(fMC, end); + if (mc) { + this->skipSpace(); + const char* wordStart = fChar; + this->skipToNonAlphaNum(); + const char* wordEnd = fChar; + if (mc + 1 < fEnd && fMC == mc[1]) { // if ## + if (markType != fParent->fMarkType) { + return this->reportError<string>("unbalanced method"); + } + if (builder.length() > 0 && wordEnd > wordStart) { + if (builder != fParent->fName) { + builder += "::"; + builder += string(wordStart, wordEnd - wordStart); + if (builder != fParent->fName) { + return this->reportError<string>("name mismatch"); + } + } + } + this->skipLine(); + return fParent->fName; + } + fChar = mc; + this->next(); + } + this->skipWhiteSpace(); + if (MarkType::kEnum == markType && fChar >= end) { + fAnonymous = true; + builder += "::_anonymous"; + return uniqueRootName(builder, markType); + } + builder = this->word(builder, "::"); + return builder; +} + +bool BmhParser::collectExternals() { + do { + this->skipWhiteSpace(); + if (this->eof()) { + break; + } + if (fMC == this->peek()) { + this->next(); + if (this->eof()) { + break; + } + if (fMC == this->peek()) { + this->skipLine(); + break; + } + if (' ' >= this->peek()) { + this->skipLine(); + continue; + } + if (this->startsWith(fMaps[(int) MarkType::kExternal].fName)) { + this->skipToNonAlphaNum(); + continue; + } + } + this->skipToAlpha(); + const char* wordStart = fChar; + this->skipToNonAlphaNum(); + if (fChar - wordStart > 0) { + fExternals.emplace_front(MarkType::kExternal, wordStart, fChar, fLineCount, fParent); + RootDefinition* definition = &fExternals.front(); + definition->fFileName = fFileName; + definition->fName = string(wordStart ,fChar - wordStart); + definition->fFiddle = normalized_name(definition->fName); + } + } while (!this->eof()); + return true; +} + +int BmhParser::endHashCount() const { + const char* end = fLine + this->lineLength(); + int count = 0; + while (fLine < end && fMC == *--end) { + count++; + } + return count; +} + +// FIXME: some examples may produce different output on different platforms +// if the text output can be different, think of how to author that + +bool BmhParser::findDefinitions() { + bool lineStart = true; + fParent = nullptr; + while (!this->eof()) { + if (this->peek() == fMC) { + this->next(); + if (this->peek() == fMC) { + this->next(); + if (!lineStart && ' ' < this->peek()) { + return this->reportError<bool>("expected definition"); + } + if (this->peek() != fMC) { + vector<string> parentName; + parentName.push_back(fParent->fName); + if (!this->addDefinition(fChar - 1, true, fParent->fMarkType, parentName)) { + return false; + } + } else { + SkAssertResult(this->next() == fMC); + fMC = this->next(); // change markup character + if (' ' >= fMC) { + return this->reportError<bool>("illegal markup character"); + } + fMarkup.emplace_front(MarkType::kMarkChar, fChar - 1, fLineCount, fParent); + Definition* markChar = &fMarkup.front(); + markChar->fContentStart = fChar - 1; + this->skipToEndBracket('\n'); + markChar->fContentEnd = fChar; + markChar->fTerminator = fChar; + fParent->fChildren.push_back(markChar); + } + } else if (this->peek() >= 'A' && this->peek() <= 'Z') { + const char* defStart = fChar - 1; + MarkType markType = this->getMarkType(MarkLookup::kRequire); + bool hasEnd = this->hasEndToken(); + if (!hasEnd) { + MarkType parentType = fParent ? fParent->fMarkType : MarkType::kRoot; + uint64_t parentMask = fMaps[(int) markType].fParentMask; + if (parentMask && !(parentMask & (1LL << (int) parentType))) { + return this->reportError<bool>("invalid parent"); + } + } + if (!this->skipName(fMaps[(int) markType].fName)) { + return this->reportError<bool>("illegal markup character"); + } + if (!this->skipSpace()) { + return this->reportError<bool>("unexpected end"); + } + bool expectEnd = true; + vector<string> typeNameBuilder = this->typeName(markType, &expectEnd); + if (fCloned && MarkType::kMethod != markType && MarkType::kExample != markType + && !fAnonymous) { + return this->reportError<bool>("duplicate name"); + } + if (hasEnd && expectEnd) { + SkASSERT(fMC != this->peek()); + } + if (!this->addDefinition(defStart, hasEnd, markType, typeNameBuilder)) { + return false; + } + continue; + } else if (this->peek() == ' ') { + if (!fParent || (MarkType::kTable != fParent->fMarkType + && MarkType::kLegend != fParent->fMarkType + && MarkType::kList != fParent->fMarkType)) { + int endHashes = this->endHashCount(); + if (endHashes <= 1) { // one line comment + if (fParent) { + fMarkup.emplace_front(MarkType::kComment, fChar - 1, fLineCount, fParent); + Definition* comment = &fMarkup.front(); + comment->fContentStart = fChar - 1; + this->skipToEndBracket('\n'); + comment->fContentEnd = fChar; + comment->fTerminator = fChar; + fParent->fChildren.push_back(comment); + } else { + fChar = fLine + this->lineLength() - 1; + } + } else { // table row + if (2 != endHashes) { + string errorStr = "expect "; + errorStr += fMC; + errorStr += fMC; + return this->reportError<bool>(errorStr.c_str()); + } + if (!fParent || MarkType::kTable != fParent->fMarkType) { + return this->reportError<bool>("missing table"); + } + } + } else { + bool parentIsList = MarkType::kList == fParent->fMarkType; + // fixme? no nested tables for now + const char* colStart = fChar - 1; + fMarkup.emplace_front(MarkType::kRow, colStart, fLineCount, fParent); + Definition* row = &fMarkup.front(); + this->skipWhiteSpace(); + row->fContentStart = fChar; + this->setAsParent(row); + const char* lineEnd = this->lineEnd(); + do { + fMarkup.emplace_front(MarkType::kColumn, colStart, fLineCount, fParent); + Definition* column = &fMarkup.front(); + column->fContentStart = fChar; + column->fContentEnd = this->trimmedBracketEnd(fMC, + parentIsList ? OneLine::kNo : OneLine::kYes); + this->skipToEndBracket(fMC); + colStart = fChar; + SkAssertResult(fMC == this->next()); + if (fMC == this->peek()) { + this->next(); + } + column->fTerminator = fChar; + fParent->fChildren.push_back(column); + this->skipSpace(); + } while (fChar < lineEnd && '\n' != this->peek()); + if (!this->popParentStack(fParent)) { + return false; + } + const Definition* lastCol = row->fChildren.back(); + row->fContentEnd = lastCol->fContentEnd; + } + } + } + lineStart = this->next() == '\n'; + } + if (fParent) { + return this->reportError<bool>("mismatched end"); + } + return true; +} + +MarkType BmhParser::getMarkType(MarkLookup lookup) const { + for (int index = 0; index <= Last_MarkType; ++index) { + int typeLen = strlen(fMaps[index].fName); + if (typeLen == 0) { + continue; + } + if (fChar + typeLen >= fEnd || fChar[typeLen] > ' ') { + continue; + } + int chCompare = strncmp(fChar, fMaps[index].fName, typeLen); + if (chCompare < 0) { + goto fail; + } + if (chCompare == 0) { + return (MarkType) index; + } + } +fail: + if (MarkLookup::kRequire == lookup) { + return this->reportError<MarkType>("unknown mark type"); + } + return MarkType::kNone; +} + +bool HackParser::hackFiles() { + string filename(fFileName); + size_t len = filename.length() - 1; + while (len > 0 && (isalnum(filename[len]) || '_' == filename[len] || '.' == filename[len])) { + --len; + } + filename = filename.substr(len + 1); + // remove trailing period from #Param and #Return + FILE* out = fopen(filename.c_str(), "wb"); + if (!out) { + SkDebugf("could not open output file %s\n", filename.c_str()); + return false; + } + const char* start = fStart; + do { + const char* match = this->strnchr('#', fEnd); + if (!match) { + break; + } + this->skipTo(match); + this->next(); + if (!this->startsWith("Param") && !this->startsWith("Return")) { + continue; + } + const char* end = this->strnstr("##", fEnd); + while (true) { + TextParser::Save lastPeriod(this); + this->next(); + if (!this->skipToEndBracket('.', end)) { + lastPeriod.restore(); + break; + } + } + if ('.' == this->peek()) { + fprintf(out, "%.*s", (int) (fChar - start), start); + this->next(); + start = fChar; + } + } while (!this->eof()); + fprintf(out, "%.*s", (int) (fEnd - start), start); + fclose(out); + return true; +} + +bool BmhParser::hasEndToken() const { + const char* last = fLine + this->lineLength(); + while (last > fLine && ' ' >= *--last) + ; + if (--last < fLine) { + return false; + } + return last[0] == fMC && last[1] == fMC; +} + +string BmhParser::memberName() { + const char* wordStart; + const char* prefixes[] = { "static", "const" }; + do { + this->skipSpace(); + wordStart = fChar; + this->skipToNonAlphaNum(); + } while (this->anyOf(wordStart, prefixes, SK_ARRAY_COUNT(prefixes))); + if ('*' == this->peek()) { + this->next(); + } + return this->className(MarkType::kMember); +} + +string BmhParser::methodName() { + if (this->hasEndToken()) { + if (!fParent || !fParent->fName.length()) { + return this->reportError<string>("missing parent method name"); + } + SkASSERT(fMC == this->peek()); + this->next(); + SkASSERT(fMC == this->peek()); + this->next(); + SkASSERT(fMC != this->peek()); + return fParent->fName; + } + string builder; + const char* end = this->lineEnd(); + const char* paren = this->strnchr('(', end); + if (!paren) { + return this->reportError<string>("missing method name and reference"); + } + const char* nameStart = paren; + char ch; + bool expectOperator = false; + bool isConstructor = false; + const char* nameEnd = nullptr; + while (nameStart > fChar && ' ' != (ch = *--nameStart)) { + if (!isalnum(ch) && '_' != ch) { + if (nameEnd) { + break; + } + expectOperator = true; + continue; + } + if (!nameEnd) { + nameEnd = nameStart + 1; + } + } + if (!nameEnd) { + return this->reportError<string>("unexpected method name char"); + } + if (' ' == nameStart[0]) { + ++nameStart; + } + if (nameEnd <= nameStart) { + return this->reportError<string>("missing method name"); + } + if (nameStart >= paren) { + return this->reportError<string>("missing method name length"); + } + string name(nameStart, nameEnd - nameStart); + bool allLower = true; + for (int index = 0; index < (int) (nameEnd - nameStart); ++index) { + if (!islower(nameStart[index])) { + allLower = false; + break; + } + } + if (expectOperator && "operator" != name) { + return this->reportError<string>("expected operator"); + } + const Definition* parent = this->parentSpace(); + if (parent && parent->fName.length() > 0) { + if (parent->fName == name) { + isConstructor = true; + } else if ('~' == name[0]) { + if (parent->fName != name.substr(1)) { + return this->reportError<string>("expected destructor"); + } + isConstructor = true; + } + builder = parent->fName + "::"; + } + if (isConstructor || expectOperator) { + paren = this->strnchr(')', end) + 1; + } + builder.append(nameStart, paren - nameStart); + if (!expectOperator && allLower) { + builder.append("()"); + } + int parens = 0; + while (fChar < end || parens > 0) { + if ('(' == this->peek()) { + ++parens; + } else if (')' == this->peek()) { + --parens; + } + this->next(); + } + TextParser::Save saveState(this); + this->skipWhiteSpace(); + if (this->startsWith("const")) { + this->skipName("const"); + } else { + saveState.restore(); + } +// this->next(); + return uniqueRootName(builder, MarkType::kMethod); +} + +const Definition* BmhParser::parentSpace() const { + Definition* parent = nullptr; + Definition* test = fParent; + while (test) { + if (MarkType::kClass == test->fMarkType || + MarkType::kEnumClass == test->fMarkType || + MarkType::kStruct == test->fMarkType) { + parent = test; + break; + } + test = test->fParent; + } + return parent; +} + +bool BmhParser::popParentStack(Definition* definition) { + if (!fParent) { + return this->reportError<bool>("missing parent"); + } + if (definition != fParent) { + return this->reportError<bool>("definition end is not parent"); + } + if (!definition->fStart) { + return this->reportError<bool>("definition missing start"); + } + if (definition->fContentEnd) { + return this->reportError<bool>("definition already ended"); + } + definition->fContentEnd = fLine - 1; + definition->fTerminator = fChar; + fParent = definition->fParent; + if (!fParent || (MarkType::kTopic == fParent->fMarkType && !fParent->fParent)) { + fRoot = nullptr; + } + return true; +} + +TextParser::TextParser(const Definition* definition) : + TextParser(definition->fFileName, definition->fContentStart, definition->fContentEnd, + definition->fLineCount) { +} + +void TextParser::reportError(const char* errorStr) const { + this->reportWarning(errorStr); + SkDebugf(""); // convenient place to set a breakpoint +} + +void TextParser::reportWarning(const char* errorStr) const { + TextParser err(fFileName, fLine, fEnd, fLineCount); + size_t lineLen = this->lineLength(); + ptrdiff_t spaces = fChar - fLine; + while (spaces > 0 && (size_t) spaces > lineLen) { + ++err.fLineCount; + err.fLine += lineLen; + spaces -= lineLen; + lineLen = err.lineLength(); + } + SkDebugf("%s(%zd): error: %s\n", fFileName.c_str(), err.fLineCount, errorStr); + if (0 == lineLen) { + SkDebugf("[blank line]\n"); + } else { + while (lineLen > 0 && '\n' == err.fLine[lineLen - 1]) { + --lineLen; + } + SkDebugf("%.*s\n", (int) lineLen, err.fLine); + SkDebugf("%*s^\n", (int) spaces, ""); + } +} + +bool BmhParser::skipNoName() { + if ('\n' == this->peek()) { + this->next(); + return true; + } + this->skipWhiteSpace(); + if (fMC != this->peek()) { + return this->reportError<bool>("expected end mark"); + } + this->next(); + if (fMC != this->peek()) { + return this->reportError<bool>("expected end mark"); + } + this->next(); + return true; +} + +bool BmhParser::skipToDefinitionEnd(MarkType markType) { + if (this->eof()) { + return this->reportError<bool>("missing end"); + } + const char* start = fLine; + int startLineCount = fLineCount; + int stack = 1; + ptrdiff_t lineLen; + bool foundEnd = false; + do { + lineLen = this->lineLength(); + if (fMC != *fChar++) { + continue; + } + if (fMC == *fChar) { + continue; + } + if (' ' == *fChar) { + continue; + } + MarkType nextType = this->getMarkType(MarkLookup::kAllowUnknown); + if (markType != nextType) { + continue; + } + bool hasEnd = this->hasEndToken(); + if (hasEnd) { + if (!--stack) { + foundEnd = true; + continue; + } + } else { + ++stack; + } + } while ((void) ++fLineCount, (void) (fLine += lineLen), (void) (fChar = fLine), + !this->eof() && !foundEnd); + if (foundEnd) { + return true; + } + fLineCount = startLineCount; + fLine = start; + fChar = start; + return this->reportError<bool>("unbalanced stack"); +} + +vector<string> BmhParser::topicName() { + vector<string> result; + this->skipWhiteSpace(); + const char* lineEnd = fLine + this->lineLength(); + const char* nameStart = fChar; + while (fChar < lineEnd) { + char ch = this->next(); + SkASSERT(',' != ch); + if ('\n' == ch) { + break; + } + if (fMC == ch) { + break; + } + } + if (fChar - 1 > nameStart) { + string builder(nameStart, fChar - nameStart - 1); + trim_start_end(builder); + result.push_back(builder); + } + if (fChar < lineEnd && fMC == this->peek()) { + this->next(); + } + return result; +} + +// typeName parsing rules depend on mark type +vector<string> BmhParser::typeName(MarkType markType, bool* checkEnd) { + fAnonymous = false; + fCloned = false; + vector<string> result; + string builder; + if (fParent) { + builder = fParent->fName; + } + switch (markType) { + case MarkType::kEnum: + // enums may be nameless + case MarkType::kConst: + case MarkType::kEnumClass: + case MarkType::kClass: + case MarkType::kStruct: + case MarkType::kTypedef: + // expect name + builder = this->className(markType); + break; + case MarkType::kExample: + // check to see if one already exists -- if so, number this one + builder = this->uniqueName(string(), markType); + this->skipNoName(); + break; + case MarkType::kCode: + case MarkType::kDeprecated: + case MarkType::kDescription: + case MarkType::kDoxygen: + case MarkType::kExperimental: + case MarkType::kExternal: + case MarkType::kFormula: + case MarkType::kFunction: + case MarkType::kLegend: + case MarkType::kList: + case MarkType::kNoExample: + case MarkType::kPrivate: + case MarkType::kTrack: + this->skipNoName(); + break; + case MarkType::kAlias: + case MarkType::kAnchor: + case MarkType::kBug: // fixme: expect number + case MarkType::kDefine: + case MarkType::kDefinedBy: + case MarkType::kError: + case MarkType::kFile: + case MarkType::kHeight: + case MarkType::kImage: + case MarkType::kPlatform: + case MarkType::kReturn: + case MarkType::kSeeAlso: + case MarkType::kSubstitute: + case MarkType::kTime: + case MarkType::kToDo: + case MarkType::kVolatile: + case MarkType::kWidth: + *checkEnd = false; // no name, may have text body + break; + case MarkType::kStdOut: + this->skipNoName(); + break; // unnamed + case MarkType::kMember: + builder = this->memberName(); + break; + case MarkType::kMethod: + builder = this->methodName(); + break; + case MarkType::kParam: + // fixme: expect camelCase + builder = this->word("", ""); + this->skipSpace(); + *checkEnd = false; + break; + case MarkType::kTable: + this->skipNoName(); + break; // unnamed + case MarkType::kSubtopic: + case MarkType::kTopic: + // fixme: start with cap, allow space, hyphen, stop on comma + // one topic can have multiple type names delineated by comma + result = this->topicName(); + if (result.size() == 0 && this->hasEndToken()) { + break; + } + return result; + default: + // fixme: don't allow silent failures + SkASSERT(0); + } + result.push_back(builder); + return result; +} + +string BmhParser::uniqueName(const string& base, MarkType markType) { + string builder(base); + if (!builder.length()) { + builder = fParent->fName; + } + if (!fParent) { + return builder; + } + int number = 2; + string numBuilder(builder); + do { + for (const auto& iter : fParent->fChildren) { + if (markType == iter->fMarkType) { + if (iter->fName == numBuilder) { + if (MarkType::kMethod == markType) { + SkDebugf(""); + } + fCloned = true; + numBuilder = builder + '_' + to_string(number); + goto tryNext; + } + } + } + break; +tryNext: ; + } while (++number); + return numBuilder; +} + +string BmhParser::uniqueRootName(const string& base, MarkType markType) { + auto checkName = [markType](const Definition& def, const string& numBuilder) -> bool { + return markType == def.fMarkType && def.fName == numBuilder; + }; + + string builder(base); + if (!builder.length()) { + builder = fParent->fName; + } + int number = 2; + string numBuilder(builder); + Definition* cloned = nullptr; + do { + if (fRoot) { + for (auto& iter : fRoot->fBranches) { + if (checkName(*iter.second, numBuilder)) { + cloned = iter.second; + goto tryNext; + } + } + for (auto& iter : fRoot->fLeaves) { + if (checkName(iter.second, numBuilder)) { + cloned = &iter.second; + goto tryNext; + } + } + } else if (fParent) { + for (auto& iter : fParent->fChildren) { + if (checkName(*iter, numBuilder)) { + cloned = &*iter; + goto tryNext; + } + } + } + break; +tryNext: ; + if ("()" == builder.substr(builder.length() - 2)) { + builder = builder.substr(0, builder.length() - 2); + } + if (MarkType::kMethod == markType) { + cloned->fCloned = true; + } + fCloned = true; + numBuilder = builder + '_' + to_string(number); + } while (++number); + return numBuilder; +} + +void BmhParser::validate() const { + for (int index = 0; index <= (int) Last_MarkType; ++index) { + SkASSERT(fMaps[index].fMarkType == (MarkType) index); + } + const char* last = ""; + for (int index = 0; index <= (int) Last_MarkType; ++index) { + const char* next = fMaps[index].fName; + if (!last[0]) { + last = next; + continue; + } + if (!next[0]) { + continue; + } + SkASSERT(strcmp(last, next) < 0); + last = next; + } +} + +string BmhParser::word(const string& prefix, const string& delimiter) { + string builder(prefix); + this->skipWhiteSpace(); + const char* lineEnd = fLine + this->lineLength(); + const char* nameStart = fChar; + while (fChar < lineEnd) { + char ch = this->next(); + if (' ' >= ch) { + break; + } + if (',' == ch) { + return this->reportError<string>("no comma needed"); + break; + } + if (fMC == ch) { + return builder; + } + if (!isalnum(ch) && '_' != ch && ':' != ch && '-' != ch) { + return this->reportError<string>("unexpected char"); + } + if (':' == ch) { + // expect pair, and expect word to start with Sk + if (nameStart[0] != 'S' || nameStart[1] != 'k') { + return this->reportError<string>("expected Sk"); + } + if (':' != this->peek()) { + return this->reportError<string>("expected ::"); + } + this->next(); + } else if ('-' == ch) { + // expect word not to start with Sk or kX where X is A-Z + if (nameStart[0] == 'k' && nameStart[1] >= 'A' && nameStart[1] <= 'Z') { + return this->reportError<string>("didn't expected kX"); + } + if (nameStart[0] == 'S' && nameStart[1] == 'k') { + return this->reportError<string>("expected Sk"); + } + } + } + if (prefix.size()) { + builder += delimiter; + } + builder.append(nameStart, fChar - nameStart - 1); + return builder; +} + +// pass one: parse text, collect definitions +// pass two: lookup references + +DEFINE_string2(bmh, b, "", "A path to a *.bmh file or a directory."); +DEFINE_string2(examples, e, "", "File of fiddlecli input, usually fiddle.json (For now, disables -r -f -s)"); +DEFINE_string2(fiddle, f, "fiddleout.json", "File of fiddlecli output."); +DEFINE_string2(include, i, "", "A path to a *.h file or a directory."); +DEFINE_bool2(hack, k, false, "Do a find/replace hack to update all *.bmh files. (Requires -b)"); +DEFINE_bool2(populate, p, false, "Populate include from bmh. (Requires -b -i)"); +DEFINE_string2(ref, r, "", "Resolve refs and write bmh_*.md files to path. (Requires -b)"); +DEFINE_bool2(spellcheck, s, false, "Spell-check. (Requires -b)"); +DEFINE_bool2(tokens, t, false, "Output include tokens. (Requires -i)"); +DEFINE_bool2(crosscheck, x, false, "Check bmh against includes. (Requires -b -i)"); + +static bool dump_examples(FILE* fiddleOut, const Definition& def, bool* continuation) { + if (MarkType::kExample == def.fMarkType) { + string result; + if (!def.exampleToScript(&result)) { + return false; + } + if (result.length() > 0) { + if (*continuation) { + fprintf(fiddleOut, ",\n"); + } else { + *continuation = true; + } + fprintf(fiddleOut, "%s", result.c_str()); + } + return true; + } + for (auto& child : def.fChildren ) { + if (!dump_examples(fiddleOut, *child, continuation)) { + return false; + } + } + return true; +} + +static int count_children(const Definition& def, MarkType markType) { + int count = 0; + if (markType == def.fMarkType) { + ++count; + } + for (auto& child : def.fChildren ) { + count += count_children(*child, markType); + } + return count; +} + +int main(int argc, char** const argv) { + BmhParser bmhParser; + bmhParser.validate(); + + SkCommandLineFlags::SetUsage( + "Common Usage: bookmaker -i path/to/include.h -t\n" + " bookmaker -b path/to/bmh_files -e fiddle.json\n" + " ~/go/bin/fiddlecli --input fiddle.json --output fiddleout.json\n" + " bookmaker -b path/to/bmh_files -f fiddleout.json -r path/to/md_files\n" + " bookmaker -b path/to/bmh_files -i path/to/include.h -x\n" + " bookmaker -b path/to/bmh_files -i path/to/include.h -p\n"); + bool help = false; + for (int i = 1; i < argc; i++) { + if (0 == strcmp("-h", argv[i]) || 0 == strcmp("--help", argv[i])) { + help = true; + for (int j = i + 1; j < argc; j++) { + if (SkStrStartsWith(argv[j], '-')) { + break; + } + help = false; + } + break; + } + } + if (!help) { + SkCommandLineFlags::Parse(argc, argv); + } else { + SkCommandLineFlags::PrintUsage(); + const char* commands[] = { "", "-h", "bmh", "-h", "examples", "-h", "include", "-h", "fiddle", + "-h", "ref", "-h", "tokens", + "-h", "crosscheck", "-h", "populate", "-h", "spellcheck" }; + SkCommandLineFlags::Parse(SK_ARRAY_COUNT(commands), (char**) commands); + return 0; + } + if (FLAGS_bmh.isEmpty() && FLAGS_include.isEmpty()) { + SkDebugf("requires -b or -i\n"); + SkCommandLineFlags::PrintUsage(); + return 1; + } + if (FLAGS_bmh.isEmpty() && !FLAGS_examples.isEmpty()) { + SkDebugf("-e requires -b\n"); + SkCommandLineFlags::PrintUsage(); + return 1; + } + if (FLAGS_hack) { + if (FLAGS_bmh.isEmpty()) { + SkDebugf("-k or --hack requires -b\n"); + SkCommandLineFlags::PrintUsage(); + return 1; + } + HackParser hacker; + if (!hacker.parseFile(FLAGS_bmh[0], ".bmh")) { + SkDebugf("hack failed\n"); + return -1; + } + SkDebugf("hack success\n"); + return 0; + } + if ((FLAGS_include.isEmpty() || FLAGS_bmh.isEmpty()) && FLAGS_populate) { + SkDebugf("-r requires -b -i\n"); + SkCommandLineFlags::PrintUsage(); + return 1; + } + if (FLAGS_bmh.isEmpty() && !FLAGS_ref.isEmpty()) { + SkDebugf("-r requires -b\n"); + SkCommandLineFlags::PrintUsage(); + return 1; + } + if (FLAGS_bmh.isEmpty() && FLAGS_spellcheck) { + SkDebugf("-s requires -b\n"); + SkCommandLineFlags::PrintUsage(); + return 1; + } + if (FLAGS_include.isEmpty() && FLAGS_tokens) { + SkDebugf("-t requires -i\n"); + SkCommandLineFlags::PrintUsage(); + return 1; + } + if ((FLAGS_include.isEmpty() || FLAGS_bmh.isEmpty()) && FLAGS_crosscheck) { + SkDebugf("-x requires -b -i\n"); + SkCommandLineFlags::PrintUsage(); + return 1; + } + if (!FLAGS_bmh.isEmpty()) { + if (!bmhParser.parseFile(FLAGS_bmh[0], ".bmh")) { + return -1; + } + } + bool done = false; + if (!FLAGS_include.isEmpty()) { + if (FLAGS_tokens || FLAGS_crosscheck) { + IncludeParser includeParser; + includeParser.validate(); + if (!includeParser.parseFile(FLAGS_include[0], ".h")) { + return -1; + } + if (FLAGS_tokens) { + includeParser.dumpTokens(); + done = true; + } else if (FLAGS_crosscheck) { + if (!includeParser.crossCheck(bmhParser)) { + return -1; + } + done = true; + } + } else if (FLAGS_populate) { + IncludeWriter includeWriter; + includeWriter.validate(); + if (!includeWriter.parseFile(FLAGS_include[0], ".h")) { + return -1; + } + if (!includeWriter.populate(bmhParser)) { + return -1; + } + done = true; + } + } + FiddleParser fparser(&bmhParser); + if (!done && !FLAGS_fiddle.isEmpty() && FLAGS_examples.isEmpty()) { + if (!fparser.parseFile(FLAGS_fiddle[0], ".txt")) { + return -1; + } + } + if (!done && !FLAGS_ref.isEmpty() && FLAGS_examples.isEmpty()) { + MdOut mdOut(bmhParser); + mdOut.buildReferences(FLAGS_bmh[0], FLAGS_ref[0]); + } + if (!done && FLAGS_spellcheck && FLAGS_examples.isEmpty()) { + bmhParser.spellCheck(FLAGS_bmh[0]); + done = true; + } + int examples = 0; + int methods = 0; + int topics = 0; + FILE* fiddleOut; + if (!done && !FLAGS_examples.isEmpty()) { + fiddleOut = fopen(FLAGS_examples[0], "wb"); + if (!fiddleOut) { + SkDebugf("could not open output file %s\n", FLAGS_examples[0]); + return -1; + } + fprintf(fiddleOut, "{\n"); + bool continuation = false; + for (const auto& topic : bmhParser.fTopicMap) { + if (topic.second->fParent) { + continue; + } + dump_examples(fiddleOut, *topic.second, &continuation); + } + fprintf(fiddleOut, "\n}\n"); + fclose(fiddleOut); + } + for (const auto& topic : bmhParser.fTopicMap) { + if (topic.second->fParent) { + continue; + } + examples += count_children(*topic.second, MarkType::kExample); + methods += count_children(*topic.second, MarkType::kMethod); + topics += count_children(*topic.second, MarkType::kSubtopic); + topics += count_children(*topic.second, MarkType::kTopic); + } + SkDebugf("topics=%d classes=%d methods=%d examples=%d\n", + bmhParser.fTopicMap.size(), bmhParser.fClassMap.size(), + methods, examples); + return 0; +} |