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

View File

@@ -6,7 +6,15 @@
namespace clice::config {
struct HoverOptions {};
struct HoverOptions {
/// Strip doxygen info and merge with lsp info
bool enable_doxygen_parsing = true;
/// If set `false`, the comment will be wrapped
/// in code block and keep ascii typesetting
bool parse_comment_as_markdown = true;
/// Show sugar type
bool show_aka = true;
};
} // namespace clice::config

59
include/Support/Doxygen.h Normal file
View File

@@ -0,0 +1,59 @@
#pragma once
#include <string>
#include <memory>
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/ArrayRef.h"
namespace clice {
class DoxygenInfo {
public:
struct BlockCommandCommentContent {
std::string content;
};
struct ParamCommandCommentContent {
std::string content;
enum class ParamDirection : uint8_t { Unspecified, In, Out, InOut };
ParamDirection direction = ParamDirection::Unspecified;
};
void add_block_command_comment(llvm::StringRef tag, llvm::StringRef content);
void add_param_command_comment(llvm::StringRef name,
llvm::StringRef content,
ParamCommandCommentContent::ParamDirection direction =
ParamCommandCommentContent::ParamDirection::Unspecified);
/// \param ret_info docs for return value
/// \param cover if already has docs for return, new value cover the old one
void add_return_info(llvm::StringRef ret_info, bool cover = true) {
if(!doc_for_return.has_value() || cover) {
doc_for_return = ret_info.str();
}
}
std::optional<ParamCommandCommentContent*> find_param_info(llvm::StringRef name);
std::vector<std::pair<llvm::StringRef, llvm::ArrayRef<BlockCommandCommentContent>>>
get_block_command_comments();
std::optional<llvm::StringRef> get_return_info() {
return doc_for_return;
}
private:
llvm::SmallDenseMap<llvm::StringRef, std::vector<BlockCommandCommentContent>>
block_command_comments;
llvm::SmallDenseMap<llvm::StringRef, ParamCommandCommentContent> param_command_comments;
std::optional<std::string> doc_for_return;
};
/// Strip doxygen info from raw comment
/// \return `DoxygenInfo` and the rest comment
std::pair<DoxygenInfo, std::string> strip_doxygen_info(llvm::StringRef raw_comment);
} // namespace clice

View File

@@ -0,0 +1,108 @@
#pragma once
#include "llvm/Support/raw_os_ostream.h"
namespace clice {
/// Base class of structed text
class Block {
public:
virtual void render_markdown(llvm::raw_ostream& os) const = 0;
virtual std::unique_ptr<Block> clone() const = 0;
std::string as_markdown() const;
virtual bool is_ruler() const {
return false;
}
virtual ~Block() = default;
};
/// Normal text and inline code
class Paragraph : public Block {
public:
enum class Kind : uint8_t {
Bold,
Italic,
PlainText,
InlineCode,
Strikethough,
};
void render_markdown(llvm::raw_ostream& os) const override;
std::unique_ptr<Block> clone() const override {
return std::make_unique<Paragraph>(*this);
}
Paragraph& append_text(std::string text, Kind kind = Kind::PlainText);
Paragraph& append_newline_char(unsigned cnt = 1);
private:
struct Chunk {
Kind kind;
std::string content;
bool space_ahead = false;
bool space_after = false;
};
std::vector<Chunk> chunks;
};
class StructedText;
/// Allow nested structure
class BulletList : public Block {
public:
BulletList();
~BulletList();
void render_markdown(llvm::raw_ostream& os) const override;
std::unique_ptr<Block> clone() const override;
StructedText& add_item();
private:
std::vector<StructedText> items;
};
class StructedText {
public:
StructedText() = default;
StructedText(const StructedText& other) {
*this = other;
}
StructedText(StructedText&&) = default;
StructedText& operator= (const StructedText& other) {
blocks.clear();
for(auto& b: other.blocks) {
blocks.push_back(b->clone());
}
return *this;
}
StructedText& operator= (StructedText&&) = default;
void append(StructedText& doc);
Paragraph& add_paragraph();
void add_ruler();
void add_code_block(std::string code, std::string lang = "");
Paragraph& add_heading(unsigned level);
BulletList& add_bullet_list();
std::string as_markdown() const;
private:
std::vector<std::unique_ptr<Block>> blocks;
};
} // namespace clice

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

View File

