8032b983fa
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>
456 lines
13 KiB
C++
456 lines
13 KiB
C++
/*
|
|
* 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 "SkOSFile.h"
|
|
#include "SkOSPath.h"
|
|
|
|
/*
|
|
things to do
|
|
if cap word is beginning of sentence, add it to table as lower-case
|
|
word must have only a single initial capital
|
|
|
|
if word is camel cased, look for :: matches on suffix
|
|
|
|
when function crosses lines, whole thing isn't seen as a 'word' e.g., search for largeArc in path
|
|
|
|
words in external not seen
|
|
*/
|
|
struct CheckEntry {
|
|
string fFile;
|
|
int fLine;
|
|
int fCount;
|
|
};
|
|
|
|
class SpellCheck : public ParserCommon {
|
|
public:
|
|
SpellCheck(const BmhParser& bmh) : ParserCommon()
|
|
, fBmhParser(bmh) {
|
|
this->reset();
|
|
}
|
|
bool check(const char* match);
|
|
void report();
|
|
private:
|
|
enum class TableState {
|
|
kNone,
|
|
kRow,
|
|
kColumn,
|
|
};
|
|
|
|
bool check(Definition* );
|
|
bool checkable(MarkType markType);
|
|
void childCheck(const Definition* def, const char* start);
|
|
void leafCheck(const char* start, const char* end);
|
|
bool parseFromFile(const char* path) override { return true; }
|
|
void printCheck(const string& str);
|
|
|
|
void reset() override {
|
|
INHERITED::resetCommon();
|
|
fMethod = nullptr;
|
|
fRoot = nullptr;
|
|
fTableState = TableState::kNone;
|
|
fInCode = false;
|
|
fInConst = false;
|
|
fInDescription = false;
|
|
fInStdOut = false;
|
|
}
|
|
|
|
void wordCheck(const string& str);
|
|
void wordCheck(ptrdiff_t len, const char* ch);
|
|
|
|
unordered_map<string, CheckEntry> fCode;
|
|
unordered_map<string, CheckEntry> fColons;
|
|
unordered_map<string, CheckEntry> fDigits;
|
|
unordered_map<string, CheckEntry> fDots;
|
|
unordered_map<string, CheckEntry> fParens; // also hold destructors, operators
|
|
unordered_map<string, CheckEntry> fUnderscores;
|
|
unordered_map<string, CheckEntry> fWords;
|
|
const BmhParser& fBmhParser;
|
|
Definition* fMethod;
|
|
RootDefinition* fRoot;
|
|
TableState fTableState;
|
|
bool fInCode;
|
|
bool fInConst;
|
|
bool fInDescription;
|
|
bool fInStdOut;
|
|
typedef ParserCommon INHERITED;
|
|
};
|
|
|
|
/* This doesn't perform a traditional spell or grammar check, although
|
|
maybe it should. Instead it looks for words used uncommonly and lower
|
|
case words that match capitalized words that are not sentence starters.
|
|
It also looks for articles preceeding capitalized words and their
|
|
modifiers to try to maintain a consistent voice.
|
|
Maybe also look for passive verbs (e.g. 'is') and suggest active ones?
|
|
*/
|
|
void BmhParser::spellCheck(const char* match) const {
|
|
SpellCheck checker(*this);
|
|
checker.check(match);
|
|
checker.report();
|
|
}
|
|
|
|
bool SpellCheck::check(const char* match) {
|
|
for (const auto& topic : fBmhParser.fTopicMap) {
|
|
Definition* topicDef = topic.second;
|
|
if (topicDef->fParent) {
|
|
continue;
|
|
}
|
|
if (!topicDef->isRoot()) {
|
|
return this->reportError<bool>("expected root topic");
|
|
}
|
|
fRoot = topicDef->asRoot();
|
|
if (string::npos == fRoot->fFileName.rfind(match)) {
|
|
continue;
|
|
}
|
|
this->check(topicDef);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool SpellCheck::check(Definition* def) {
|
|
fFileName = def->fFileName;
|
|
fLineCount = def->fLineCount;
|
|
string printable = def->printableName();
|
|
const char* textStart = def->fContentStart;
|
|
if (MarkType::kParam != def->fMarkType && MarkType::kConst != def->fMarkType &&
|
|
TableState::kNone != fTableState) {
|
|
fTableState = TableState::kNone;
|
|
}
|
|
switch (def->fMarkType) {
|
|
case MarkType::kAlias:
|
|
break;
|
|
case MarkType::kAnchor:
|
|
break;
|
|
case MarkType::kBug:
|
|
break;
|
|
case MarkType::kClass:
|
|
this->wordCheck(def->fName);
|
|
break;
|
|
case MarkType::kCode:
|
|
fInCode = true;
|
|
break;
|
|
case MarkType::kColumn:
|
|
break;
|
|
case MarkType::kComment:
|
|
break;
|
|
case MarkType::kConst: {
|
|
fInConst = true;
|
|
if (TableState::kNone == fTableState) {
|
|
fTableState = TableState::kRow;
|
|
}
|
|
if (TableState::kRow == fTableState) {
|
|
fTableState = TableState::kColumn;
|
|
}
|
|
this->wordCheck(def->fName);
|
|
const char* lineEnd = strchr(textStart, '\n');
|
|
this->wordCheck(lineEnd - textStart, textStart);
|
|
textStart = lineEnd;
|
|
} break;
|
|
case MarkType::kDefine:
|
|
break;
|
|
case MarkType::kDefinedBy:
|
|
break;
|
|
case MarkType::kDeprecated:
|
|
break;
|
|
case MarkType::kDescription:
|
|
fInDescription = true;
|
|
break;
|
|
case MarkType::kDoxygen:
|
|
break;
|
|
case MarkType::kEnum:
|
|
case MarkType::kEnumClass:
|
|
this->wordCheck(def->fName);
|
|
break;
|
|
case MarkType::kError:
|
|
break;
|
|
case MarkType::kExample:
|
|
break;
|
|
case MarkType::kExternal:
|
|
break;
|
|
case MarkType::kFile:
|
|
break;
|
|
case MarkType::kFormula:
|
|
break;
|
|
case MarkType::kFunction:
|
|
break;
|
|
case MarkType::kHeight:
|
|
break;
|
|
case MarkType::kImage:
|
|
break;
|
|
case MarkType::kLegend:
|
|
break;
|
|
case MarkType::kList:
|
|
break;
|
|
case MarkType::kMember:
|
|
break;
|
|
case MarkType::kMethod: {
|
|
string method_name = def->methodName();
|
|
string formattedStr = def->formatFunction();
|
|
if (!def->isClone()) {
|
|
this->wordCheck(method_name);
|
|
}
|
|
fTableState = TableState::kNone;
|
|
fMethod = def;
|
|
} break;
|
|
case MarkType::kParam: {
|
|
if (TableState::kNone == fTableState) {
|
|
fTableState = TableState::kRow;
|
|
}
|
|
if (TableState::kRow == fTableState) {
|
|
fTableState = TableState::kColumn;
|
|
}
|
|
TextParser paramParser(def->fFileName, def->fStart, def->fContentStart,
|
|
def->fLineCount);
|
|
paramParser.skipWhiteSpace();
|
|
SkASSERT(paramParser.startsWith("#Param"));
|
|
paramParser.next(); // skip hash
|
|
paramParser.skipToNonAlphaNum(); // skip Param
|
|
paramParser.skipSpace();
|
|
const char* paramName = paramParser.fChar;
|
|
paramParser.skipToSpace();
|
|
fInCode = true;
|
|
this->wordCheck(paramParser.fChar - paramName, paramName);
|
|
fInCode = false;
|
|
} break;
|
|
case MarkType::kPlatform:
|
|
break;
|
|
case MarkType::kReturn:
|
|
break;
|
|
case MarkType::kRow:
|
|
break;
|
|
case MarkType::kSeeAlso:
|
|
break;
|
|
case MarkType::kStdOut: {
|
|
fInStdOut = true;
|
|
TextParser code(def);
|
|
code.skipSpace();
|
|
while (!code.eof()) {
|
|
const char* end = code.trimmedLineEnd();
|
|
this->wordCheck(end - code.fChar, code.fChar);
|
|
code.skipToLineStart();
|
|
}
|
|
fInStdOut = false;
|
|
} break;
|
|
case MarkType::kStruct:
|
|
fRoot = def->asRoot();
|
|
this->wordCheck(def->fName);
|
|
break;
|
|
case MarkType::kSubtopic:
|
|
this->printCheck(printable);
|
|
break;
|
|
case MarkType::kTable:
|
|
break;
|
|
case MarkType::kTemplate:
|
|
break;
|
|
case MarkType::kText:
|
|
break;
|
|
case MarkType::kTime:
|
|
break;
|
|
case MarkType::kToDo:
|
|
break;
|
|
case MarkType::kTopic:
|
|
this->printCheck(printable);
|
|
break;
|
|
case MarkType::kTrack:
|
|
// don't output children
|
|
return true;
|
|
case MarkType::kTypedef:
|
|
break;
|
|
case MarkType::kUnion:
|
|
break;
|
|
case MarkType::kWidth:
|
|
break;
|
|
default:
|
|
SkASSERT(0); // handle everything
|
|
break;
|
|
}
|
|
this->childCheck(def, textStart);
|
|
switch (def->fMarkType) { // post child work, at least for tables
|
|
case MarkType::kCode:
|
|
fInCode = false;
|
|
break;
|
|
case MarkType::kColumn:
|
|
break;
|
|
case MarkType::kDescription:
|
|
fInDescription = false;
|
|
break;
|
|
case MarkType::kEnum:
|
|
case MarkType::kEnumClass:
|
|
break;
|
|
case MarkType::kExample:
|
|
break;
|
|
case MarkType::kLegend:
|
|
break;
|
|
case MarkType::kMethod:
|
|
fMethod = nullptr;
|
|
break;
|
|
case MarkType::kConst:
|
|
fInConst = false;
|
|
case MarkType::kParam:
|
|
SkASSERT(TableState::kColumn == fTableState);
|
|
fTableState = TableState::kRow;
|
|
break;
|
|
case MarkType::kReturn:
|
|
case MarkType::kSeeAlso:
|
|
break;
|
|
case MarkType::kRow:
|
|
break;
|
|
case MarkType::kStruct:
|
|
fRoot = fRoot->rootParent();
|
|
break;
|
|
case MarkType::kTable:
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool SpellCheck::checkable(MarkType markType) {
|
|
return BmhParser::Resolvable::kYes == fBmhParser.fMaps[(int) markType].fResolve;
|
|
}
|
|
|
|
void SpellCheck::childCheck(const Definition* def, const char* start) {
|
|
const char* end;
|
|
fLineCount = def->fLineCount;
|
|
if (def->isRoot()) {
|
|
fRoot = const_cast<RootDefinition*>(def->asRoot());
|
|
}
|
|
for (auto& child : def->fChildren) {
|
|
end = child->fStart;
|
|
if (this->checkable(def->fMarkType)) {
|
|
this->leafCheck(start, end);
|
|
}
|
|
this->check(child);
|
|
start = child->fTerminator;
|
|
}
|
|
if (this->checkable(def->fMarkType)) {
|
|
end = def->fContentEnd;
|
|
this->leafCheck(start, end);
|
|
}
|
|
}
|
|
|
|
void SpellCheck::leafCheck(const char* start, const char* end) {
|
|
TextParser text("", start, end, fLineCount);
|
|
do {
|
|
const char* lineStart = text.fChar;
|
|
text.skipToAlpha();
|
|
if (text.eof()) {
|
|
break;
|
|
}
|
|
const char* wordStart = text.fChar;
|
|
text.fChar = lineStart;
|
|
text.skipTo(wordStart); // advances line number
|
|
text.skipToNonAlphaNum();
|
|
fLineCount = text.fLineCount;
|
|
string word(wordStart, text.fChar - wordStart);
|
|
wordCheck(word);
|
|
} while (!text.eof());
|
|
}
|
|
|
|
void SpellCheck::printCheck(const string& str) {
|
|
string word;
|
|
for (std::stringstream stream(str); stream >> word; ) {
|
|
wordCheck(word);
|
|
}
|
|
}
|
|
|
|
void SpellCheck::report() {
|
|
for (auto iter : fWords) {
|
|
if (string::npos != iter.second.fFile.find("undocumented.bmh")) {
|
|
continue;
|
|
}
|
|
if (string::npos != iter.second.fFile.find("markup.bmh")) {
|
|
continue;
|
|
}
|
|
if (string::npos != iter.second.fFile.find("usingBookmaker.bmh")) {
|
|
continue;
|
|
}
|
|
if (iter.second.fCount == 1) {
|
|
SkDebugf("%s %s %d\n", iter.first.c_str(), iter.second.fFile.c_str(),
|
|
iter.second.fLine);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SpellCheck::wordCheck(const string& str) {
|
|
bool hasColon = false;
|
|
bool hasDot = false;
|
|
bool hasParen = false;
|
|
bool hasUnderscore = false;
|
|
bool sawDash = false;
|
|
bool sawDigit = false;
|
|
bool sawSpecial = false;
|
|
SkASSERT(str.length() > 0);
|
|
SkASSERT(isalpha(str[0]) || '~' == str[0]);
|
|
for (char ch : str) {
|
|
if (isalpha(ch) || '-' == ch) {
|
|
sawDash |= '-' == ch;
|
|
continue;
|
|
}
|
|
bool isColon = ':' == ch;
|
|
hasColon |= isColon;
|
|
bool isDot = '.' == ch;
|
|
hasDot |= isDot;
|
|
bool isParen = '(' == ch || ')' == ch || '~' == ch || '=' == ch || '!' == ch;
|
|
hasParen |= isParen;
|
|
bool isUnderscore = '_' == ch;
|
|
hasUnderscore |= isUnderscore;
|
|
if (isColon || isDot || isUnderscore || isParen) {
|
|
continue;
|
|
}
|
|
if (isdigit(ch)) {
|
|
sawDigit = true;
|
|
continue;
|
|
}
|
|
if ('&' == ch || ',' == ch || ' ' == ch) {
|
|
sawSpecial = true;
|
|
continue;
|
|
}
|
|
SkASSERT(0);
|
|
}
|
|
if (sawSpecial && !hasParen) {
|
|
SkASSERT(0);
|
|
}
|
|
bool inCode = fInCode;
|
|
if (hasUnderscore && isupper(str[0]) && ('S' != str[0] || 'K' != str[1])
|
|
&& !hasColon && !hasDot && !hasParen && !fInStdOut && !inCode && !fInConst
|
|
&& !sawDigit && !sawSpecial && !sawDash) {
|
|
std::istringstream ss(str);
|
|
string token;
|
|
while (std::getline(ss, token, '_')) {
|
|
this->wordCheck(token);
|
|
}
|
|
return;
|
|
}
|
|
if (!hasColon && !hasDot && !hasParen && !hasUnderscore
|
|
&& !fInStdOut && !inCode && !fInConst && !sawDigit
|
|
&& islower(str[0]) && isupper(str[1])) {
|
|
inCode = true;
|
|
}
|
|
auto& mappy = hasColon ? fColons :
|
|
hasDot ? fDots :
|
|
hasParen ? fParens :
|
|
hasUnderscore ? fUnderscores :
|
|
fInStdOut || inCode || fInConst ? fCode :
|
|
sawDigit ? fDigits : fWords;
|
|
auto iter = mappy.find(str);
|
|
if (mappy.end() != iter) {
|
|
iter->second.fCount += 1;
|
|
} else {
|
|
CheckEntry* entry = &mappy[str];
|
|
entry->fFile = fFileName;
|
|
entry->fLine = fLineCount;
|
|
entry->fCount = 1;
|
|
}
|
|
}
|
|
|
|
void SpellCheck::wordCheck(ptrdiff_t len, const char* ch) {
|
|
leafCheck(ch, ch + len);
|
|
}
|