forked from OSchip/llvm-project
200 lines
6.0 KiB
C++
200 lines
6.0 KiB
C++
//===--- 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
|