@@ -0,0 +1,321 @@
#include "Test/Test.h"
#include "Support/Doxygen.h"
#include "Support/Format.h"
namespace clice::testing {
TEST(DoxygenSupport, DoxygenInfo) {
DoxygenInfo di;
di.add_param_command_comment("foo",
"Doc for foo",
DoxygenInfo::ParamCommandCommentContent::ParamDirection::In);
di.add_param_command_comment("bar",
"Doc for bar",
DoxygenInfo::ParamCommandCommentContent::ParamDirection::InOut);
di.add_param_command_comment("baz",
"Doc for baz",
DoxygenInfo::ParamCommandCommentContent::ParamDirection::Out);
auto param_foo = di.find_param_info("foo");
EXPECT_TRUE(param_foo.has_value() && param_foo.value()->content.compare("Doc for foo") == 0);
auto param_bar = di.find_param_info("bar");
EXPECT_TRUE(param_bar.has_value() && param_bar.value()->content.compare("Doc for bar") == 0);
auto param_non_exists = di.find_param_info("xxx");
EXPECT_FALSE(param_non_exists.has_value());
for(int i = 0; i < 3; ++i) {
di.add_block_command_comment("detail", std::format("Detail{}", i));
di.add_block_command_comment("warning", std::format("Warning{}", i));
di.add_block_command_comment("note", std::format("Note{}", i));
}
std::set<std::string> expected_detail = {"Detail0", "Detail1", "Detail2"};
std::set<std::string> expected_warning = {"Warning0", "Warning1", "Warning2"};
std::set<std::string> expected_note = {"Note0", "Note1", "Note2"};
std::map<std::string, std::set<std::string>> expected{
{"detail", std::move(expected_detail) },
{"warning", std::move(expected_warning)},
{"note", std::move(expected_note) },
};
auto bcc_list = di.get_block_command_comments();
for(auto& [tag, content]: bcc_list) {
std::set<std::string> actual;
for(auto& block: content) {
actual.insert(block.content);
}
EXPECT_TRUE(expected.contains(tag.str()));
EXPECT_TRUE(actual == expected[tag.str()]);
expected.erase(tag.str());
}
EXPECT_TRUE(expected.empty());
}
TEST(DoxygenSupport, DoxygenParserSimple) {
// Inline commands
{
constexpr auto raw_comment = R"(
This is a @b Bold word
This is an \e Italic word
This is @c InlineCode
)";
auto [di, md] = strip_doxygen_info(raw_comment);
EXPECT_TRUE(di.get_block_command_comments().size() == 0);
clice::println("Rest:\n```{}```", md);
}
// ParamCommandComment
{
constexpr auto raw_comment = R"( @)";
clice::println("Processing raw comment: `{}`", raw_comment);
auto [di, md] = strip_doxygen_info(raw_comment);
clice::println("Rest:\n```\n{}\n```\n", md);
}
{
constexpr auto raw_comment = R"( @param)";
clice::println("Processing raw comment: `{}`", raw_comment);
auto [di, md] = strip_doxygen_info(raw_comment);
clice::println("Rest:\n```\n{}\n```\n", md);
}
{
constexpr auto raw_comment = R"( @param[in,out] foo doc for foo)";
clice::println("Processing raw comment: `{}`", raw_comment);
auto [di, md] = strip_doxygen_info(raw_comment);
EXPECT_TRUE(md.size() == 0);
auto info_foo = di.find_param_info("foo");
EXPECT_TRUE(info_foo.has_value());
if(info_foo.has_value()) {
llvm::StringRef doc = info_foo.value()->content;
EXPECT_TRUE(info_foo.value()->direction ==
DoxygenInfo::ParamCommandCommentContent::ParamDirection::InOut);
clice::println("Doc:\n```\n{}\n```\n", doc);
}
}
{
constexpr auto raw_comment = R"(
@param[out] foo doc for foo
doc for foo line2
\param[in] bar
doc for bar
@param baz
)";
auto [di, md] = strip_doxygen_info(raw_comment);
llvm::StringRef rest = md;
EXPECT_TRUE(rest.trim().size() == 0);
auto info_foo = di.find_param_info("foo");
EXPECT_TRUE(info_foo.has_value());
if(info_foo.has_value()) {
llvm::StringRef doc = info_foo.value()->content;
EXPECT_TRUE(info_foo.value()->direction ==
DoxygenInfo::ParamCommandCommentContent::ParamDirection::Out);
clice::println("Doc:\n```\n{}\n```", doc);
}
auto info_bar = di.find_param_info("bar");
EXPECT_TRUE(info_bar.has_value());
if(info_bar.has_value()) {
llvm::StringRef doc = info_bar.value()->content;
EXPECT_TRUE(info_bar.value()->direction ==
DoxygenInfo::ParamCommandCommentContent::ParamDirection::In);
clice::println("Doc:\n```\n{}\n```", doc);
}
auto info_baz = di.find_param_info("baz");
EXPECT_TRUE(info_baz.has_value());
if(info_baz.has_value()) {
llvm::StringRef doc = info_baz.value()->content;
EXPECT_TRUE(info_baz.value()->direction ==
DoxygenInfo::ParamCommandCommentContent::ParamDirection::Unspecified);
EXPECT_TRUE(doc.trim().size() == 0);
}
}
}
TEST(DoxygenInfo, DoxygenParserIntergrated) {
{
clice::println(
"##################################################################################");
constexpr auto raw_comment = R"(
@brief Calculates the area of a rectangle.
This function computes the area using the formula \c width * height.
It is considered \b fast and \e reliable.
@param[in] width The width of the rectangle (must be > 0)
@param[in] height The height of the rectangle (must be > 0)
@return The area as an integer.
@note If either width or height is zero, the function returns zero.
@details
details 1 blah blah... line1
details 1 blah blah... line2
aabbcssss
~~~~~~^
A line not in a block
@details
details 2 blah blah... line1
details 2 blah blah... line2
)";
auto [di, md] = strip_doxygen_info(raw_comment);
clice::println("Markdown After Stripping:\n```\n{}\n```", md);
auto info_width = di.find_param_info("width");
EXPECT_TRUE(info_width.has_value());
if(info_width.has_value()) {
EXPECT_TRUE(info_width.value()->direction ==
DoxygenInfo::ParamCommandCommentContent::ParamDirection::In);
clice::println("Doc for `width`:\n```\n{}\n```", info_width.value()->content);
}
auto info_height = di.find_param_info("height");
EXPECT_TRUE(info_height.has_value());
if(info_height.has_value()) {
EXPECT_TRUE(info_height.value()->direction ==
DoxygenInfo::ParamCommandCommentContent::ParamDirection::In);
clice::println("Doc for `height`:\n```\n{}\n```", info_height.value()->content);
}
auto bcc_list = di.get_block_command_comments();
EXPECT_TRUE(bcc_list.size() == 3);
clice::println("RegularTags:");
for(auto& [tag_name, content]: bcc_list) {
clice::println("=================================");
clice::println("Tag name: `{}`", tag_name);
for(auto& item: content) {
clice::println("Item:\n```\n{}\n```", item.content);
}
clice::println("=================================");
}
auto ret_info = di.get_return_info();
EXPECT_TRUE(ret_info.has_value());
if(ret_info.has_value()) {
clice::println("Doc for return value:\n```\n{}\n```", ret_info.value());
}
clice::println(
"##################################################################################");
}
// Full test
{
constexpr auto raw_comment = R"( @brief brief block
brief line2
normal line...
normal line...
a b c d e f
~~~~^
normal line...
@param[in] foo doc for foo
@param[out] bar doc for bar
doc for bar line2
@param[in,out] baz doc for baz
@param awa not exist. deprecated
@param foo doc for foo extra line
@details here are some details
details line2
details line3 unproper indent but also detail block
normal comment line
@warning watch out
warn line2
+------[foo]------+
| |
| I'm a box |
| |
+-----------------+
desc line outside
a b c d e f
~~~~^
This is inline display: @b Bold \e Italic @c InlineCode
@warning watch out *2
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
YYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
@note here's note1
note1 line2
@note here's note2
note2 line2
not note2 line3, normal comment
@return doc for return value
)";
auto [di, md] = strip_doxygen_info(raw_comment);
clice::println("Markdown After Stripping:\n```\n{}\n```", md);
auto info_foo = di.find_param_info("foo");
EXPECT_TRUE(info_foo.has_value());
if(info_foo.has_value()) {
EXPECT_TRUE(info_foo.value()->direction ==
DoxygenInfo::ParamCommandCommentContent::ParamDirection::In);
clice::println("Doc for `foo`:\n```\n{}\n```", info_foo.value()->content);
}
auto info_bar = di.find_param_info("bar");
EXPECT_TRUE(info_bar.has_value());
if(info_bar.has_value()) {
EXPECT_TRUE(info_bar.value()->direction ==
DoxygenInfo::ParamCommandCommentContent::ParamDirection::Out);
clice::println("Doc for `bar`:\n```\n{}\n```", info_bar.value()->content);
}
auto info_baz = di.find_param_info("baz");
EXPECT_TRUE(info_baz.has_value());
if(info_baz.has_value()) {
EXPECT_TRUE(info_baz.value()->direction ==
DoxygenInfo::ParamCommandCommentContent::ParamDirection::InOut);
clice::println("Doc for `baz`:\n```\n{}\n```", info_baz.value()->content);
}
auto info_awa = di.find_param_info("awa");
EXPECT_TRUE(info_awa.has_value());
if(info_awa.has_value()) {
EXPECT_TRUE(info_awa.value()->direction ==
DoxygenInfo::ParamCommandCommentContent::ParamDirection::Unspecified);
clice::println("Doc for `awa`:\n```\n{}\n```", info_awa.value()->content);
}
auto bcc_list = di.get_block_command_comments();
EXPECT_TRUE(bcc_list.size() == 4);
clice::println("RegularTags:");
for(auto& [tag_name, content]: bcc_list) {
clice::println("=================================");
clice::println("Tag name: `{}`", tag_name);
for(auto& item: content) {
clice::println("Item:\n```\n{}\n```", item.content);
}
clice::println("=================================");
}
auto ret_info = di.get_return_info();
EXPECT_TRUE(ret_info.has_value());
if(ret_info.has_value()) {
clice::println("Doc for return value:\n```\n{}\n```", ret_info.value());
}
clice::println(
"##################################################################################");
}
}
} // namespace clice::testing

