| /* 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; | |
| } | |
| void cmDocumentationFormatter::PrintFormatted(std::ostream& os, | |
| std::string const& text) const | |
| { | |
| os << this->Format(text); | |
| } |