| //===--- FormattedString.cpp --------------------------------*- C++-*------===// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| #include "FormattedString.h" |
| #include "clang/Basic/CharInfo.h" |
| #include "llvm/ADT/StringRef.h" |
| #include "llvm/Support/ErrorHandling.h" |
| #include "llvm/Support/FormatVariadic.h" |
| #include <cstddef> |
| #include <string> |
| |
| namespace clang { |
| namespace clangd { |
| |
| namespace { |
| /// Escape a markdown text block. Ensures the punctuation will not introduce |
| /// any of the markdown constructs. |
| static std::string renderText(llvm::StringRef Input) { |
| // Escaping ASCII punctiation ensures we can't start a markdown construct. |
| constexpr llvm::StringLiteral Punctuation = |
| R"txt(!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~)txt"; |
| |
| std::string R; |
| for (size_t From = 0; From < Input.size();) { |
| size_t Next = Input.find_first_of(Punctuation, From); |
| R += Input.substr(From, Next - From); |
| if (Next == llvm::StringRef::npos) |
| break; |
| R += "\\"; |
| R += Input[Next]; |
| |
| From = Next + 1; |
| } |
| return R; |
| } |
| |
| /// Renders \p Input as an inline block of code in markdown. The returned value |
| /// is surrounded by backticks and the inner contents are properly escaped. |
| static std::string renderInlineBlock(llvm::StringRef Input) { |
| std::string R; |
| // Double all backticks to make sure we don't close the inline block early. |
| for (size_t From = 0; From < Input.size();) { |
| size_t Next = Input.find("`", From); |
| R += Input.substr(From, Next - From); |
| if (Next == llvm::StringRef::npos) |
| break; |
| R += "``"; // double the found backtick. |
| |
| From = Next + 1; |
| } |
| // If results starts with a backtick, add spaces on both sides. The spaces |
| // are ignored by markdown renderers. |
| if (llvm::StringRef(R).startswith("`") || llvm::StringRef(R).endswith("`")) |
| return "` " + std::move(R) + " `"; |
| // Markdown render should ignore first and last space if both are there. We |
| // add an extra pair of spaces in that case to make sure we render what the |
| // user intended. |
| if (llvm::StringRef(R).startswith(" ") && llvm::StringRef(R).endswith(" ")) |
| return "` " + std::move(R) + " `"; |
| return "`" + std::move(R) + "`"; |
| } |
| /// Render \p Input as markdown code block with a specified \p Language. The |
| /// result is surrounded by >= 3 backticks. Although markdown also allows to use |
| /// '~' for code blocks, they are never used. |
| static std::string renderCodeBlock(llvm::StringRef Input, |
| llvm::StringRef Language) { |
| // Count the maximum number of consecutive backticks in \p Input. We need to |
| // start and end the code block with more. |
| unsigned MaxBackticks = 0; |
| unsigned Backticks = 0; |
| for (char C : Input) { |
| if (C == '`') { |
| ++Backticks; |
| continue; |
| } |
| MaxBackticks = std::max(MaxBackticks, Backticks); |
| Backticks = 0; |
| } |
| MaxBackticks = std::max(Backticks, MaxBackticks); |
| // Use the corresponding number of backticks to start and end a code block. |
| std::string BlockMarker(/*Repeat=*/std::max(3u, MaxBackticks + 1), '`'); |
| return BlockMarker + Language.str() + "\n" + Input.str() + "\n" + BlockMarker; |
| } |
| |
| } // namespace |
| |
| void FormattedString::appendText(std::string Text) { |
| Chunk C; |
| C.Kind = ChunkKind::PlainText; |
| C.Contents = Text; |
| Chunks.push_back(C); |
| } |
| |
| void FormattedString::appendCodeBlock(std::string Code, std::string Language) { |
| Chunk C; |
| C.Kind = ChunkKind::CodeBlock; |
| C.Contents = std::move(Code); |
| C.Language = std::move(Language); |
| Chunks.push_back(std::move(C)); |
| } |
| |
| void FormattedString::appendInlineCode(std::string Code) { |
| Chunk C; |
| C.Kind = ChunkKind::InlineCodeBlock; |
| C.Contents = std::move(Code); |
| Chunks.push_back(std::move(C)); |
| } |
| |
| std::string FormattedString::renderAsMarkdown() const { |
| std::string R; |
| auto EnsureWhitespace = [&R]() { |
| // Adds a space for nicer rendering. |
| if (!R.empty() && !isWhitespace(R.back())) |
| R += " "; |
| }; |
| for (const auto &C : Chunks) { |
| switch (C.Kind) { |
| case ChunkKind::PlainText: |
| if (!C.Contents.empty() && !isWhitespace(C.Contents.front())) |
| EnsureWhitespace(); |
| R += renderText(C.Contents); |
| continue; |
| case ChunkKind::InlineCodeBlock: |
| EnsureWhitespace(); |
| R += renderInlineBlock(C.Contents); |
| continue; |
| case ChunkKind::CodeBlock: |
| if (!R.empty() && !llvm::StringRef(R).endswith("\n")) |
| R += "\n"; |
| R += renderCodeBlock(C.Contents, C.Language); |
| R += "\n"; |
| continue; |
| } |
| llvm_unreachable("unhanlded ChunkKind"); |
| } |
| return R; |
| } |
| |
| std::string FormattedString::renderAsPlainText() const { |
| std::string R; |
| auto EnsureWhitespace = [&]() { |
| if (R.empty() || isWhitespace(R.back())) |
| return; |
| R += " "; |
| }; |
| Optional<bool> LastWasBlock; |
| for (const auto &C : Chunks) { |
| bool IsBlock = C.Kind == ChunkKind::CodeBlock; |
| if (LastWasBlock.hasValue() && (IsBlock || *LastWasBlock)) |
| R += "\n\n"; |
| LastWasBlock = IsBlock; |
| |
| switch (C.Kind) { |
| case ChunkKind::PlainText: |
| EnsureWhitespace(); |
| R += C.Contents; |
| break; |
| case ChunkKind::InlineCodeBlock: |
| EnsureWhitespace(); |
| R += C.Contents; |
| break; |
| case ChunkKind::CodeBlock: |
| R += C.Contents; |
| break; |
| } |
| // Trim trailing whitespace in chunk. |
| while (!R.empty() && isWhitespace(R.back())) |
| R.pop_back(); |
| } |
| return R; |
| } |
| |
| std::string FormattedString::renderForTests() const { |
| std::string R; |
| for (const auto &C : Chunks) { |
| switch (C.Kind) { |
| case ChunkKind::PlainText: |
| R += "text[" + C.Contents + "]"; |
| break; |
| case ChunkKind::InlineCodeBlock: |
| R += "code[" + C.Contents + "]"; |
| break; |
| case ChunkKind::CodeBlock: |
| if (!R.empty()) |
| R += "\n"; |
| R += llvm::formatv("codeblock({0}) [\n{1}\n]\n", C.Language, C.Contents); |
| break; |
| } |
| } |
| while (!R.empty() && isWhitespace(R.back())) |
| R.pop_back(); |
| return R; |
| } |
| } // namespace clangd |
| } // namespace clang |