View File

@@ -0,0 +1,111 @@
#include <print>
#include "Test/Test.h"
#include "Support/StructedText.h"
namespace clice::testing {
TEST(StructedText, Paragraph) {
constexpr const char* cb =
R"c(// Without processing, recommended
char *longestPalindrome_solv2(const char *s) {
int len = strlen(s);
int max_start = 0;
int max_len = 0;
for (int i = 0; i < len; ++i) {
// j = 0, max_len is odd
// j = 1, max_len is even
for (int j = 0; j <= 1; ++j) {
int l = i;
int r = i + j;
// expand the range from center
while (l >= 0 && r < len && s[l] == s[r]) {
--l;
++r;
}
++l;
--r;
if (max_len < r - l + 1) {
max_len = r - l + 1;
max_start = i;
}
}
}
char *res = (char *)malloc((max_len + 1) * sizeof(char));
memcpy(res, s + max_start, max_len);
res[max_len] = '\0';
return res;
}
)c";
StructedText st;
st.add_paragraph().append_text("CodeBlock Example:").append_newline_char();
st.add_code_block(cb, "c");
auto& para = st.add_paragraph();
para.append_text("para1").append_newline_char();
std::print("{}", st.as_markdown());
}
TEST(StructedText, BulletList) {
StructedText st;
st.add_bullet_list().add_item().add_paragraph().append_text("Item1");
st.add_bullet_list().add_item().add_paragraph().append_text("Item2", Paragraph::Kind::InlineCode);
st.add_bullet_list().add_item().add_paragraph().append_text("Item3", Paragraph::Kind::Bold);
st.add_bullet_list().add_item().add_paragraph().append_text("Item4", Paragraph::Kind::Italic);
st.add_bullet_list().add_item().add_paragraph().append_text("Item5", Paragraph::Kind::Strikethough);
std::print("{}", st.as_markdown());
}
TEST(StructedText, FullText) {
StructedText st;
st.add_heading(3)
.append_text("function")
.append_text("test_bar", Paragraph::Kind::InlineCode)
.append_newline_char()
.append_text("Provided by:")
.append_text("`foo/bar/baz.h`");
st.add_ruler();
st.add_paragraph()
.append_text("")
.append_text("int", Paragraph::Kind::InlineCode)
.append_newline_char();
st.add_paragraph().append_text("Paramaters:", Paragraph::Kind::Bold).append_newline_char();
auto& params = st.add_bullet_list();
params.add_item()
.add_paragraph()
.append_text("int foo", Paragraph::Kind::InlineCode)
.append_text("doc for foo\ndoc for foo line2");
params.add_item()
.add_paragraph()
.append_text("char** bar", Paragraph::Kind::InlineCode)
.append_text("doc for bar");
params.add_item()
.add_paragraph()
.append_text("char** baz", Paragraph::Kind::InlineCode)
.append_text("doc for baz");
st.add_paragraph().append_text(R"md(
brief block
brief line2
a b c d e f
~~~~^
This is *Italic* **Bold** ~~Striketough~~, `InlineCode`
)md");
st.add_ruler();
st.add_paragraph().append_text("Details:", Paragraph::Kind::Bold).append_newline_char();
auto& details = st.add_bullet_list();
details.add_item().add_paragraph().append_text("Detail1: blah blah...");
details.add_item().add_paragraph().append_text("Detail2: blah blah...\n Line2: ......");
details.add_item().add_paragraph().append_text("Detail3: blah blah...");
st.add_ruler();
st.add_paragraph().append_text("Details:", Paragraph::Kind::Bold).append_newline_char();
auto& warnings = st.add_bullet_list();
warnings.add_item().add_paragraph().append_text("warnings1: blah blah...");
warnings.add_item().add_paragraph().append_text("warnings2: blah blah...\n Line2: ......");
warnings.add_item().add_paragraph().append_text("warnings3: blah blah...");
st.add_ruler();
st.add_code_block("int test_bar(int foo, char **bar, char **baz);\n", "cpp");
std::print("{}", st.as_markdown());
}
} // namespace clice::testing