1 /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
2 file Copyright.txt or https://cmake.org/licensing for details. */
10 #include "cmsys/FStream.hxx"
12 #include "cmAlgorithms.h"
14 #include "cmStringAlgorithms.h"
15 #include "cmSystemTools.h"
16 #include "cmVersion.h"
18 cmRST::cmRST(std::ostream
& os
, std::string docroot
)
20 , DocRoot(std::move(docroot
))
21 , CMakeDirective("^.. (cmake:)?("
22 "command|envvar|genex|signature|variable"
24 , CMakeModuleDirective("^.. cmake-module::[ \t]+([^ \t\n]+)$")
25 , ParsedLiteralDirective("^.. parsed-literal::[ \t]*(.*)$")
26 , CodeBlockDirective("^.. code-block::[ \t]*(.*)$")
27 , ReplaceDirective("^.. (\\|[^|]+\\|) replace::[ \t]*(.*)$")
28 , IncludeDirective("^.. include::[ \t]+([^ \t\n]+)$")
29 , TocTreeDirective("^.. toctree::[ \t]*(.*)$")
30 , ProductionListDirective("^.. productionlist::[ \t]*(.*)$")
31 , NoteDirective("^.. note::[ \t]*(.*)$")
32 , VersionDirective("^.. version(added|changed)::[ \t]*(.*)$")
33 , ModuleRST(R
"(^#\[(=*)\[\.rst:$)")
34 , CMakeRole("(:cmake)?:("
36 "command|cpack_gen|generator|genex|"
37 "variable|envvar|module|policy|"
38 "prop_cache|prop_dir|prop_gbl|prop_inst|prop_sf|"
41 "):`(<*([^`<]|[^` \t]<)*)([ \t]+<[^`]*>)?`")
42 , InlineLink("`(<*([^`<]|[^` \t]<)*)([ \t]+<[^`]*>)?`_")
43 , InlineLiteral("``([^`]*)``")
44 , Substitution("(^|[^A-Za-z0-9_])"
45 "((\\|[^| \t\r\n]([^|\r\n]*[^| \t\r\n])?\\|)(__|_|))"
47 , TocTreeLink("^.*[ \t]+<([^>]+)>$")
49 this->Replace
["|release|"] = cmVersion::GetCMakeVersion();
52 bool cmRST::ProcessFile(std::string
const& fname
, bool isModule
)
54 cmsys::ifstream
fin(fname
.c_str());
56 this->DocDir
= cmSystemTools::GetFilenamePath(fname
);
58 this->ProcessModule(fin
);
60 this->ProcessRST(fin
);
62 this->OutputLinePending
= true;
68 void cmRST::ProcessRST(std::istream
& is
)
71 while (cmSystemTools::GetLineFromStream(is
, line
)) {
72 this->ProcessLine(line
);
77 void cmRST::ProcessModule(std::istream
& is
)
81 while (cmSystemTools::GetLineFromStream(is
, line
)) {
82 if (!rst
.empty() && rst
!= "#") {
83 // Bracket mode: check for end bracket
84 std::string::size_type pos
= line
.find(rst
);
85 if (pos
== std::string::npos
) {
86 this->ProcessLine(line
);
90 this->ProcessLine(line
);
94 this->OutputLinePending
= true;
97 // Line mode: check for .rst start (bracket or line)
100 this->ProcessLine("");
103 if (cmHasLiteralPrefix(line
, "# ")) {
105 this->ProcessLine(line
);
110 this->OutputLinePending
= true;
112 if (line
== "#.rst:") {
114 } else if (this->ModuleRST
.find(line
)) {
115 rst
= "]" + this->ModuleRST
.match(1) + "]";
126 if (!this->MarkupLines
.empty()) {
127 cmRST::UnindentLines(this->MarkupLines
);
129 switch (this->DirectiveType
) {
130 case Directive::None
:
132 case Directive::ParsedLiteral
:
133 this->ProcessDirectiveParsedLiteral();
135 case Directive::LiteralBlock
:
136 this->ProcessDirectiveLiteralBlock();
138 case Directive::CodeBlock
:
139 this->ProcessDirectiveCodeBlock();
141 case Directive::Replace
:
142 this->ProcessDirectiveReplace();
144 case Directive::TocTree
:
145 this->ProcessDirectiveTocTree();
148 this->MarkupType
= Markup::None
;
149 this->DirectiveType
= Directive::None
;
150 this->MarkupLines
.clear();
153 void cmRST::ProcessLine(std::string
const& line
)
155 bool lastLineEndedInColonColon
= this->LastLineEndedInColonColon
;
156 this->LastLineEndedInColonColon
= false;
158 // A line starting in .. is an explicit markup start.
160 (line
.size() >= 3 && line
[0] == '.' && line
[1] == '.' &&
161 cmIsSpace(line
[2]))) {
164 (line
.find_first_not_of(" \t", 2) == std::string::npos
? Markup::Empty
166 // XXX(clang-tidy): https://bugs.llvm.org/show_bug.cgi?id=44165
167 // NOLINTNEXTLINE(bugprone-branch-clone)
168 if (this->CMakeDirective
.find(line
)) {
169 // Output cmake domain directives and their content normally.
170 this->NormalLine(line
);
171 } else if (this->CMakeModuleDirective
.find(line
)) {
172 // Process cmake-module directive: scan .cmake file comments.
173 std::string file
= this->CMakeModuleDirective
.match(1);
174 if (file
.empty() || !this->ProcessInclude(file
, Include::Module
)) {
175 this->NormalLine(line
);
177 } else if (this->ParsedLiteralDirective
.find(line
)) {
178 // Record the literal lines to output after whole block.
179 this->DirectiveType
= Directive::ParsedLiteral
;
180 this->MarkupLines
.push_back(this->ParsedLiteralDirective
.match(1));
181 } else if (this->CodeBlockDirective
.find(line
)) {
182 // Record the literal lines to output after whole block.
183 // Ignore the language spec and record the opening line as blank.
184 this->DirectiveType
= Directive::CodeBlock
;
185 this->MarkupLines
.emplace_back();
186 } else if (this->ReplaceDirective
.find(line
)) {
187 // Record the replace directive content.
188 this->DirectiveType
= Directive::Replace
;
189 this->ReplaceName
= this->ReplaceDirective
.match(1);
190 this->MarkupLines
.push_back(this->ReplaceDirective
.match(2));
191 } else if (this->IncludeDirective
.find(line
)) {
192 // Process the include directive or output the directive and its
193 // content normally if it fails.
194 std::string file
= this->IncludeDirective
.match(1);
195 if (file
.empty() || !this->ProcessInclude(file
, Include::Normal
)) {
196 this->NormalLine(line
);
198 } else if (this->TocTreeDirective
.find(line
)) {
199 // Record the toctree entries to process after whole block.
200 this->DirectiveType
= Directive::TocTree
;
201 this->MarkupLines
.push_back(this->TocTreeDirective
.match(1));
202 } else if (this->ProductionListDirective
.find(line
)) {
203 // Output productionlist directives and their content normally.
204 this->NormalLine(line
);
205 } else if (this->NoteDirective
.find(line
)) {
206 // Output note directives and their content normally.
207 this->NormalLine(line
);
208 } else if (this->VersionDirective
.find(line
)) {
209 // Output versionadded and versionchanged directives and their content
211 this->NormalLine(line
);
214 // An explicit markup start followed by nothing but whitespace and a
215 // blank line does not consume any indented text following.
216 else if (this->MarkupType
== Markup::Empty
&& line
.empty()) {
217 this->NormalLine(line
);
219 // Indented lines following an explicit markup start are explicit markup.
220 else if (this->MarkupType
!= Markup::None
&&
221 (line
.empty() || cmIsSpace(line
[0]))) {
222 this->MarkupType
= Markup::Normal
;
223 // Record markup lines if the start line was recorded.
224 if (!this->MarkupLines
.empty()) {
225 this->MarkupLines
.push_back(line
);
228 // A blank line following a paragraph ending in "::" starts a literal block.
229 else if (lastLineEndedInColonColon
&& line
.empty()) {
230 // Record the literal lines to output after whole block.
231 this->MarkupType
= Markup::Normal
;
232 this->DirectiveType
= Directive::LiteralBlock
;
233 this->MarkupLines
.emplace_back();
234 this->OutputLine("", false);
236 // Print non-markup lines.
238 this->NormalLine(line
);
239 this->LastLineEndedInColonColon
=
240 (line
.size() >= 2 && line
[line
.size() - 2] == ':' && line
.back() == ':');
244 void cmRST::NormalLine(std::string
const& line
)
247 this->OutputLine(line
, true);
250 void cmRST::OutputLine(std::string
const& line_in
, bool inlineMarkup
)
252 if (this->OutputLinePending
) {
254 this->OutputLinePending
= false;
257 std::string line
= this->ReplaceSubstitutions(line_in
);
258 std::string::size_type pos
= 0;
260 std::string::size_type
* first
= nullptr;
261 std::string::size_type role_start
= std::string::npos
;
262 std::string::size_type link_start
= std::string::npos
;
263 std::string::size_type lit_start
= std::string::npos
;
264 if (this->CMakeRole
.find(line
.c_str() + pos
)) {
265 role_start
= this->CMakeRole
.start();
268 if (this->InlineLiteral
.find(line
.c_str() + pos
)) {
269 lit_start
= this->InlineLiteral
.start();
270 if (!first
|| lit_start
< *first
) {
274 if (this->InlineLink
.find(line
.c_str() + pos
)) {
275 link_start
= this->InlineLink
.start();
276 if (!first
|| link_start
< *first
) {
280 if (first
== &role_start
) {
281 this->OS
<< line
.substr(pos
, role_start
);
282 std::string text
= this->CMakeRole
.match(3);
283 // If a command reference has no explicit target and
284 // no explicit "(...)" then add "()" to the text.
285 if (this->CMakeRole
.match(2) == "command" &&
286 this->CMakeRole
.match(5).empty() &&
287 text
.find_first_of("()") == std::string::npos
) {
290 this->OS
<< "``" << text
<< "``";
291 pos
+= this->CMakeRole
.end();
292 } else if (first
== &lit_start
) {
293 this->OS
<< line
.substr(pos
, lit_start
);
294 std::string text
= this->InlineLiteral
.match(1);
295 pos
+= this->InlineLiteral
.end();
296 this->OS
<< "``" << text
<< "``";
297 } else if (first
== &link_start
) {
298 this->OS
<< line
.substr(pos
, link_start
);
299 std::string text
= this->InlineLink
.match(1);
300 bool escaped
= false;
301 for (char c
: text
) {
305 } else if (c
== '\\') {
311 pos
+= this->InlineLink
.end();
316 this->OS
<< line
.substr(pos
) << "\n";
318 this->OS
<< line_in
<< "\n";
322 std::string
cmRST::ReplaceSubstitutions(std::string
const& line
)
325 std::string::size_type pos
= 0;
326 while (this->Substitution
.find(line
.c_str() + pos
)) {
327 std::string::size_type start
= this->Substitution
.start(2);
328 std::string::size_type end
= this->Substitution
.end(2);
329 std::string substitute
= this->Substitution
.match(3);
330 auto replace
= this->Replace
.find(substitute
);
331 if (replace
!= this->Replace
.end()) {
332 std::pair
<std::set
<std::string
>::iterator
, bool> replaced
=
333 this->Replaced
.insert(substitute
);
334 if (replaced
.second
) {
335 substitute
= this->ReplaceSubstitutions(replace
->second
);
336 this->Replaced
.erase(replaced
.first
);
339 out
+= line
.substr(pos
, start
);
343 out
+= line
.substr(pos
);
347 void cmRST::OutputMarkupLines(bool inlineMarkup
)
349 for (auto line
: this->MarkupLines
) {
351 line
= cmStrCat(" ", line
);
353 this->OutputLine(line
, inlineMarkup
);
355 this->OutputLinePending
= true;
358 bool cmRST::ProcessInclude(std::string file
, Include type
)
361 if (this->IncludeDepth
< 10) {
362 cmRST
r(this->OS
, this->DocRoot
);
363 r
.IncludeDepth
= this->IncludeDepth
+ 1;
364 r
.OutputLinePending
= this->OutputLinePending
;
365 if (type
!= Include::TocTree
) {
366 r
.Replace
= this->Replace
;
368 if (file
[0] == '/') {
369 file
= this->DocRoot
+ file
;
371 file
= this->DocDir
+ "/" + file
;
373 found
= r
.ProcessFile(file
, type
== Include::Module
);
374 if (type
!= Include::TocTree
) {
375 this->Replace
= r
.Replace
;
377 this->OutputLinePending
= r
.OutputLinePending
;
382 void cmRST::ProcessDirectiveParsedLiteral()
384 this->OutputMarkupLines(true);
387 void cmRST::ProcessDirectiveLiteralBlock()
389 this->OutputMarkupLines(false);
392 void cmRST::ProcessDirectiveCodeBlock()
394 this->OutputMarkupLines(false);
397 void cmRST::ProcessDirectiveReplace()
399 // Record markup lines as replacement text.
400 std::string
& replacement
= this->Replace
[this->ReplaceName
];
401 replacement
+= cmJoin(this->MarkupLines
, " ");
402 this->ReplaceName
.clear();
405 void cmRST::ProcessDirectiveTocTree()
407 // Process documents referenced by toctree directive.
408 for (std::string
const& line
: this->MarkupLines
) {
409 if (!line
.empty() && line
[0] != ':') {
410 if (this->TocTreeLink
.find(line
)) {
411 std::string
const& link
= this->TocTreeLink
.match(1);
412 this->ProcessInclude(link
+ ".rst", Include::TocTree
);
414 this->ProcessInclude(line
+ ".rst", Include::TocTree
);
420 void cmRST::UnindentLines(std::vector
<std::string
>& lines
)
422 // Remove the common indentation from the second and later lines.
423 std::string indentText
;
424 std::string::size_type indentEnd
= 0;
426 for (size_t i
= 1; i
< lines
.size(); ++i
) {
427 std::string
const& line
= lines
[i
];
429 // Do not consider empty lines.
434 // Record indentation on first non-empty line.
437 indentEnd
= line
.find_first_not_of(" \t");
438 indentText
= line
.substr(0, indentEnd
);
442 // Truncate indentation to match that on this line.
443 indentEnd
= std::min(indentEnd
, line
.size());
444 for (std::string::size_type j
= 0; j
!= indentEnd
; ++j
) {
445 if (line
[j
] != indentText
[j
]) {
452 // Update second and later lines.
453 for (size_t i
= 1; i
< lines
.size(); ++i
) {
454 std::string
& line
= lines
[i
];
456 line
= line
.substr(indentEnd
);
460 auto it
= lines
.cbegin();
461 size_t leadingEmpty
= std::distance(it
, cmFindNot(lines
, std::string()));
463 auto rit
= lines
.crbegin();
464 size_t trailingEmpty
=
465 std::distance(rit
, cmFindNot(cmReverseRange(lines
), std::string()));
467 if ((leadingEmpty
+ trailingEmpty
) >= lines
.size()) {
468 // All lines are empty. The markup block is empty. Leave only one.
473 auto contentEnd
= cmRotate(lines
.begin(), lines
.begin() + leadingEmpty
,
474 lines
.end() - trailingEmpty
);
475 lines
.erase(contentEnd
, lines
.end());