| /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying |
| file LICENSE.rst or https://cmake.org/licensing for details. */ |
| #include "cmDocumentationFormatter.h" |
| |
| #include <algorithm> // IWYU pragma: keep |
| #include <cassert> |
| #include <iomanip> |
| #include <iterator> |
| #include <ostream> |
| #include <string> |
| #include <vector> |
| |
| #include <cm/string_view> |
| #include <cmext/string_view> |
| |
| #include "cmDocumentationEntry.h" |
| #include "cmDocumentationSection.h" |
| #include "cmStringAlgorithms.h" |
| |
| namespace { |
| auto const EOL = "\n"_s; |
| auto const SPACE = " "_s; |
| auto const TWO_SPACES = " "_s; |
| auto const MAX_WIDTH_PADDING = |
| std::string(cmDocumentationFormatter::TEXT_WIDTH, ' '); |
| |
| void FormatLine(std::back_insert_iterator<std::vector<cm::string_view>> outIt, |
| cm::string_view const text, cm::string_view const padding) |
| { |
| auto tokens = cmTokenizedView(text, ' ', cmTokenizerMode::New); |
| if (tokens.empty()) { |
| return; |
| } |
| |
| // Push padding in front of a first line |
| if (!padding.empty()) { |
| outIt = padding; |
| } |
| |
| auto currentWidth = padding.size(); |
| auto newSentence = false; |
| |
| for (auto token : tokens) { |
| // It's no need to add a space if this is a very first |
| // word on a line. |
| auto const needSpace = currentWidth > padding.size(); |
| // Evaluate the size of a current token + possibly spaces before it. |
| auto const tokenWithSpaceSize = token.size() + std::size_t(needSpace) + |
| std::size_t(needSpace && newSentence); |
| // Check if a current word fits on a line. |
| // Also, take in account: |
| // - extra space if not a first word on a line |
| // - extra space if last token ends w/ a period |
| if (currentWidth + tokenWithSpaceSize <= |
| cmDocumentationFormatter::TEXT_WIDTH) { |
| // If not a first word on a line... |
| if (needSpace) { |
| // ... add a space after the last token + |
| // possibly one more space if the last token |
| // ends with a period (means, end of a sentence). |
| outIt = newSentence ? TWO_SPACES : SPACE; |
| } |
| outIt = token; |
| currentWidth += tokenWithSpaceSize; |
| } else { |
| // Start a new line! |
| outIt = EOL; |
| if (!padding.empty()) { |
| outIt = padding; |
| } |
| outIt = token; |
| currentWidth = padding.size() + token.size(); |
| } |
| |
| // Start a new sentence if the current word ends with period |
| newSentence = token.back() == '.'; |
| } |
| // Always add EOL at the end of formatted text |
| outIt = EOL; |
| } |
| } // anonymous namespace |
| |
| std::string cmDocumentationFormatter::Format(cm::string_view text) const |
| { |
| // Exit early on empty text |
| if (text.empty()) { |
| return {}; |
| } |
| |
| assert(this->TextIndent < this->TEXT_WIDTH); |
| |
| auto const padding = |
| cm::string_view(MAX_WIDTH_PADDING.c_str(), this->TextIndent); |
| |
| std::vector<cm::string_view> tokens; |
| auto outIt = std::back_inserter(tokens); |
| auto prevWasPreFormatted = false; |
| |
| // NOTE Can't use `cmTokenizedView()` cuz every sequential EOL does matter |
| // (and `cmTokenizedView()` will squeeze 'em) |
| for ( // clang-format off |
| std::string::size_type start = 0 |
| , end = text.find('\n') |
| ; start < text.size() |
| ; start = end + ((end != std::string::npos) ? 1 : 0) |
| , end = text.find('\n', start) |
| ) // clang-format on |
| { |
| auto const isLastLine = end == std::string::npos; |
| auto const line = |
| isLastLine ? text.substr(start) : text.substr(start, end - start); |
| |
| if (!line.empty() && line.front() == ' ') { |
| // Preformatted lines go as is w/ a leading padding |
| if (!padding.empty()) { |
| outIt = padding; |
| } |
| outIt = line; |
| prevWasPreFormatted = true; |
| } else { |
| // Separate a normal paragraph from a pre-formatted |
| // w/ an extra EOL |
| if (prevWasPreFormatted) { |
| outIt = EOL; |
| } |
| if (line.empty()) { |
| if (!isLastLine) { |
| outIt = EOL; |
| } |
| } else { |
| FormatLine(outIt, line, padding); |
| } |
| prevWasPreFormatted = false; |
| } |
| if (!isLastLine) { |
| outIt = EOL; |
| } |
| } |
| |
| if (prevWasPreFormatted) { |
| outIt = EOL; |
| } |
| |
| return cmJoinStrings(tokens, {}, {}); |
| } |
| |
| void cmDocumentationFormatter::PrintSection( |
| std::ostream& os, cmDocumentationSection const& section) |
| { |
| std::size_t const PREFIX_SIZE = |
| sizeof(cmDocumentationEntry::CustomNamePrefix) + 1u; |
| // length of the "= " literal (see below) |
| std::size_t const SUFFIX_SIZE = 2u; |
| // legacy magic number ;-) |
| std::size_t const NAME_SIZE = 29u; |
| |
| std::size_t const PADDING_SIZE = PREFIX_SIZE + SUFFIX_SIZE; |
| std::size_t const TITLE_SIZE = NAME_SIZE + PADDING_SIZE; |
| |
| auto const savedIndent = this->TextIndent; |
| |
| os << section.GetName() << '\n'; |
| |
| for (cmDocumentationEntry const& entry : section.GetEntries()) { |
| if (!entry.Name.empty()) { |
| this->TextIndent = TITLE_SIZE; |
| os << std::setw(PREFIX_SIZE) << std::left << entry.CustomNamePrefix |
| << std::setw(int(std::max(NAME_SIZE, entry.Name.size()))) |
| << entry.Name; |
| if (entry.Name.size() > NAME_SIZE) { |
| os << '\n' << std::setw(int(this->TextIndent - PREFIX_SIZE)) << ' '; |
| } |
| os << "= " << this->Format(entry.Brief).substr(this->TextIndent); |
| } else { |
| this->TextIndent = 0u; |
| os << '\n' << this->Format(entry.Brief); |
| } |
| } |
| |
| os << '\n'; |
| |
| this->TextIndent = savedIndent; |
| } |