/src/CMake/Source/cmDocumentationFormatter.cxx
Line | Count | Source |
1 | | /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying |
2 | | file LICENSE.rst or https://cmake.org/licensing for details. */ |
3 | | #include "cmDocumentationFormatter.h" |
4 | | |
5 | | #include <algorithm> // IWYU pragma: keep |
6 | | #include <cassert> |
7 | | #include <iomanip> |
8 | | #include <iterator> |
9 | | #include <ostream> |
10 | | #include <string> |
11 | | #include <vector> |
12 | | |
13 | | #include <cm/string_view> |
14 | | #include <cmext/string_view> |
15 | | |
16 | | #include "cmDocumentationEntry.h" |
17 | | #include "cmDocumentationSection.h" |
18 | | #include "cmStringAlgorithms.h" |
19 | | |
20 | | namespace { |
21 | | auto const EOL = "\n"_s; |
22 | | auto const SPACE = " "_s; |
23 | | auto const TWO_SPACES = " "_s; |
24 | | auto const MAX_WIDTH_PADDING = |
25 | | std::string(cmDocumentationFormatter::TEXT_WIDTH, ' '); |
26 | | |
27 | | void FormatLine(std::back_insert_iterator<std::vector<cm::string_view>> outIt, |
28 | | cm::string_view const text, cm::string_view const padding) |
29 | 1 | { |
30 | 1 | auto tokens = cmTokenizedView(text, ' ', cmTokenizerMode::New); |
31 | 1 | if (tokens.empty()) { |
32 | 0 | return; |
33 | 0 | } |
34 | | |
35 | | // Push padding in front of a first line |
36 | 1 | if (!padding.empty()) { |
37 | 1 | outIt = padding; |
38 | 1 | } |
39 | | |
40 | 1 | auto currentWidth = padding.size(); |
41 | 1 | auto newSentence = false; |
42 | | |
43 | 12 | for (auto token : tokens) { |
44 | | // It's no need to add a space if this is a very first |
45 | | // word on a line. |
46 | 12 | auto const needSpace = currentWidth > padding.size(); |
47 | | // Evaluate the size of a current token + possibly spaces before it. |
48 | 12 | auto const tokenWithSpaceSize = token.size() + std::size_t(needSpace) + |
49 | 12 | std::size_t(needSpace && newSentence); |
50 | | // Check if a current word fits on a line. |
51 | | // Also, take in account: |
52 | | // - extra space if not a first word on a line |
53 | | // - extra space if last token ends w/ a period |
54 | 12 | if (currentWidth + tokenWithSpaceSize <= |
55 | 12 | cmDocumentationFormatter::TEXT_WIDTH) { |
56 | | // If not a first word on a line... |
57 | 12 | if (needSpace) { |
58 | | // ... add a space after the last token + |
59 | | // possibly one more space if the last token |
60 | | // ends with a period (means, end of a sentence). |
61 | 11 | outIt = newSentence ? TWO_SPACES : SPACE; |
62 | 11 | } |
63 | 12 | outIt = token; |
64 | 12 | currentWidth += tokenWithSpaceSize; |
65 | 12 | } else { |
66 | | // Start a new line! |
67 | 0 | outIt = EOL; |
68 | 0 | if (!padding.empty()) { |
69 | 0 | outIt = padding; |
70 | 0 | } |
71 | 0 | outIt = token; |
72 | 0 | currentWidth = padding.size() + token.size(); |
73 | 0 | } |
74 | | |
75 | | // Start a new sentence if the current word ends with period |
76 | 12 | newSentence = token.back() == '.'; |
77 | 12 | } |
78 | | // Always add EOL at the end of formatted text |
79 | 1 | outIt = EOL; |
80 | 1 | } |
81 | | } // anonymous namespace |
82 | | |
83 | | std::string cmDocumentationFormatter::Format(cm::string_view text) const |
84 | 1 | { |
85 | | // Exit early on empty text |
86 | 1 | if (text.empty()) { |
87 | 0 | return {}; |
88 | 0 | } |
89 | | |
90 | 1 | assert(this->TextIndent < this->TEXT_WIDTH); |
91 | | |
92 | 1 | auto const padding = |
93 | 1 | cm::string_view(MAX_WIDTH_PADDING.c_str(), this->TextIndent); |
94 | | |
95 | 1 | std::vector<cm::string_view> tokens; |
96 | 1 | auto outIt = std::back_inserter(tokens); |
97 | 1 | auto prevWasPreFormatted = false; |
98 | | |
99 | | // NOTE Can't use `cmTokenizedView()` cuz every sequential EOL does matter |
100 | | // (and `cmTokenizedView()` will squeeze 'em) |
101 | 1 | for ( // clang-format off |
102 | 1 | std::string::size_type start = 0 |
103 | 1 | , end = text.find('\n') |
104 | 2 | ; start < text.size() |
105 | 1 | ; start = end + ((end != std::string::npos) ? 1 : 0) |
106 | 1 | , end = text.find('\n', start) |
107 | 1 | ) // clang-format on |
108 | 1 | { |
109 | 1 | auto const isLastLine = end == std::string::npos; |
110 | 1 | auto const line = |
111 | 1 | isLastLine ? text.substr(start) : text.substr(start, end - start); |
112 | | |
113 | 1 | if (!line.empty() && line.front() == ' ') { |
114 | | // Preformatted lines go as is w/ a leading padding |
115 | 0 | if (!padding.empty()) { |
116 | 0 | outIt = padding; |
117 | 0 | } |
118 | 0 | outIt = line; |
119 | 0 | prevWasPreFormatted = true; |
120 | 1 | } else { |
121 | | // Separate a normal paragraph from a pre-formatted |
122 | | // w/ an extra EOL |
123 | 1 | if (prevWasPreFormatted) { |
124 | 0 | outIt = EOL; |
125 | 0 | } |
126 | 1 | if (line.empty()) { |
127 | 0 | if (!isLastLine) { |
128 | 0 | outIt = EOL; |
129 | 0 | } |
130 | 1 | } else { |
131 | 1 | FormatLine(outIt, line, padding); |
132 | 1 | } |
133 | 1 | prevWasPreFormatted = false; |
134 | 1 | } |
135 | 1 | if (!isLastLine) { |
136 | 0 | outIt = EOL; |
137 | 0 | } |
138 | 1 | } |
139 | | |
140 | 1 | if (prevWasPreFormatted) { |
141 | 0 | outIt = EOL; |
142 | 0 | } |
143 | | |
144 | 1 | return cmJoinStrings(tokens, {}, {}); |
145 | 1 | } |
146 | | |
147 | | void cmDocumentationFormatter::PrintSection( |
148 | | std::ostream& os, cmDocumentationSection const& section) |
149 | 0 | { |
150 | 0 | std::size_t const PREFIX_SIZE = |
151 | 0 | sizeof(cmDocumentationEntry::CustomNamePrefix) + 1u; |
152 | | // length of the "= " literal (see below) |
153 | 0 | std::size_t const SUFFIX_SIZE = 2u; |
154 | | // legacy magic number ;-) |
155 | 0 | std::size_t const NAME_SIZE = 29u; |
156 | |
|
157 | 0 | std::size_t const PADDING_SIZE = PREFIX_SIZE + SUFFIX_SIZE; |
158 | 0 | std::size_t const TITLE_SIZE = NAME_SIZE + PADDING_SIZE; |
159 | |
|
160 | 0 | auto const savedIndent = this->TextIndent; |
161 | |
|
162 | 0 | os << section.GetName() << '\n'; |
163 | |
|
164 | 0 | for (cmDocumentationEntry const& entry : section.GetEntries()) { |
165 | 0 | if (!entry.Name.empty()) { |
166 | 0 | this->TextIndent = TITLE_SIZE; |
167 | 0 | os << std::setw(PREFIX_SIZE) << std::left << entry.CustomNamePrefix |
168 | 0 | << std::setw(int(std::max(NAME_SIZE, entry.Name.size()))) |
169 | 0 | << entry.Name; |
170 | 0 | if (entry.Name.size() > NAME_SIZE) { |
171 | 0 | os << '\n' << std::setw(int(this->TextIndent - PREFIX_SIZE)) << ' '; |
172 | 0 | } |
173 | 0 | os << "= " << this->Format(entry.Brief).substr(this->TextIndent); |
174 | 0 | } else { |
175 | 0 | this->TextIndent = 0u; |
176 | 0 | os << '\n' << this->Format(entry.Brief); |
177 | 0 | } |
178 | 0 | } |
179 | |
|
180 | 0 | os << '\n'; |
181 | |
|
182 | 0 | this->TextIndent = savedIndent; |
183 | 0 | } |
184 | | |
185 | | void cmDocumentationFormatter::PrintFormatted(std::ostream& os, |
186 | | std::string const& text) const |
187 | 1 | { |
188 | 1 | os << this->Format(text); |
189 | 1 | } |