Structural markdown representation (#162)

This commit is contained in:
qingfengzl
2025-08-11 22:43:48 +08:00
committed by GitHub
parent 31fca83788
commit ce30acd8d3
7 changed files with 1073 additions and 1 deletions

249
src/Support/Doxygen.cpp Normal file
View 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

View 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