From ce30acd8d383475ea52824952feef73f43db7302 Mon Sep 17 00:00:00 2001 From: qingfengzl Date: Mon, 11 Aug 2025 22:43:48 +0800 Subject: [PATCH] Structural markdown representation (#162) --- include/Feature/Hover.h | 10 +- include/Support/Doxygen.h | 59 +++++ include/Support/StructedText.h | 108 ++++++++++ src/Support/Doxygen.cpp | 249 +++++++++++++++++++++ src/Support/StructedText.cpp | 216 +++++++++++++++++++ tests/unit/Support/Doxygen.cpp | 321 ++++++++++++++++++++++++++++ tests/unit/Support/StructedText.cpp | 111 ++++++++++ 7 files changed, 1073 insertions(+), 1 deletion(-) create mode 100644 include/Support/Doxygen.h create mode 100644 include/Support/StructedText.h create mode 100644 src/Support/Doxygen.cpp create mode 100644 src/Support/StructedText.cpp create mode 100644 tests/unit/Support/Doxygen.cpp create mode 100644 tests/unit/Support/StructedText.cpp diff --git a/include/Feature/Hover.h b/include/Feature/Hover.h index a73c3e7f..07b79361 100644 --- a/include/Feature/Hover.h +++ b/include/Feature/Hover.h @@ -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 diff --git a/include/Support/Doxygen.h b/include/Support/Doxygen.h new file mode 100644 index 00000000..0ed16df3 --- /dev/null +++ b/include/Support/Doxygen.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include + +#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 find_param_info(llvm::StringRef name); + + std::vector>> + get_block_command_comments(); + + std::optional get_return_info() { + return doc_for_return; + } + +private: + llvm::SmallDenseMap> + block_command_comments; + llvm::SmallDenseMap param_command_comments; + std::optional doc_for_return; +}; + +/// Strip doxygen info from raw comment +/// \return `DoxygenInfo` and the rest comment +std::pair strip_doxygen_info(llvm::StringRef raw_comment); + +} // namespace clice diff --git a/include/Support/StructedText.h b/include/Support/StructedText.h new file mode 100644 index 00000000..f4f7c9a7 --- /dev/null +++ b/include/Support/StructedText.h @@ -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 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 clone() const override { + return std::make_unique(*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 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 clone() const override; + + StructedText& add_item(); + +private: + std::vector 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> blocks; +}; + +} // namespace clice + diff --git a/src/Support/Doxygen.cpp b/src/Support/Doxygen.cpp new file mode 100644 index 00000000..d7550524 --- /dev/null +++ b/src/Support/Doxygen.cpp @@ -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::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>> + DoxygenInfo::get_block_command_comments() { + std::vector>> + 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::iterator& line_ref, + const llvm::SmallVector::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 strip_doxygen_info(llvm::StringRef raw_comment) { + DoxygenInfo di; + std::string s; + llvm::raw_string_ostream os{s}; + llvm::SmallVector 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 diff --git a/src/Support/StructedText.cpp b/src/Support/StructedText.cpp new file mode 100644 index 00000000..de10cefc --- /dev/null +++ b/src/Support/StructedText.cpp @@ -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 BulletList::clone() const { + return std::make_unique(*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 clone() const override { + return std::make_unique(*this); + } +}; + +class CodeBlock : public Block { +public: + void render_markdown(llvm::raw_ostream& os) const override { + os << "```" << lang << '\n' << code << "```\n"; + } + + std::unique_ptr clone() const override { + return std::make_unique(*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> blocks) { + std::string md; + llvm::raw_string_ostream os(md); + + // Trim rulers. + blocks = blocks.drop_while([](const std::unique_ptr& C) { return C->is_ruler(); }); + auto last = llvm::find_if(llvm::reverse(blocks), + [](const std::unique_ptr& 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()); + return *static_cast(blocks.back().get()); +} + +void StructedText::add_ruler() { + blocks.push_back(std::make_unique()); +} + +void StructedText::add_code_block(std::string code, std::string lang) { + blocks.emplace_back(std::make_unique(std::move(code), std::move(lang))); +} + +Paragraph& StructedText::add_heading(unsigned level) { + blocks.emplace_back(std::make_unique(level)); + return *static_cast(blocks.back().get()); +} + +BulletList& StructedText::add_bullet_list() { + blocks.push_back(std::make_unique()); + return *static_cast(blocks.back().get()); +} + +std::string StructedText::as_markdown() const { + return render_blocks(blocks); +} + +} // namespace clice diff --git a/tests/unit/Support/Doxygen.cpp b/tests/unit/Support/Doxygen.cpp new file mode 100644 index 00000000..ccc6b931 --- /dev/null +++ b/tests/unit/Support/Doxygen.cpp @@ -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 expected_detail = {"Detail0", "Detail1", "Detail2"}; + std::set expected_warning = {"Warning0", "Warning1", "Warning2"}; + std::set expected_note = {"Note0", "Note1", "Note2"}; + + std::map> 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 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 diff --git a/tests/unit/Support/StructedText.cpp b/tests/unit/Support/StructedText.cpp new file mode 100644 index 00000000..c33aca45 --- /dev/null +++ b/tests/unit/Support/StructedText.cpp @@ -0,0 +1,111 @@ +#include + +#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