Structural markdown representation (#162)
This commit is contained in:
@@ -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
59
include/Support/Doxygen.h
Normal 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
|
||||
108
include/Support/StructedText.h
Normal file
108
include/Support/StructedText.h
Normal 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
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
|
||||
321
tests/unit/Support/Doxygen.cpp
Normal file
321
tests/unit/Support/Doxygen.cpp
Normal 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
|
||||
111
tests/unit/Support/StructedText.cpp
Normal file
111
tests/unit/Support/StructedText.cpp
Normal 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
|
||||
Reference in New Issue
Block a user