diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 3ec0e47a7..d549ccbc8 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -262,6 +262,8 @@ set(SRCS cmPropertyMap.h cmQtAutomoc.cxx cmQtAutomoc.h + cmRST.cxx + cmRST.h cmScriptGenerator.h cmScriptGenerator.cxx cmSourceFile.cxx diff --git a/Source/cmRST.cxx b/Source/cmRST.cxx new file mode 100644 index 000000000..72c42d6bf --- /dev/null +++ b/Source/cmRST.cxx @@ -0,0 +1,435 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2013 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmRST.h" + +#include "cmSystemTools.h" + +#include + +//---------------------------------------------------------------------------- +cmRST::cmRST(std::ostream& os, std::string const& docroot): + OS(os), + DocRoot(docroot), + IncludeDepth(0), + OutputLinePending(false), + Markup(MarkupNone), + Directive(DirectiveNone), + CMakeDirective("^.. (cmake:)?(" + "command|variable" + ")::[ \t]+([^ \t\n]+)$"), + CMakeModuleDirective("^.. cmake-module::[ \t]+([^ \t\n]+)$"), + ParsedLiteralDirective("^.. parsed-literal::[ \t]*(.*)$"), + CodeBlockDirective("^.. code-block::[ \t]*(.*)$"), + ReplaceDirective("^.. (\\|[^|]+\\|) replace::[ \t]*(.*)$"), + IncludeDirective("^.. include::[ \t]+([^ \t\n]+)$"), + TocTreeDirective("^.. toctree::[ \t]*(.*)$"), + CMakeRole("(:cmake)?:(" + "command|generator|variable|module|policy|" + "prop_cache|prop_dir|prop_gbl|prop_sf|prop_test|prop_tgt|" + "manual" + "):`(<*([^`<]|[^` \t]<)*)([ \t]+<[^`]*>)?`"), + Substitution("(^|[^A-Za-z0-9_])" + "((\\|[^| \t\r\n]([^|\r\n]*[^| \t\r\n])?\\|)(__|_|))" + "([^A-Za-z0-9_]|$)") +{ +} + +//---------------------------------------------------------------------------- +bool cmRST::ProcessFile(std::string const& fname, bool isModule) +{ + std::ifstream fin(fname.c_str()); + if(fin) + { + this->DocDir = cmSystemTools::GetFilenamePath(fname); + if(isModule) + { + this->ProcessModule(fin); + } + else + { + this->ProcessRST(fin); + } + this->OutputLinePending = true; + return true; + } + return false; +} + +//---------------------------------------------------------------------------- +void cmRST::ProcessRST(std::istream& is) +{ + std::string line; + while(cmSystemTools::GetLineFromStream(is, line)) + { + this->ProcessLine(line); + } + this->Reset(); +} + +//---------------------------------------------------------------------------- +void cmRST::ProcessModule(std::istream& is) +{ + std::string line; + std::string rst; + while(cmSystemTools::GetLineFromStream(is, line)) + { + if(rst == "#") + { + if(line == "#") + { + this->ProcessLine(""); + continue; + } + else if(line.substr(0, 2) == "# ") + { + this->ProcessLine(line.substr(2, line.npos)); + continue; + } + else + { + rst = ""; + this->Reset(); + this->OutputLinePending = true; + } + } + if(line == "#.rst:") + { + rst = "#"; + } + } + if(rst == "#") + { + this->Reset(); + } +} + +//---------------------------------------------------------------------------- +void cmRST::Reset() +{ + if(!this->MarkupLines.empty()) + { + this->UnindentLines(this->MarkupLines); + } + switch(this->Directive) + { + case DirectiveNone: break; + case DirectiveParsedLiteral: this->ProcessDirectiveParsedLiteral(); break; + case DirectiveCodeBlock: this->ProcessDirectiveCodeBlock(); break; + case DirectiveReplace: this->ProcessDirectiveReplace(); break; + case DirectiveTocTree: this->ProcessDirectiveTocTree(); break; + } + this->Markup = MarkupNone; + this->Directive = DirectiveNone; + this->MarkupLines.clear(); +} + +//---------------------------------------------------------------------------- +void cmRST::ProcessLine(std::string const& line) +{ + // A line starting in .. is an explicit markup start. + if(line == ".." || (line.size() >= 3 && line[0] == '.' && + line[1] == '.' && isspace(line[2]))) + { + this->Reset(); + this->Markup = (line.find_first_not_of(" \t", 2) == line.npos ? + MarkupEmpty : MarkupNormal); + if(this->CMakeDirective.find(line)) + { + // Output cmake domain directives and their content normally. + this->NormalLine(line); + } + else if(this->CMakeModuleDirective.find(line)) + { + // Process cmake-module directive: scan .cmake file comments. + std::string file = this->CMakeModuleDirective.match(1); + if(file.empty() || !this->ProcessInclude(file, IncludeModule)) + { + this->NormalLine(line); + } + } + else if(this->ParsedLiteralDirective.find(line)) + { + // Record the literal lines to output after whole block. + this->Directive = DirectiveParsedLiteral; + this->MarkupLines.push_back(this->ParsedLiteralDirective.match(1)); + } + else if(this->CodeBlockDirective.find(line)) + { + // Record the literal lines to output after whole block. + // Ignore the language spec and record the opening line as blank. + this->Directive = DirectiveCodeBlock; + this->MarkupLines.push_back(""); + } + else if(this->ReplaceDirective.find(line)) + { + // Record the replace directive content. + this->Directive = DirectiveReplace; + this->ReplaceName = this->ReplaceDirective.match(1); + this->MarkupLines.push_back(this->ReplaceDirective.match(2)); + } + else if(this->IncludeDirective.find(line)) + { + // Process the include directive or output the directive and its + // content normally if it fails. + std::string file = this->IncludeDirective.match(1); + if(file.empty() || !this->ProcessInclude(file, IncludeNormal)) + { + this->NormalLine(line); + } + } + else if(this->TocTreeDirective.find(line)) + { + // Record the toctree entries to process after whole block. + this->Directive = DirectiveTocTree; + this->MarkupLines.push_back(this->TocTreeDirective.match(1)); + } + } + // An explicit markup start followed nothing but whitespace and a + // blank line does not consume any indented text following. + else if(this->Markup == MarkupEmpty && line.empty()) + { + this->NormalLine(line); + } + // Indented lines following an explicit markup start are explicit markup. + else if(this->Markup && (line.empty() || isspace(line[0]))) + { + this->Markup = MarkupNormal; + // Record markup lines if the start line was recorded. + if(!this->MarkupLines.empty()) + { + this->MarkupLines.push_back(line); + } + } + // Print non-markup lines. + else + { + this->NormalLine(line); + } +} + +//---------------------------------------------------------------------------- +void cmRST::NormalLine(std::string const& line) +{ + this->Reset(); + this->OutputLine(line); +} + +//---------------------------------------------------------------------------- +void cmRST::OutputLine(std::string const& line_in) +{ + if(this->OutputLinePending) + { + this->OS << "\n"; + this->OutputLinePending = false; + } + std::string line = this->ReplaceSubstitutions(line_in); + std::string::size_type pos = 0; + while(this->CMakeRole.find(line.c_str()+pos)) + { + this->OS << line.substr(pos, this->CMakeRole.start()); + std::string text = this->CMakeRole.match(3); + // If a command reference has no explicit target and + // no explicit "(...)" then add "()" to the text. + if(this->CMakeRole.match(2) == "command" && + this->CMakeRole.match(5).empty() && + text.find_first_of("()") == text.npos) + { + text += "()"; + } + this->OS << "``" << text << "``"; + pos += this->CMakeRole.end(); + } + this->OS << line.substr(pos) << "\n"; +} + +//---------------------------------------------------------------------------- +std::string cmRST::ReplaceSubstitutions(std::string const& line) +{ + std::string out; + std::string::size_type pos = 0; + while(this->Substitution.find(line.c_str()+pos)) + { + std::string::size_type start = this->Substitution.start(2); + std::string::size_type end = this->Substitution.end(2); + std::string substitute = this->Substitution.match(3); + std::map::iterator + replace = this->Replace.find(substitute); + if(replace != this->Replace.end()) + { + std::pair::iterator, bool> replaced = + this->Replaced.insert(substitute); + if(replaced.second) + { + substitute = this->ReplaceSubstitutions(replace->second); + this->Replaced.erase(replaced.first); + } + } + out += line.substr(pos, start); + out += substitute; + pos += end; + } + out += line.substr(pos); + return out; +} + +//---------------------------------------------------------------------------- +bool cmRST::ProcessInclude(std::string file, IncludeType type) +{ + bool found = false; + if(this->IncludeDepth < 10) + { + cmRST r(this->OS, this->DocRoot); + r.IncludeDepth = this->IncludeDepth + 1; + r.OutputLinePending = this->OutputLinePending; + if(type != IncludeTocTree) + { + r.Replace = this->Replace; + } + if(file[0] == '/') + { + file = this->DocRoot + file; + } + else + { + file = this->DocDir + "/" + file; + } + found = r.ProcessFile(file, type == IncludeModule); + if(type != IncludeTocTree) + { + this->Replace = r.Replace; + } + this->OutputLinePending = r.OutputLinePending; + } + return found; +} + +//---------------------------------------------------------------------------- +void cmRST::ProcessDirectiveParsedLiteral() +{ + // Output markup lines as literal text. + for(std::vector::iterator i = this->MarkupLines.begin(); + i != this->MarkupLines.end(); ++i) + { + std::string line = *i; + if(!line.empty()) + { + line = " " + line; + } + this->OutputLine(line); + } + this->OutputLinePending = true; +} + +//---------------------------------------------------------------------------- +void cmRST::ProcessDirectiveCodeBlock() +{ + // Treat markup lines the same as a parsed literal. + this->ProcessDirectiveParsedLiteral(); +} + +//---------------------------------------------------------------------------- +void cmRST::ProcessDirectiveReplace() +{ + // Record markup lines as replacement text. + std::string& replacement = this->Replace[this->ReplaceName]; + const char* sep = ""; + for(std::vector::iterator i = this->MarkupLines.begin(); + i != this->MarkupLines.end(); ++i) + { + replacement += sep; + replacement += *i; + sep = " "; + } + this->ReplaceName = ""; +} + +//---------------------------------------------------------------------------- +void cmRST::ProcessDirectiveTocTree() +{ + // Process documents referenced by toctree directive. + for(std::vector::iterator i = this->MarkupLines.begin(); + i != this->MarkupLines.end(); ++i) + { + if(!i->empty() && i->find_first_of(":") == i->npos) + { + this->ProcessInclude(*i + ".rst", IncludeTocTree); + } + } +} + +//---------------------------------------------------------------------------- +void cmRST::UnindentLines(std::vector& lines) +{ + // Remove the common indentation from the second and later lines. + std::string indentText; + std::string::size_type indentEnd = 0; + bool first = true; + for(size_t i = 1; i < lines.size(); ++i) + { + std::string const& line = lines[i]; + + // Do not consider empty lines. + if(line.empty()) + { + continue; + } + + // Record indentation on first non-empty line. + if(first) + { + first = false; + indentEnd = line.find_first_not_of(" \t"); + indentText = line.substr(0, indentEnd); + continue; + } + + // Truncate indentation to match that on this line. + if(line.size() < indentEnd) + { + indentEnd = line.size(); + } + for(std::string::size_type j = 0; j != indentEnd; ++j) + { + if(line[j] != indentText[j]) + { + indentEnd = j; + break; + } + } + } + + // Update second and later lines. + for(size_t i = 1; i < lines.size(); ++i) + { + std::string& line = lines[i]; + if(!line.empty()) + { + line = line.substr(indentEnd); + } + } + + // Drop leading blank lines. + size_t leadingEmpty = 0; + for(size_t i = 0; i < lines.size() && lines[i].empty(); ++i) + { + ++leadingEmpty; + } + lines.erase(lines.begin(), lines.begin()+leadingEmpty); + + // Drop trailing blank lines. + size_t trailingEmpty = 0; + for(size_t i = lines.size(); i > 0 && lines[i-1].empty(); --i) + { + ++trailingEmpty; + } + lines.erase(lines.end()-trailingEmpty, lines.end()); +} diff --git a/Source/cmRST.h b/Source/cmRST.h new file mode 100644 index 000000000..9352941de --- /dev/null +++ b/Source/cmRST.h @@ -0,0 +1,83 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2013 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef _cmRST_h +#define _cmRST_h + +#include "cmStandardIncludes.h" + +#include + +class cmRST +{ +public: + cmRST(std::ostream& os, std::string const& docroot); + bool ProcessFile(std::string const& fname, bool isModule = false); +private: + enum IncludeType + { + IncludeNormal, + IncludeModule, + IncludeTocTree + }; + enum MarkupType + { + MarkupNone, + MarkupNormal, + MarkupEmpty + }; + enum DirectiveType + { + DirectiveNone, + DirectiveParsedLiteral, + DirectiveCodeBlock, + DirectiveReplace, + DirectiveTocTree + }; + + void ProcessRST(std::istream& is); + void ProcessModule(std::istream& is); + void Reset(); + void ProcessLine(std::string const& line); + void NormalLine(std::string const& line); + void OutputLine(std::string const& line); + std::string ReplaceSubstitutions(std::string const& line); + bool ProcessInclude(std::string file, IncludeType type); + void ProcessDirectiveParsedLiteral(); + void ProcessDirectiveCodeBlock(); + void ProcessDirectiveReplace(); + void ProcessDirectiveTocTree(); + static void UnindentLines(std::vector& lines); + + std::ostream& OS; + std::string DocRoot; + int IncludeDepth; + bool OutputLinePending; + MarkupType Markup; + DirectiveType Directive; + cmsys::RegularExpression CMakeDirective; + cmsys::RegularExpression CMakeModuleDirective; + cmsys::RegularExpression ParsedLiteralDirective; + cmsys::RegularExpression CodeBlockDirective; + cmsys::RegularExpression ReplaceDirective; + cmsys::RegularExpression IncludeDirective; + cmsys::RegularExpression TocTreeDirective; + cmsys::RegularExpression CMakeRole; + cmsys::RegularExpression Substitution; + + std::vector MarkupLines; + std::string DocDir; + std::map Replace; + std::set Replaced; + std::string ReplaceName; +}; + +#endif diff --git a/Tests/CMakeLib/CMakeLists.txt b/Tests/CMakeLib/CMakeLists.txt index 070c9cc22..0e1fe8d42 100644 --- a/Tests/CMakeLib/CMakeLists.txt +++ b/Tests/CMakeLib/CMakeLists.txt @@ -6,6 +6,7 @@ include_directories( set(CMakeLib_TESTS testGeneratedFileStream + testRST testSystemTools testUTF8 testXMLParser diff --git a/Tests/CMakeLib/testRST.cxx b/Tests/CMakeLib/testRST.cxx new file mode 100644 index 000000000..bad95600e --- /dev/null +++ b/Tests/CMakeLib/testRST.cxx @@ -0,0 +1,96 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2013 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmRST.h" + +#include "cmSystemTools.h" + +void reportLine(std::ostream& os, bool ret, std::string line, bool eol) +{ + if(ret) + { + os << "\"" << line << "\" (" << (eol?"with EOL":"without EOL") << ")"; + } + else + { + os << "EOF"; + } +} + +int testRST(int, char*[]) +{ + std::string dir = cmSystemTools::GetFilenamePath(__FILE__); + if(dir.empty()) + { + dir = "."; + } + std::string a_name = "testRST.actual"; + std::string e_name = dir + "/testRST.expect"; + + // Process the test RST file. + { + std::string fname = dir + "/testRST.rst"; + std::ofstream fout(a_name.c_str()); + if(!fout) + { + std::cerr << "Could not open output " << a_name << std::endl; + return 1; + } + + cmRST r(fout, dir); + if(!r.ProcessFile(fname)) + { + std::cerr << "Could not open input " << fname << std::endl; + return 1; + } + } + + // Compare expected and actual outputs. + std::ifstream e_fin(e_name.c_str()); + std::ifstream a_fin(a_name.c_str()); + if(!e_fin) + { + std::cerr << "Could not open input " << e_name << std::endl; + return 1; + } + if(!a_fin) + { + std::cerr << "Could not open input " << a_name << std::endl; + return 1; + } + int lineno = 0; + bool e_ret; + bool a_ret; + do + { + std::string e_line; + std::string a_line; + bool e_eol; + bool a_eol; + e_ret = cmSystemTools::GetLineFromStream(e_fin, e_line, &e_eol); + a_ret = cmSystemTools::GetLineFromStream(a_fin, a_line, &a_eol); + ++lineno; + if(e_ret != a_ret || e_line != a_line || e_eol != a_eol) + { + a_fin.seekg(0, std::ios::beg); + std::cerr << "Actual output does not match that expected on line " + << lineno << "." << std::endl << "Expected "; + reportLine(std::cerr, e_ret, e_line, e_eol); + std::cerr << " but got "; + reportLine(std::cerr, a_ret, a_line, a_eol); + std::cerr << "." << std::endl + << "Actual output:" << std::endl + << a_fin.rdbuf(); + return 1; + } + } while(e_ret && a_ret); + return 0; +} diff --git a/Tests/CMakeLib/testRST.expect b/Tests/CMakeLib/testRST.expect new file mode 100644 index 000000000..6a890021b --- /dev/null +++ b/Tests/CMakeLib/testRST.expect @@ -0,0 +1,60 @@ +title_text +---------- + +Command ``some_cmd()`` explicit cmake domain. +Command ``some_cmd()`` without target. +Command ``some_cmd`` with target. +Command ``some_cmd_()`` placeholder without target. +Command ``some_cmd_`` placholder with target. +Command ``some_cmd()`` with parens. +Command ``some_cmd(SUB)`` with subcommand. +Command ``some_cmd(SUB)`` with subcommand and target. +Command ``some_cmd (SUB)`` with space and subcommand and target. +Command ``some command`` with space and target. +Variable ``some variable`` space and target. +Variable ``_VARIABLE`` with leading placeholder. +Variable ``VARIABLE_`` with trailing placeholder. +Variable ``_VARIABLE`` with leading placeholder and target. +Variable ``VARIABLE_`` with trailing placeholder and target. +Generator ``Some Generator`` with space. + +First TOC entry. + +|not replaced| +Second TOC entry. + +CMake Module Content + +More CMake Module Content + +.. cmake:command:: some_cmd + + Command some_cmd description. + +.. command:: other_cmd + + Command other_cmd description. + +.. cmake:variable:: some_var + + Variable some_var description. + +.. variable:: other_var + + Variable other_var description. + + Parsed-literal included without directive. + Common Indentation Removed + + # Sample CMake code block + if(condition) + message(indented) + endif() + +substituted text with multiple lines becomes one line + +End of first include. + +Cross-include substitution text with ``some_cmd()`` reference. + +End of second include. diff --git a/Tests/CMakeLib/testRST.rst b/Tests/CMakeLib/testRST.rst new file mode 100644 index 000000000..f7059cf4e --- /dev/null +++ b/Tests/CMakeLib/testRST.rst @@ -0,0 +1,72 @@ +.. index:: + single: directive ignored + +title_text +---------- + +.. comment ignored +.. + comment ignored + +Command :cmake:command:`some_cmd` explicit cmake domain. +Command :command:`some_cmd` without target. +Command :command:`some_cmd ` with target. +Command :command:`some_cmd_` placeholder without target. +Command :command:`some_cmd_ ` placholder with target. +Command :command:`some_cmd()` with parens. +Command :command:`some_cmd(SUB)` with subcommand. +Command :command:`some_cmd(SUB) ` with subcommand and target. +Command :command:`some_cmd (SUB) ` with space and subcommand and target. +Command :command:`some command ` with space and target. +Variable :variable:`some variable ` space and target. +Variable :variable:`_VARIABLE` with leading placeholder. +Variable :variable:`VARIABLE_` with trailing placeholder. +Variable :variable:`_VARIABLE ` with leading placeholder and target. +Variable :variable:`VARIABLE_ ` with trailing placeholder and target. +Generator :generator:`Some Generator` with space. + +.. |not replaced| replace:: not replaced through toctree + +.. toctree:: + :maxdepth: 2 + + testRSTtoc1 + /testRSTtoc2 + +.. cmake-module:: testRSTmod.cmake + +.. cmake:command:: some_cmd + + Command some_cmd description. + +.. command:: other_cmd + + Command other_cmd description. + +.. cmake:variable:: some_var + + Variable some_var description. + +.. variable:: other_var + + Variable other_var description. + +.. parsed-literal:: + + Parsed-literal included without directive. + Common Indentation Removed + +.. code-block:: cmake + + # Sample CMake code block + if(condition) + message(indented) + endif() + +.. |substitution| replace:: + |nested substitution| + with multiple lines becomes one line +.. |nested substitution| replace:: substituted text + +.. include:: testRSTinclude1.rst +.. include:: /testRSTinclude2.rst diff --git a/Tests/CMakeLib/testRSTinclude1.rst b/Tests/CMakeLib/testRSTinclude1.rst new file mode 100644 index 000000000..91d394e3a --- /dev/null +++ b/Tests/CMakeLib/testRSTinclude1.rst @@ -0,0 +1,6 @@ +|substitution| + +.. |cross-include substitution| replace:: Cross-include substitution text + with :command:`some_cmd` reference. + +End of first include. diff --git a/Tests/CMakeLib/testRSTinclude2.rst b/Tests/CMakeLib/testRSTinclude2.rst new file mode 100644 index 000000000..f2d619c4f --- /dev/null +++ b/Tests/CMakeLib/testRSTinclude2.rst @@ -0,0 +1,3 @@ +|cross-include substitution| + +End of second include. diff --git a/Tests/CMakeLib/testRSTmod.cmake b/Tests/CMakeLib/testRSTmod.cmake new file mode 100644 index 000000000..dfa793dd9 --- /dev/null +++ b/Tests/CMakeLib/testRSTmod.cmake @@ -0,0 +1,4 @@ +#.rst: +# CMake Module Content +#.rst: +# More CMake Module Content diff --git a/Tests/CMakeLib/testRSTtoc1.rst b/Tests/CMakeLib/testRSTtoc1.rst new file mode 100644 index 000000000..fa7806ede --- /dev/null +++ b/Tests/CMakeLib/testRSTtoc1.rst @@ -0,0 +1,2 @@ +.. |not replaced| replace:: not replaced across toctree +First TOC entry. diff --git a/Tests/CMakeLib/testRSTtoc2.rst b/Tests/CMakeLib/testRSTtoc2.rst new file mode 100644 index 000000000..9fd2fcb19 --- /dev/null +++ b/Tests/CMakeLib/testRSTtoc2.rst @@ -0,0 +1,2 @@ +|not replaced| +Second TOC entry.