/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying file Copyright.txt or https://cmake.org/licensing for details. */ #include "cmDocumentationFormatter.h" #include // IWYU pragma: keep #include #include #include #include #include #include #include #include #include "cmDocumentationEntry.h" #include "cmDocumentationSection.h" #include "cmStringAlgorithms.h" namespace { const auto EOL = "\n"_s; const auto SPACE = " "_s; const auto TWO_SPACES = " "_s; const auto MAX_WIDTH_PADDING = std::string(cmDocumentationFormatter::TEXT_WIDTH, ' '); void FormatLine(std::back_insert_iterator> outIt, const cm::string_view text, const cm::string_view 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. const auto needSpace = currentWidth > padding.size(); // Evaluate the size of a current token + possibly spaces before it. const auto 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(std::string text) const { // Exit early on empty text if (text.empty()) { return {}; } assert(this->TextIndent < this->TEXT_WIDTH); const auto padding = cm::string_view(MAX_WIDTH_PADDING.c_str(), this->TextIndent); std::vector 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 { const auto isLastLine = end == std::string::npos; const auto line = isLastLine ? cm::string_view{ text.c_str() + start } : cm::string_view{ text.c_str() + 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) { const std::size_t PREFIX_SIZE = sizeof(cmDocumentationEntry::CustomNamePrefix) + 1u; // length of the "= " literal (see below) const std::size_t SUFFIX_SIZE = 2u; // legacy magic number ;-) const std::size_t NAME_SIZE = 29u; const std::size_t PADDING_SIZE = PREFIX_SIZE + SUFFIX_SIZE; const std::size_t TITLE_SIZE = NAME_SIZE + PADDING_SIZE; const auto 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; }