Structural markdown representation (#162)
This commit is contained in:
249
src/Support/Doxygen.cpp
Normal file
249
src/Support/Doxygen.cpp
Normal file
@@ -0,0 +1,249 @@
|
||||
#include "Support/Doxygen.h"
|
||||
#include "llvm/Support/raw_ostream.h"
|
||||
#include "llvm/ADT/StringSwitch.h"
|
||||
#include "llvm/ADT/StringExtras.h"
|
||||
|
||||
namespace clice {
|
||||
void DoxygenInfo::add_block_command_comment(llvm::StringRef tag, llvm::StringRef content) {
|
||||
auto [it, _] = block_command_comments.try_emplace(tag);
|
||||
it->second.emplace_back(content.str());
|
||||
}
|
||||
|
||||
void DoxygenInfo::add_param_command_comment(
|
||||
llvm::StringRef name,
|
||||
llvm::StringRef content,
|
||||
DoxygenInfo::ParamCommandCommentContent::ParamDirection direction) {
|
||||
auto [it, not_exist] = param_command_comments.try_emplace(name);
|
||||
if(not_exist) {
|
||||
it->second.content = content;
|
||||
it->second.direction = direction;
|
||||
} else {
|
||||
// Merge the info as doxygen does
|
||||
if(it->second.direction == ParamCommandCommentContent::ParamDirection::Unspecified &&
|
||||
direction != ParamCommandCommentContent::ParamDirection::Unspecified) {
|
||||
// Update the direction if not assigned
|
||||
it->second.direction = direction;
|
||||
}
|
||||
it->second.content += "\n";
|
||||
it->second.content += content;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<DoxygenInfo::ParamCommandCommentContent*>
|
||||
DoxygenInfo::find_param_info(llvm::StringRef name) {
|
||||
if(auto it = param_command_comments.find_as(name); it != param_command_comments.end()) {
|
||||
return &it->getSecond();
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<std::pair<llvm::StringRef, llvm::ArrayRef<DoxygenInfo::BlockCommandCommentContent>>>
|
||||
DoxygenInfo::get_block_command_comments() {
|
||||
std::vector<std::pair<llvm::StringRef, llvm::ArrayRef<DoxygenInfo::BlockCommandCommentContent>>>
|
||||
res{};
|
||||
for(auto& [tag, content]: block_command_comments) {
|
||||
auto& pair = res.emplace_back();
|
||||
pair.first = tag;
|
||||
pair.second = content;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/// Process inline commands, we only interested in `\b` (bold), `\e` (italic) and `\c` (inline code)
|
||||
///
|
||||
/// \param line The line
|
||||
/// \param result Where should we output the result to
|
||||
static void process_non_command_line(llvm::StringRef line, llvm::raw_ostream& result) {
|
||||
while(!line.empty()) {
|
||||
auto pos = line.find_first_of("\\@");
|
||||
if(pos == llvm::StringRef::npos || pos == line.size()) {
|
||||
result << line;
|
||||
break;
|
||||
}
|
||||
result << line.take_front(pos);
|
||||
line = line.drop_front(pos);
|
||||
if(line.size() <= 4) {
|
||||
// shorter than `@b x`
|
||||
result << line;
|
||||
break;
|
||||
}
|
||||
|
||||
char opt = line[1];
|
||||
if(!llvm::isSpace(line[2])) {
|
||||
// Not an inline command, output as is
|
||||
result << line.take_front(2);
|
||||
line = line.drop_front(2);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip spaces
|
||||
size_t word_left = line.find_first_not_of(" \t\v\f\r", 2);
|
||||
if(word_left == llvm::StringRef::npos) {
|
||||
result << line;
|
||||
break;
|
||||
}
|
||||
|
||||
word_left -= 2;
|
||||
// adjust relative to current line
|
||||
llvm::StringRef rest = line.drop_front(word_left + 2);
|
||||
size_t word_end = rest.find_first_of(" \t\v\f\r");
|
||||
if(word_end == llvm::StringRef::npos)
|
||||
word_end = rest.size();
|
||||
|
||||
llvm::StringRef word = rest.take_front(word_end);
|
||||
line = rest.drop_front(word_end);
|
||||
|
||||
if(word.empty()) {
|
||||
result << line;
|
||||
break;
|
||||
}
|
||||
|
||||
switch(opt) {
|
||||
case 'b': result << "**" << word << "**"; break;
|
||||
case 'e': result << '*' << word << '*'; break;
|
||||
case 'c': result << '`' << word << '`'; break;
|
||||
default: result << '\\' << opt << ' ' << word; break;
|
||||
}
|
||||
}
|
||||
result << '\n';
|
||||
}
|
||||
|
||||
/// Always returns the referense of next line after this paragragh
|
||||
static void process_paragragh(llvm::SmallVector<llvm::StringRef>::iterator& line_ref,
|
||||
const llvm::SmallVector<llvm::StringRef>::iterator& end,
|
||||
DoxygenInfo& di,
|
||||
llvm::raw_ostream& rest) {
|
||||
auto consume_command_block = [&line_ref, &end](llvm::raw_ostream& os) {
|
||||
while(++line_ref != end) {
|
||||
if(auto trimed = line_ref->trim();
|
||||
trimed.empty() || trimed.starts_with('@') || trimed.starts_with('\\')) {
|
||||
// Empty line or next command
|
||||
if(trimed.empty()) {
|
||||
++line_ref;
|
||||
}
|
||||
break;
|
||||
}
|
||||
process_non_command_line(*line_ref, os);
|
||||
}
|
||||
};
|
||||
|
||||
if(auto trimed = line_ref->trim();
|
||||
!trimed.empty() && (trimed.starts_with('@') || trimed.starts_with('\\'))) {
|
||||
// Maybe a doxygen command
|
||||
auto command_end = trimed.find_first_of(" \t\v\f\r[");
|
||||
llvm::StringRef command, rest_of_line;
|
||||
if(command_end == trimed.npos) {
|
||||
command = trimed.substr(1);
|
||||
rest_of_line = "";
|
||||
} else {
|
||||
command = trimed.slice(1, command_end);
|
||||
rest_of_line = trimed.drop_front(command_end);
|
||||
}
|
||||
|
||||
if(command.equals_insensitive("param")) {
|
||||
// Got param command
|
||||
auto direction = DoxygenInfo::ParamCommandCommentContent::ParamDirection::Unspecified;
|
||||
llvm::StringRef param_name;
|
||||
|
||||
if(!rest_of_line.empty()) {
|
||||
if(rest_of_line.starts_with('[')) {
|
||||
// Parse direction
|
||||
auto close_bracket = rest_of_line.find(']');
|
||||
if(close_bracket != rest_of_line.npos) {
|
||||
auto param_direction = rest_of_line.slice(1, close_bracket);
|
||||
rest_of_line = rest_of_line.substr(close_bracket + 1);
|
||||
direction =
|
||||
llvm::StringSwitch<
|
||||
DoxygenInfo::ParamCommandCommentContent::ParamDirection>(
|
||||
param_direction)
|
||||
.CaseLower(
|
||||
"in",
|
||||
DoxygenInfo::ParamCommandCommentContent::ParamDirection::In)
|
||||
.CaseLower(
|
||||
"out",
|
||||
DoxygenInfo::ParamCommandCommentContent::ParamDirection::Out)
|
||||
.CaseLower(
|
||||
"in,out",
|
||||
DoxygenInfo::ParamCommandCommentContent::ParamDirection::InOut)
|
||||
.Default(DoxygenInfo::ParamCommandCommentContent::ParamDirection::
|
||||
Unspecified);
|
||||
} else {
|
||||
// not a closed '[', treat as normal line
|
||||
process_non_command_line(*line_ref, rest);
|
||||
++line_ref;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Parse name
|
||||
rest_of_line = rest_of_line.ltrim(" \t\v\f\r");
|
||||
if(rest_of_line.empty()) {
|
||||
// Not a legal line, cannot find name
|
||||
++line_ref;
|
||||
return;
|
||||
}
|
||||
auto name_end = rest_of_line.find_first_of(" \t\v\f\r");
|
||||
if(name_end == llvm::StringRef::npos) {
|
||||
param_name = rest_of_line;
|
||||
rest_of_line = "";
|
||||
} else {
|
||||
param_name = rest_of_line.slice(0, name_end);
|
||||
rest_of_line = rest_of_line.drop_front(name_end);
|
||||
}
|
||||
|
||||
// Parse rest of the block
|
||||
std::string s;
|
||||
llvm::raw_string_ostream this_comment_content{s};
|
||||
if(!rest_of_line.empty()) {
|
||||
this_comment_content << rest_of_line << '\n';
|
||||
}
|
||||
consume_command_block(this_comment_content);
|
||||
di.add_param_command_comment(param_name, this_comment_content.str(), direction);
|
||||
return;
|
||||
}
|
||||
|
||||
// line of '@param' only is illegal, escape.
|
||||
++line_ref;
|
||||
return;
|
||||
|
||||
} else if(command.equals_insensitive("return")) {
|
||||
// Got return command
|
||||
std::string s;
|
||||
llvm::raw_string_ostream this_comment_content{s};
|
||||
if(!rest_of_line.empty()) {
|
||||
this_comment_content << rest_of_line << '\n';
|
||||
}
|
||||
consume_command_block(this_comment_content);
|
||||
di.add_return_info(this_comment_content.str());
|
||||
return;
|
||||
} else {
|
||||
// Got normal commands
|
||||
std::string s;
|
||||
llvm::raw_string_ostream this_comment_content{s};
|
||||
if(!rest_of_line.empty()) {
|
||||
this_comment_content << rest_of_line << '\n';
|
||||
}
|
||||
consume_command_block(this_comment_content);
|
||||
// Now add to doxygen info and return
|
||||
di.add_block_command_comment(command, this_comment_content.str());
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Not a command block, but may include commands like '@b', '@e'
|
||||
process_non_command_line(*line_ref, rest);
|
||||
++line_ref;
|
||||
}
|
||||
|
||||
std::pair<DoxygenInfo, std::string> strip_doxygen_info(llvm::StringRef raw_comment) {
|
||||
DoxygenInfo di;
|
||||
std::string s;
|
||||
llvm::raw_string_ostream os{s};
|
||||
llvm::SmallVector<llvm::StringRef> lines;
|
||||
raw_comment.split(lines, "\n");
|
||||
// '\n' is not included in each line
|
||||
auto line_ref = lines.begin();
|
||||
while(line_ref != lines.end()) {
|
||||
process_paragragh(line_ref, lines.end(), di, os);
|
||||
}
|
||||
return {di, os.str()};
|
||||
}
|
||||
} // namespace clice
|
||||
216
src/Support/StructedText.cpp
Normal file
216
src/Support/StructedText.cpp
Normal file
@@ -0,0 +1,216 @@
|
||||
#include "Support/StructedText.h"
|
||||
|
||||
#include "llvm/ADT/ArrayRef.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
|
||||
namespace clice {
|
||||
|
||||
std::string Block::as_markdown() const {
|
||||
std::string md;
|
||||
llvm::raw_string_ostream os(md);
|
||||
render_markdown(os);
|
||||
return llvm::StringRef(os.str()).trim().str();
|
||||
}
|
||||
|
||||
BulletList::BulletList() = default;
|
||||
BulletList::~BulletList() = default;
|
||||
|
||||
std::unique_ptr<Block> BulletList::clone() const {
|
||||
return std::make_unique<BulletList>(*this);
|
||||
}
|
||||
|
||||
void BulletList::render_markdown(llvm::raw_ostream& os) const {
|
||||
for(auto& item: items) {
|
||||
os << "- " << item.as_markdown() << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
StructedText& BulletList::add_item() {
|
||||
return items.emplace_back();
|
||||
}
|
||||
|
||||
// Clangd inserts escape char '\' before '*', '-' and other markdown markers
|
||||
// That causes markdown comments are escaped and cannot be rendered properly
|
||||
// on editors
|
||||
// We do nothing on it. All the left comments are regarded as markdown rather
|
||||
// than plain text
|
||||
void Paragraph::render_markdown(llvm::raw_ostream& os) const {
|
||||
bool need_space = false;
|
||||
bool has_chunks = false;
|
||||
for(auto& chunk: chunks) {
|
||||
if(chunk.space_ahead || need_space) {
|
||||
os << ' ';
|
||||
}
|
||||
switch(chunk.kind) {
|
||||
case Kind::Bold: {
|
||||
os << "**" << chunk.content << "**";
|
||||
break;
|
||||
}
|
||||
case Kind::Italic: {
|
||||
os << '*' << chunk.content << '*';
|
||||
break;
|
||||
}
|
||||
case Kind::InlineCode: {
|
||||
os << '`' << chunk.content << '`';
|
||||
break;
|
||||
}
|
||||
case Kind::Strikethough: {
|
||||
os << "~~" << chunk.content << "~~";
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// Kind::PlainText
|
||||
os << chunk.content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
has_chunks = true;
|
||||
need_space = chunk.space_after;
|
||||
}
|
||||
}
|
||||
|
||||
Paragraph& Paragraph::append_text(std::string text, Kind kind) {
|
||||
if(kind == Kind::PlainText) {
|
||||
llvm::StringRef s{text};
|
||||
// s = s.trim(" \t\v\f\r");
|
||||
if(s.empty()) {
|
||||
return *this;
|
||||
}
|
||||
bool flag = !chunks.empty() && !std::isspace(chunks.back().content.back());
|
||||
auto& chunk = chunks.emplace_back();
|
||||
chunk.kind = Kind::PlainText;
|
||||
chunk.content = std::move(s.str());
|
||||
chunk.space_ahead = flag;
|
||||
chunk.space_after = !std::isspace(s.back());
|
||||
} else {
|
||||
bool flag = !chunks.empty() && chunks.back().kind != Kind::PlainText;
|
||||
auto& chunk = chunks.emplace_back();
|
||||
chunk.kind = kind;
|
||||
chunk.content = std::move(text);
|
||||
chunk.space_ahead = flag;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
Paragraph& Paragraph::append_newline_char(unsigned cnt) {
|
||||
auto& chunk = chunks.emplace_back();
|
||||
chunk.kind = Kind::PlainText;
|
||||
chunk.content = std::string(cnt, '\n');
|
||||
return *this;
|
||||
}
|
||||
|
||||
class Heading : public Paragraph {
|
||||
public:
|
||||
Heading(unsigned level) : level(level) {}
|
||||
|
||||
void render_markdown(llvm::raw_ostream& os) const override {
|
||||
os << std::string(level, '#') << ' ';
|
||||
Paragraph::render_markdown(os);
|
||||
}
|
||||
|
||||
private:
|
||||
unsigned level;
|
||||
};
|
||||
|
||||
class Ruler : public Block {
|
||||
public:
|
||||
void render_markdown(llvm::raw_ostream& os) const override {
|
||||
os << "\n---\n";
|
||||
}
|
||||
|
||||
bool is_ruler() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<Block> clone() const override {
|
||||
return std::make_unique<Ruler>(*this);
|
||||
}
|
||||
};
|
||||
|
||||
class CodeBlock : public Block {
|
||||
public:
|
||||
void render_markdown(llvm::raw_ostream& os) const override {
|
||||
os << "```" << lang << '\n' << code << "```\n";
|
||||
}
|
||||
|
||||
std::unique_ptr<Block> clone() const override {
|
||||
return std::make_unique<CodeBlock>(*this);
|
||||
}
|
||||
|
||||
CodeBlock(std::string code, std::string lang = "") :
|
||||
code(std::move(code)), lang(std::move(lang)) {};
|
||||
|
||||
private:
|
||||
std::string lang;
|
||||
std::string code;
|
||||
};
|
||||
|
||||
static std::string render_blocks(llvm::ArrayRef<std::unique_ptr<Block>> blocks) {
|
||||
std::string md;
|
||||
llvm::raw_string_ostream os(md);
|
||||
|
||||
// Trim rulers.
|
||||
blocks = blocks.drop_while([](const std::unique_ptr<Block>& C) { return C->is_ruler(); });
|
||||
auto last = llvm::find_if(llvm::reverse(blocks),
|
||||
[](const std::unique_ptr<Block>& C) { return !C->is_ruler(); });
|
||||
blocks = blocks.drop_back(blocks.end() - last.base());
|
||||
|
||||
bool last_block_was_ruler = true;
|
||||
// render
|
||||
for(const auto& b: blocks) {
|
||||
if(b->is_ruler() && last_block_was_ruler) {
|
||||
continue;
|
||||
}
|
||||
last_block_was_ruler = b->is_ruler();
|
||||
b->render_markdown(os);
|
||||
}
|
||||
|
||||
// Get rid of redundant empty lines introduced in plaintext while imitating
|
||||
// padding in markdown.
|
||||
std::string adjusted_result;
|
||||
llvm::StringRef trimmed_text(os.str());
|
||||
trimmed_text = trimmed_text.trim(" \t\v\f\r");
|
||||
|
||||
llvm::copy_if(trimmed_text,
|
||||
std::back_inserter(adjusted_result),
|
||||
[&trimmed_text](const char& C) {
|
||||
return !llvm::StringRef(trimmed_text.data(), &C - trimmed_text.data() + 1)
|
||||
// We allow at most two newlines.
|
||||
.ends_with("\n\n\n");
|
||||
});
|
||||
|
||||
return adjusted_result;
|
||||
}
|
||||
|
||||
void StructedText::append(StructedText& other) {
|
||||
std::move(other.blocks.begin(), other.blocks.end(), std::back_inserter(blocks));
|
||||
}
|
||||
|
||||
Paragraph& StructedText::add_paragraph() {
|
||||
blocks.emplace_back(std::make_unique<Paragraph>());
|
||||
return *static_cast<Paragraph*>(blocks.back().get());
|
||||
}
|
||||
|
||||
void StructedText::add_ruler() {
|
||||
blocks.push_back(std::make_unique<Ruler>());
|
||||
}
|
||||
|
||||
void StructedText::add_code_block(std::string code, std::string lang) {
|
||||
blocks.emplace_back(std::make_unique<CodeBlock>(std::move(code), std::move(lang)));
|
||||
}
|
||||
|
||||
Paragraph& StructedText::add_heading(unsigned level) {
|
||||
blocks.emplace_back(std::make_unique<Heading>(level));
|
||||
return *static_cast<Paragraph*>(blocks.back().get());
|
||||
}
|
||||
|
||||
BulletList& StructedText::add_bullet_list() {
|
||||
blocks.push_back(std::make_unique<BulletList>());
|
||||
return *static_cast<BulletList*>(blocks.back().get());
|
||||
}
|
||||
|
||||
std::string StructedText::as_markdown() const {
|
||||
return render_blocks(blocks);
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
Reference in New Issue
Block a user