diff --git a/.gitignore b/.gitignore index 9ea7a31..e78de53 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ /docs/demo /.cache /src/config.h +cmbuild.sh + diff --git a/CMakeLists.txt b/CMakeLists.txt index 4530e44..b36a9cc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,7 @@ project(doxide cmake_policy(SET CMP0074 NEW) # use _ROOT to look for packages set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") # Available options to control how dependencies are sourced @@ -91,6 +92,7 @@ add_executable(doxide src/JSONCounter.cpp src/JSONGenerator.cpp src/MarkdownGenerator.cpp + src/PlainMarkdownGenerator.cpp src/SourceWatcher.cpp src/YAMLNode.cpp src/YAMLParser.cpp diff --git a/docs/getting-started.md b/docs/getting-started.md index daa897e..da85076 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -10,3 +10,10 @@ MkDocs](https://squidfunk.github.io/mkdocs-material/). To start, it is not necessary to modify any of these. Add at least `doxide.yaml` to version control, and the other files if you intend to use Material for MkDocs (highly recommended for a quick start---you can always try something else later). + +### For small projects +Sometimes you don't want the dependencies of Mkdocs, and just plain Markdown. You can use: +``` +doxide init --plain +``` +This will skip the creation of the web oriented output (JavaScript, CSS, etc.) and use only Markdown. diff --git a/docs/running.md b/docs/running.md index d5f049d..2853a95 100644 --- a/docs/running.md +++ b/docs/running.md @@ -20,3 +20,10 @@ To serve the documentation locally, use: mkdocs serve ``` and point your browser to the URL reported, usually `localhost:8000`. + +### For small projects +If you don't want the Mkdocs noise, you can run: +``` +doxide gitdoc +``` +This will populate the output directory with GitHub webview-ready Markdown. Block quotes are substituted for Mkdocs admonitions, and unsupported icons are replaced with near-identical unicode characters. diff --git a/src/Driver.cpp b/src/Driver.cpp index 4c3c5bc..d930c9b 100644 --- a/src/Driver.cpp +++ b/src/Driver.cpp @@ -2,20 +2,23 @@ #include "YAMLParser.hpp" #include "CppParser.hpp" #include "MarkdownGenerator.hpp" +#include "PlainMarkdownGenerator.hpp" #include "GcovCounter.hpp" #include "JSONCounter.hpp" #include "JSONGenerator.hpp" #include "SourceWatcher.hpp" +#include #include /** * Contents of initial `doxide.yaml` file. - * + * * @ingroup developer */ static const char* init_doxide_yaml = -R""""(title: +R""""(style: +title: description: files: - "*.hpp" @@ -26,12 +29,12 @@ R""""(title: /** * Contents of initial `mkdocs.yaml` file. - * + * * @ingroup developer */ static const char* init_mkdocs_yaml = R""""(site_name: -site_description: +site_description: theme: name: material custom_dir: docs/overrides @@ -43,7 +46,7 @@ R""""(site_name: primary: red accent: red toggle: - icon: material/brightness-7 + icon: material/brightness-7 name: Switch to dark mode # Palette toggle for dark mode @@ -79,7 +82,7 @@ R""""(site_name: /** * Contents of initial `docs/javascripts/mathjax.js` file. - * + * * @ingroup developer */ static const char* init_docs_javascripts_mathjax_js = @@ -96,14 +99,14 @@ R""""(window.MathJax = { } }; -document$.subscribe(() => { +document$.subscribe(() => { MathJax.typesetPromise() }) )""""; /** * Contents of initial `docs/javascripts/tablesort.js` file. - * + * * @ingroup developer */ static const char* init_docs_javascripts_tablesort_js = @@ -117,7 +120,7 @@ R""""(document$.subscribe(function() { /** * Contents of initial `docs/stylesheets/doxide.css` file. - * + * * @ingroup developer */ static const char* init_docs_stylesheets_doxide_css = @@ -183,7 +186,7 @@ R""""(:root { /** * Contents of initial `docs/overrides/partials/copyright.html` file. - * + * * @ingroup developer */ static const char* init_docs_overrides_partials_copyright_html = @@ -212,26 +215,35 @@ Driver::Driver() : // } -void Driver::init() { +void Driver::init(bool plain_md) { std::string doxide_yaml = init_doxide_yaml; std::string mkdocs_yaml = init_mkdocs_yaml; + if (plain_md) { + doxide_yaml = std::regex_replace(doxide_yaml, std::regex("style:"), + "style: plain"); + } else { + doxide_yaml = std::regex_replace(doxide_yaml, std::regex("style:"), + "style: mkdocs"); + } doxide_yaml = std::regex_replace(doxide_yaml, std::regex("title:"), "title: " + title); doxide_yaml = std::regex_replace(doxide_yaml, std::regex("description:"), "description: " + description); - + mkdocs_yaml = std::regex_replace(mkdocs_yaml, std::regex("site_name:"), "site_name: " + title); mkdocs_yaml = std::regex_replace(mkdocs_yaml, std::regex("site_description:"), "site_description: " + description); write_file_prompt(doxide_yaml, "doxide.yaml"); - write_file_prompt(mkdocs_yaml, "mkdocs.yaml"); - write_file_prompt(init_docs_javascripts_mathjax_js, "docs/javascripts/mathjax.js"); - write_file_prompt(init_docs_javascripts_tablesort_js, "docs/javascripts/tablesort.js"); - write_file_prompt(init_docs_stylesheets_doxide_css, "docs/stylesheets/doxide.css"); - write_file_prompt(init_docs_overrides_partials_copyright_html, "docs/overrides/partials/copyright.html"); + if (!plain_md) { + write_file_prompt(mkdocs_yaml, "mkdocs.yaml"); + write_file_prompt(init_docs_javascripts_mathjax_js, "docs/javascripts/mathjax.js"); + write_file_prompt(init_docs_javascripts_tablesort_js, "docs/javascripts/tablesort.js"); + write_file_prompt(init_docs_stylesheets_doxide_css, "docs/stylesheets/doxide.css"); + write_file_prompt(init_docs_overrides_partials_copyright_html, "docs/overrides/partials/copyright.html"); + } } void Driver::build() { @@ -257,7 +269,7 @@ void Driver::watch() { for (;;){ std::this_thread::sleep_for(std::chrono::milliseconds(2000)); - + if (config_watcher.changed()){ std::cout << std::endl << "Detected configuration file change." << std::endl; std::cout << "Rebuilding documentation..." << std::endl; @@ -350,6 +362,13 @@ void Driver::config() { YAMLParser parser; YAMLNode yaml = parser.parse(config_file); + if (yaml.has("style")) { + if (yaml.isValue("style")) { + style = yaml.value("style"); + } else { + warn("'style' must be a value in configuration."); + } + } if (yaml.has("title")) { if (yaml.isValue("title")) { title = yaml.value("title"); @@ -390,7 +409,7 @@ void Driver::config() { warn("'defines' must be a mapping in configuration."); } } - + /* expand file patterns in file list */ filenames.clear(); if (yaml.isSequence("files")) { @@ -416,6 +435,7 @@ void Driver::config() { /* initialize root entity */ groups(yaml, root); + root.style = style; root.title = title; root.docs = description; } diff --git a/src/Driver.hpp b/src/Driver.hpp index 091b28e..3bfb234 100644 --- a/src/Driver.hpp +++ b/src/Driver.hpp @@ -6,7 +6,7 @@ /** * Driver for running commands - * + * * @ingroup developer */ class Driver { @@ -19,13 +19,18 @@ class Driver { /** * Create a new configuration file. */ - void init(); + void init(bool plain); /** * Build documentation. */ void build(); + /** + * Build GetHub webview ready documentation. + */ + void git_build(); + /** * Watch and build documentation on changes. */ @@ -41,6 +46,11 @@ class Driver { */ void clean(); + /** + * Style. + */ + std::string style; + /** * Title. */ @@ -61,6 +71,11 @@ class Driver { */ std::filesystem::path output; + /** + * Whether to remove Mkdocs noise. + */ + bool plain; + private: /** * Read in the configuration file. diff --git a/src/Entity.hpp b/src/Entity.hpp index 0391388..1f30c8a 100644 --- a/src/Entity.hpp +++ b/src/Entity.hpp @@ -6,7 +6,7 @@ /** * Entity types. - * + * * @ingroup developer */ enum class EntityType { @@ -28,7 +28,7 @@ enum class EntityType { /** * Entity in a C++ source file, e.g. variable, function, class, etc. - * + * * @ingroup developer */ struct Entity { @@ -45,9 +45,9 @@ struct Entity { /** * Add child entity. - * + * * @param o Child entity. - * + * * If the child has `ingroup` set, then will search for and add to that * group instead. */ @@ -55,14 +55,14 @@ struct Entity { /** * Merge the children of another entity into this one. - * + * * @param o Other entity. */ void merge(Entity&& o); /** * Does a file exist of the given name? - * + * * @param path File path. */ bool exists(std::filesystem::path& path) const; @@ -167,6 +167,11 @@ struct Entity { */ std::string docs; + /** + * Entity style. This is used to generate certain types of Markdown. + */ + std::string style; + /** * Entity title. This is used for the title of the page. */ @@ -232,9 +237,9 @@ struct Entity { private: /** * Add child entity to a group. - * + * * @param o Child entity with `ingroup` set. - * + * * @return True if a group of the given name was found, in which case @p o * will have been added to it, false otherwise. */ @@ -242,9 +247,9 @@ struct Entity { /** * Add child entity. - * + * * @param o Child entity. - * + * * If the child has `ingroup` set, it is ignored. */ void addToThis(Entity&& o); diff --git a/src/MarkdownGenerator.cpp b/src/MarkdownGenerator.cpp index f30aa87..0f8d3a8 100644 --- a/src/MarkdownGenerator.cpp +++ b/src/MarkdownGenerator.cpp @@ -44,7 +44,7 @@ void MarkdownGenerator::clean() { } for (auto& dir : empty) { std::filesystem::remove(dir); - } + } } while (empty.size()); } } @@ -52,10 +52,12 @@ void MarkdownGenerator::clean() { void MarkdownGenerator::generate(const std::filesystem::path& output, const Entity& entity, const bool cov) { std::string name = sanitize(entity.name); // entity name, empty for root + std::string style; // Style of Markdown to output. std::string dirname; // directory name for this entity std::string filename; // file name for this entity std::string childdir; // directory name for children, relative to filename if (entity.type == EntityType::ROOT) { + style = entity.style; /* root node */ dirname = ""; filename = "index"; @@ -103,24 +105,43 @@ void MarkdownGenerator::generate(const std::filesystem::path& output, /* groups */ for (auto& child : view(entity.groups, false)) { - out << ":material-format-section: [" << title(*child) << ']'; - out << "(" << childdir << sanitize(child->name) << "/index.md)" << std::endl; - out << ": " << line(brief(*child)) << std::endl; - out << std::endl; + if (style == "plain") { + out << "ยง [" << title(*child) << ']'; + out << "(" << childdir << sanitize(child->name) << "/index.md)" << std::endl; + out << "- " << line(brief(*child)) << std::endl; + out << std::endl; + } else if (style == "mkdocs") { + out << ":material-format-section: [" << title(*child) << ']'; + out << "(" << childdir << sanitize(child->name) << "/index.md)" << std::endl; + out << ": " << line(brief(*child)) << std::endl; + out << std::endl; + } } /* namespaces */ for (auto& child : view(entity.namespaces, true)) { - out << ":material-package: [" << child->name << ']'; - out << "(" << childdir << sanitize(child->name) << "/index.md)" << std::endl; - out << ": " << line(brief(*child)) << std::endl; - out << std::endl; + if (style == "plain") { + out << "๐Ÿ—ƒ [" << child->name << ']'; + out << "(" << childdir << sanitize(child->name) << "/index.md)" << std::endl; + out << "- " << line(brief(*child)) << std::endl; + out << std::endl; + } else if (style == "mkdocs") { + out << ":material-package: [" << child->name << ']'; + out << "(" << childdir << sanitize(child->name) << "/index.md)" << std::endl; + out << ": " << line(brief(*child)) << std::endl; + out << std::endl; + } } /* code coverage */ if (entity.type == EntityType::ROOT && cov) { - out << ":material-chart-pie: [Code Coverage](coverage/index.md)" << std::endl; - out << std::endl; + if (style == "plain") { + out << "๐Ÿ—  [Code Coverage](coverage/index.md)" << std::endl; + out << std::endl; + } else if (style == "mkdocs") { + out << ":material-chart-pie: [Code Coverage](coverage/index.md)" << std::endl; + out << std::endl; + } } /* brief descriptions */ @@ -233,9 +254,15 @@ void MarkdownGenerator::generate(const std::filesystem::path& output, auto enums = view(entity.enums, false); if (enums.size() > 0) { for (auto& child : enums) { - out << "**" << child->decl << "**" << std::endl; - out << ": " << child->docs << std::endl; - out << std::endl; + if (style == "plain") { + out << "**" << child->decl << "**" << std::endl; + out << "- " << child->docs << std::endl; + out << std::endl; + } else if (style == "mkdocs") { + out << "**" << child->decl << "**" << std::endl; + out << ": " << child->docs << std::endl; + out << std::endl; + } } out << std::endl; } @@ -249,7 +276,11 @@ void MarkdownGenerator::generate(const std::filesystem::path& output, out << "### " << child->name; out << "name) << "\">" << std::endl; out << std::endl; - out << "!!! typedef \"" << htmlize(line(child->decl)) << '"' << std::endl; + if (style == "plain") { + out << "> ๐™ฉ **Type**" << std::endl << "> " << htmlize(line(child->decl)) << std::endl; + } else if (style == "mkdocs") { + out << "!!! typedef \"" << htmlize(line(child->decl)) << '"' << std::endl; + } out << std::endl; out << indent(child->docs) << std::endl; out << std::endl; @@ -264,7 +295,11 @@ void MarkdownGenerator::generate(const std::filesystem::path& output, out << "### " << child->name; out << "name) << "\">" << std::endl; out << std::endl; + if (style == "plain") { + out << "> โ›ถ **Concept**" << std::endl << "> " << htmlize(line(child->decl)) << std::endl; + } else if (style == "mkdocs") { out << "!!! concept \"" << htmlize(line(child->decl)) << '"' << std::endl; + } out << std::endl; out << indent(child->docs) << std::endl; out << std::endl; @@ -279,7 +314,11 @@ void MarkdownGenerator::generate(const std::filesystem::path& output, out << "### " << child->name; out << "name) << "\">" << std::endl; out << std::endl; - out << "!!! macro \"" << htmlize(line(child->decl)) << '"' << std::endl; + if (style == "plain") { + out << "> ๏ผƒ**Macro**" << std::endl << "> " << htmlize(line(child->decl)) << std::endl; + } else if (style == "plain") { + out << "!!! macro \"" << htmlize(line(child->decl)) << '"' << std::endl; + } out << std::endl; out << indent(child->docs) << std::endl; out << std::endl; @@ -294,7 +333,11 @@ void MarkdownGenerator::generate(const std::filesystem::path& output, out << "### " << child->name; out << "name) << "\">" << std::endl; out << std::endl; - out << "!!! variable \"" << htmlize(line(child->decl)) << '"' << std::endl; + if (style == "plain") { + out << "> โ’ณ **Variable**" << std::endl << "> " << htmlize(line(child->decl)) << std::endl; + } else if (style == "mkdocs") { + out << "!!! variable \"" << htmlize(line(child->decl)) << '"' << std::endl; + } out << std::endl; out << indent(child->docs) << std::endl; out << std::endl; @@ -313,7 +356,11 @@ void MarkdownGenerator::generate(const std::filesystem::path& output, out << "name) << "\">" << std::endl; out << std::endl; } - out << "!!! function \"" << htmlize(line(child->decl)) << '"' << std::endl; + if (style == "plain") { + out << "> ฦ’ **Operator**" << std::endl << "> " << htmlize(line(child->decl)) << std::endl; + } else if (style == "mkdocs") { + out << "!!! function \"" << htmlize(line(child->decl)) << '"' << std::endl; + } out << std::endl; out << indent(child->docs) << std::endl; out << std::endl; @@ -332,7 +379,11 @@ void MarkdownGenerator::generate(const std::filesystem::path& output, out << "### " << child->name; out << "name) << "\">" << std::endl; } - out << "!!! function \"" << htmlize(line(child->decl)) << '"' << std::endl; + if (style == "plain") { + out << "> ฦ’ **Function**" << std::endl << "> " << htmlize(line(child->decl)) << std::endl; + } else if (style == "mkdocs") { + out << "!!! function \"" << htmlize(line(child->decl)) << '"' << std::endl; + } out << std::endl; out << indent(child->docs) << std::endl; out << std::endl; diff --git a/src/PlainMarkdownGenerator.cpp b/src/PlainMarkdownGenerator.cpp new file mode 100644 index 0000000..f1a6b1f --- /dev/null +++ b/src/PlainMarkdownGenerator.cpp @@ -0,0 +1,911 @@ +#include "PlainMarkdownGenerator.hpp" +#include "YAMLParser.hpp" + +PlainMarkdownGenerator::PlainMarkdownGenerator(const std::filesystem::path& output) : + output(output) { + // +} + +void PlainMarkdownGenerator::generate(const Entity& root, const bool cov) { + generate(output, root, cov); + if (cov) { + coverage(output, root); + } +} + +void PlainMarkdownGenerator::clean() { + if (std::filesystem::exists(output) && std::filesystem::is_directory(output)) { + for (auto& entry : std::filesystem::recursive_directory_iterator(output)) { + if (entry.is_regular_file() && entry.path().extension() == ".md" && + !files.contains(entry)) { + try { + YAMLParser parser; + YAMLNode frontmatter = parser.parse(entry.path().string()); + if (frontmatter.isValue("generator") && + frontmatter.value("generator") == "doxide") { + std::filesystem::remove(entry.path()); + } + } catch (const std::runtime_error& e) { + warn(e.what()); + } + } + } + + /* traverse the output directory again, this time removing any empty + * directories; because removing a directory may make its parent directory + * empty, repeat until there are no further empty directories */ + std::vector empty; + do { + empty.clear(); + for (auto& entry : std::filesystem::recursive_directory_iterator(output)) { + if (entry.is_directory() && std::filesystem::is_empty(entry.path())) { + empty.push_back(entry.path()); + } + } + for (auto& dir : empty) { + std::filesystem::remove(dir); + } + } while (empty.size()); + } +} + +void PlainMarkdownGenerator::generate(const std::filesystem::path& output, + const Entity& entity, const bool cov) { + std::string name = sanitize(entity.name); // entity name, empty for root + std::string dirname; // directory name for this entity + std::string filename; // file name for this entity + std::string childdir; // directory name for children, relative to filename + if (entity.type == EntityType::ROOT) { + /* root node */ + dirname = ""; + filename = "index"; + childdir = ""; + } else if (entity.type == EntityType::TYPE) { + /* when building the navigation menu, mkdocs modifies directory names by + * replacing underscores and capitalizing words; this is problematic for + * type names; rather than generating the docs for a class at + * ClassName/index.md, they are generated at ClassName.md, but children + * still go in a ClassName/ subdirectory; final URLs are the same, but + * this unfortunately adds two links to the menu, one for the class, + * another with the same name that expands for the children, if they + * exist; this is not such a problem for classes, which don't often have + * children, so we prefer the option... */ + dirname = ""; + filename = name; + childdir = name + "/"; + } else { + /* ...whereas namespaces and groups tend to have simpler names not + * affected by the mkdocs changes, while two links in the menu is a + * more unsightly, so don't change them */ + dirname = name; + filename = "index"; + childdir = ""; + } + + std::filesystem::create_directories(output / dirname); + std::filesystem::path file = output / dirname / (filename + ".md"); + if (can_write(file)) { + files.insert(file); + std::ofstream out(file); + + /* frontmatter*/ + out << frontmatter(entity) << std::endl; + + /* header */ + out << "# " << title(entity) << std::endl; + out << std::endl; + if (entity.type == EntityType::TYPE) { + out << "**" << htmlize(line(entity.decl)) << "**" << std::endl; + out << std::endl; + } + out << entity.docs << std::endl; + out << std::endl; + + /* groups */ + for (auto& child : view(entity.groups, false)) { + out << "ยง [" << title(*child) << ']'; + out << "(" << childdir << sanitize(child->name) << "/index.md)" << std::endl; + out << "- " << line(brief(*child)) << std::endl; + out << std::endl; + } + + /* namespaces */ + for (auto& child : view(entity.namespaces, true)) { + out << "๐Ÿ—ƒ [" << child->name << ']'; + out << "(" << childdir << sanitize(child->name) << "/index.md)" << std::endl; + out << "- " << line(brief(*child)) << std::endl; + out << std::endl; + } + + /* code coverage */ + if (entity.type == EntityType::ROOT && cov) { + out << "๐Ÿ—  [Code Coverage](coverage/index.md)" << std::endl; + out << std::endl; + } + + /* brief descriptions */ + auto types = view(entity.types, + entity.type == EntityType::NAMESPACE || + entity.type == EntityType::GROUP); + if (types.size() > 0) { + out << "## Types" << std::endl; + out << std::endl; + out << "| Name | Description |" << std::endl; + out << "| ---- | ----------- |" << std::endl; + for (auto& child : types) { + out << "| [" << child->name << "](" << childdir << sanitize(child->name) << ".md) | "; + out << line(brief(*child)) << " |" << std::endl; + } + out << std::endl; + } + + auto typedefs = view(entity.typedefs, + entity.type == EntityType::NAMESPACE || + entity.type == EntityType::GROUP); + if (typedefs.size() > 0) { + out << "## Type Aliases" << std::endl; + out << std::endl; + out << "| Name | Description |" << std::endl; + out << "| ---- | ----------- |" << std::endl; + for (auto& child : typedefs) { + out << "| [" << child->name << "](#" << sanitize(child->name) << ") | "; + out << line(brief(*child)) << " |" << std::endl; + } + out << std::endl; + } + + auto concepts = view(entity.concepts, + entity.type == EntityType::NAMESPACE || + entity.type == EntityType::GROUP); + if (concepts.size() > 0) { + out << "## Concepts" << std::endl; + out << std::endl; + out << "| Name | Description |" << std::endl; + out << "| ---- | ----------- |" << std::endl; + for (auto& child : concepts) { + out << "| [" << child->name << "](#" << sanitize(child->name) << ") | "; + out << line(brief(*child)) << " |" << std::endl; + } + out << std::endl; + } + + auto macros = view(entity.macros, + entity.type == EntityType::NAMESPACE || + entity.type == EntityType::GROUP); + if (macros.size() > 0) { + out << "## Macros" << std::endl; + out << std::endl; + out << "| Name | Description |" << std::endl; + out << "| ---- | ----------- |" << std::endl; + for (auto& child : macros) { + out << "| [" << child->name << "](#" << sanitize(child->name) << ") | "; + out << line(brief(*child)) << " |" << std::endl; + } + out << std::endl; + } + + auto variables = view(entity.variables, + entity.type == EntityType::NAMESPACE || + entity.type == EntityType::GROUP); + if (variables.size() > 0) { + out << "## Variables" << std::endl; + out << std::endl; + out << "| Name | Description |" << std::endl; + out << "| ---- | ----------- |" << std::endl; + for (auto& child : variables) { + out << "| [" << child->name << "](#" << sanitize(child->name) << ") | "; + out << line(brief(*child)) << " |" << std::endl; + } + out << std::endl; + } + + auto operators = view(entity.operators, + entity.type == EntityType::NAMESPACE || + entity.type == EntityType::GROUP); + if (operators.size() > 0) { + out << "## Operators" << std::endl; + out << std::endl; + out << "| Name | Description |" << std::endl; + out << "| ---- | ----------- |" << std::endl; + for (auto& child : operators) { + out << "| [" << child->name << "](#" << sanitize(child->name) << ") | "; + out << line(brief(*child)) << " |" << std::endl; + } + out << std::endl; + } + + auto functions = view(entity.functions, + entity.type == EntityType::NAMESPACE || + entity.type == EntityType::GROUP); + if (functions.size() > 0) { + out << "## Functions" << std::endl; + out << std::endl; + out << "| Name | Description |" << std::endl; + out << "| ---- | ----------- |" << std::endl; + for (auto& child : functions) { + out << "| [" << child->name << "](#" << sanitize(child->name) << ") | "; + out << line(brief(*child)) << " |" << std::endl; + } + out << std::endl; + } + + /* for an enumerator, output the possible values */ + auto enums = view(entity.enums, false); + if (enums.size() > 0) { + for (auto& child : enums) { + out << "**" << child->decl << "**" << std::endl; + out << "- " << child->docs << std::endl; + out << std::endl; + } + out << std::endl; + } + + /* detailed descriptions */ + typedefs = view(entity.typedefs, true); + if (typedefs.size() > 0) { + out << "## Type Alias Details" << std::endl; + out << std::endl; + for (auto& child : typedefs) { + out << "### " << child->name; + out << "name) << "\">" << std::endl; + out << std::endl; + out << "> ๐™ฉ **Type**" << std::endl << "> " << htmlize(line(child->decl)) << std::endl; + out << std::endl; + out << indent(child->docs) << std::endl; + out << std::endl; + } + } + + concepts = view(entity.concepts, true); + if (concepts.size() > 0) { + out << "## Concept Details" << std::endl; + out << std::endl; + for (auto& child : concepts) { + out << "### " << child->name; + out << "name) << "\">" << std::endl; + out << std::endl; + out << "> โ›ถ **Concept**" << std::endl << "> " << htmlize(line(child->decl)) << std::endl; + out << std::endl; + out << indent(child->docs) << std::endl; + out << std::endl; + } + } + + macros = view(entity.macros, true); + if (macros.size() > 0) { + out << "## Macro Details" << std::endl; + out << std::endl; + for (auto& child : macros) { + out << "### " << child->name; + out << "name) << "\">" << std::endl; + out << std::endl; + out << "> ๏ผƒ**Macro**" << std::endl << "> " << htmlize(line(child->decl)) << std::endl; + out << std::endl; + out << indent(child->docs) << std::endl; + out << std::endl; + } + } + + variables = view(entity.variables, true); + if (variables.size() > 0) { + out << "## Variable Details" << std::endl; + out << std::endl; + for (auto& child : variables) { + out << "### " << child->name; + out << "name) << "\">" << std::endl; + out << std::endl; + out << "> โ’ณ **Variable**" << std::endl << "> " << htmlize(line(child->decl)) << std::endl; + out << std::endl; + out << indent(child->docs) << std::endl; + out << std::endl; + } + } + + operators = view(entity.operators, true); + if (operators.size() > 0) { + out << "## Operator Details" << std::endl; + out << std::endl; + std::string prev; + for (auto& child : operators) { + if (child->name != prev) { + /* heading only for the first overload of this name */ + out << "### " << child->name; + out << "name) << "\">" << std::endl; + out << std::endl; + } + out << "> ฦ’ **Operator**" << std::endl << "> " << htmlize(line(child->decl)) << std::endl; + out << std::endl; + out << indent(child->docs) << std::endl; + out << std::endl; + prev = child->name; + } + } + + functions = view(entity.functions, true); + if (functions.size() > 0) { + out << "## Function Details" << std::endl; + out << std::endl; + std::string prev; + for (auto& child : functions) { + if (child->name != prev) { + /* heading only for the first overload of this name */ + out << "### " << child->name; + out << "name) << "\">" << std::endl; + } + out << "> ฦ’ **Function**" << std::endl << "> " << htmlize(line(child->decl)) << std::endl; + out << std::endl; + out << indent(child->docs) << std::endl; + out << std::endl; + prev = child->name; + } + } + } + + /* child pages */ + std::filesystem::create_directories(output / name); + for (auto& child : view(entity.groups, false)) { + generate(output / name, *child, cov); + } + for (auto& child : view(entity.namespaces, false)) { + generate(output / name, *child, cov); + } + for (auto& child : view(entity.types, false)) { + generate(output / name, *child, cov); + } +} + +void PlainMarkdownGenerator::coverage(const std::filesystem::path& output, + const Entity& entity) { + std::string name = sanitize(entity.type == EntityType::ROOT ? + "coverage" : entity.name); // entity name, "coverage"" for root + std::string dirname; // directory name for this entity + std::string filename; // file name for this entity + std::string childdir; // directory name for children, relative to filename + if (entity.type == EntityType::FILE) { + /* as for types in generate(), working around some mkdocs behavior */ + dirname = ""; + filename = name; + childdir = name + "/"; + } else { + dirname = name; + filename = "index"; + childdir = ""; + } + + std::filesystem::create_directories(output / dirname); + std::filesystem::path file = output / dirname / (filename + ".md"); + if (can_write(file)) { + files.insert(file); + std::ofstream out(file); + + /* frontmatter*/ + out << frontmatter(entity) << std::endl; + + /* header */ + if (entity.type == EntityType::ROOT) { + out << "# Code Coverage" << std::endl; + out << std::endl; + } else { + out << "# " << title(entity) << std::endl; + out << std::endl; + out << entity.docs << std::endl; + out << std::endl; + } + + if (entity.type == EntityType::ROOT || entity.type == EntityType::DIR) { + /* code coverage chart */ + sunburst(entity, entity, out); + out << std::endl; + + /* code coverage table */ + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + coverage_data(entity, entity, out); + out << "" << std::endl; + out << "" << std::endl; + coverage_foot(entity, entity, out); + out << "" << std::endl; + out << "
NameLinesCoveredUncoveredCoverage
" << std::endl; + out << std::endl; + } else if (entity.type == EntityType::FILE) { + /* for a file, output the whole contents; line numbers are added with + * the Markdown notation `linenums="1"`, which ultimately creates a HTML + * of one row , with two cells includes a data-parent attribute to facilitate filtering the table + * dynamically based on the currently-selected directory */ + + /* " << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + + coverage_data(*child, root, out); + } + + auto files = view(entity.files, true); + for (auto& child : files) { + std::string parent = sanitize(relative(child->path.parent_path(), root.path)); + std::string name = sanitize(child->name); + uint32_t lines_included = child->lines_included; + uint32_t lines_covered = child->lines_covered; + uint32_t lines_uncovered = lines_included - lines_covered; + double lines_percent = (lines_included > 0) ? + 100.0*lines_covered/lines_included : 100.0; + const std::string& lines_color = color(lines_percent); + std::string path = parent.empty() ? name : parent + '/' + name; + std::string style = parent.empty() ? "" : " style=\"display:none;\""; + + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + } +} + +void PlainMarkdownGenerator::coverage_foot(const Entity& entity, + const Entity& root, std::ofstream& out) { + auto dirs = view(entity.dirs, true); + for (auto& child : dirs) { + coverage_foot(*child, root, out); + } + + /* here data-parent is set to the name of the entity itself, not its parent, + * as the summary row in the footer should be shown when the entity is + * selected as the root */ + std::string name = sanitize(entity.path.string()); + uint32_t lines_included = entity.lines_included; + uint32_t lines_covered = entity.lines_covered; + uint32_t lines_uncovered = lines_included - lines_covered; + double lines_percent = (lines_included > 0) ? + 100.0*lines_covered/lines_included : 100.0; + const std::string& lines_color = color(lines_percent); + std::string style = (name == root.path.string()) ? "" : ", style=\"display:none;\""; + + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; + out << "" << std::endl; +} + +void PlainMarkdownGenerator::sunburst(const Entity& entity, const Entity& root, + std::ofstream& out) { + out << + R""""( +
+
+
+ + + )""""; +} + +void PlainMarkdownGenerator::sunburst_data(const Entity& entity, + const Entity& root, std::ofstream& out) { + bool first = true; + for (auto& dir: view(entity.dirs, true)) { + double percent = (dir->lines_included > 0) ? + 100.0*dir->lines_covered/dir->lines_included : 100.0; + const std::string& c = color(percent); + const std::string& ico = icon(percent); + std::filesystem::path path = relative(dir->path, root.path); + + if (!first) { + out << ", "; + } + first = false; + + out << '{'; + out << "name: \"" << dir->name << "\", "; + out << "path: \"" << path.string() << "\", "; + out << "value: " << dir->lines_included << ", "; + out << "type: \"dir\", "; + out << "icon: \"" << ico << "\", "; + out << "children: ["; + sunburst_data(*dir, root, out); + out << "], "; + out << "itemStyle: { color: \"#" << c << "dd\", borderColor: \"#" << c << "\"}, "; + out << "label: { textBorderColor: \"#" << c << "\"}"; + out << '}'; + } + for (auto& file: view(entity.files, true)) { + double percent = (file->lines_included > 0) ? + 100.0*file->lines_covered/file->lines_included : 100.0; + const std::string& c = color(percent); + const std::string& ico = icon(percent); + std::filesystem::path path = relative(file->path, root.path); + + if (!first) { + out << ", "; + } + first = false; + + out << '{'; + out << "name: \"" << file->name << "\", "; + out << "path: \"" << path.string() << "\", "; + out << "value: " << file->lines_included << ", "; + out << "type: \"file\", "; + out << "icon: \"" << ico << "\", "; + out << "itemStyle: { color: \"#" << c << "dd\", borderColor: \"#" << c << "\"}, "; + out << "label: { textBorderColor: \"#" << c << "\"}"; + out << '}'; + } +} + +bool PlainMarkdownGenerator::can_write(const std::filesystem::path& path) { + bool canWrite = true; + if (std::filesystem::exists(path)) { + try { + YAMLParser parser; + YAMLNode node = parser.parse(path.string()); + canWrite = node.isValue("generator") && node.value("generator") == "doxide"; + } catch (const std::runtime_error& e) { + warn(e.what()); + canWrite = false; + } + } + if (!canWrite) { + warn(path.string() << " already exists and was not generated by doxide, will not overwrite"); + } + return canWrite; +} + +std::string PlainMarkdownGenerator::relative(const std::filesystem::path& path, + const std::filesystem::path& base) { + if (base.empty()) { + return path.string(); + } else { + std::filesystem::path result = std::filesystem::relative(path, base); + if (result == ".") { + return ""; + } else { + return result.string(); + } + } +} + +std::string PlainMarkdownGenerator::frontmatter(const Entity& entity) { + /* use YAML frontmatter to ensure correct capitalization of title, and to + * mark as managed by Doxide */ + std::stringstream buf; + buf << "---" << std::endl; + buf << "generator: doxide" << std::endl; + buf << "---" << std::endl; + buf << std::endl; + return buf.str(); +} + +std::string PlainMarkdownGenerator::title(const Entity& entity) { + if (!entity.title.empty()) { + return entity.title; + } else { + return entity.name; + } +} + +std::string PlainMarkdownGenerator::brief(const Entity& entity) { + if (!entity.brief.empty()) { + return entity.brief; + } else { + static const std::regex reg("^(`.*?`|\\[.*?\\]\\(.*?\\)|[^;:.?!])*[\\.\\?\\!](?=\\s|$)", + regex_flags); + std::string l = line(entity.docs); + std::smatch match; + if (std::regex_search(l, match, reg)) { + return match.str(); + } else { + return l; + } + } +} + +std::string PlainMarkdownGenerator::line(const std::string& str) { + static const std::regex newline("\\s*\\n\\s*", regex_flags); + return std::regex_replace(str, newline, " "); +} + +std::string PlainMarkdownGenerator::indent(const std::string& str) { + static const std::regex start("\\n", regex_flags); + return " " + std::regex_replace(str, start, "\n "); +} + +std::string PlainMarkdownGenerator::stringify(const std::string& str) { + static const std::regex quote("(\"|\\\\)", regex_flags); + std::string r; + r.append("\""); + r.append(std::regex_replace(str, quote, "\\$1")); + r.append("\""); + return r; +} + +std::string PlainMarkdownGenerator::htmlize(const std::string& str) { + /* basic replacements */ + static const std::regex amp("&", regex_flags); + static const std::regex lt("<", regex_flags); + static const std::regex gt(">", regex_flags); + static const std::regex quot("\"", regex_flags); + static const std::regex apos("'", regex_flags); + static const std::regex ptr("\\*", regex_flags); + + /* the sequence operator[](...) looks like a link in Markdown */ + static const std::regex operator_brackets("operator\\[\\]", regex_flags); + + std::string r = str; + r = std::regex_replace(r, amp, "&"); // must go first or new & replaced + r = std::regex_replace(r, lt, "<"); + r = std::regex_replace(r, gt, ">"); + r = std::regex_replace(r, quot, """); + r = std::regex_replace(r, apos, "'"); + r = std::regex_replace(r, ptr, "*"); + r = std::regex_replace(r, operator_brackets, "operator[]"); + return r; +} + +std::string PlainMarkdownGenerator::sanitize(const std::string& str) { + static const std::regex word("\\w|[./\\\\]", regex_flags); + static const std::regex space("\\s", regex_flags); + + std::stringstream buf; + for (auto iter = str.begin(); iter != str.end(); ++iter) { + if (std::regex_match(iter, iter + 1, word)) { + buf << *iter; + } else if (std::regex_match(iter, iter + 1, space)) { + // skip whitespace + } else { + /* encode non-word and non-space characters */ + buf << "_u" << std::setfill('0') << std::setw(4) << std::hex << int(*iter); + } + } + + /* on Linux and Mac, the maximum file name length is 255 bytes, plus leave + * room for a four-character file extension (e.g. .html); on Windows it is + * 260 bytes, so use the minimum */ + return buf.str().substr(0, 255 - 5); +} + +const std::string& PlainMarkdownGenerator::color(const double percent) { + assert(0.0 <= percent && percent <= 100.0); + + static const std::string red = "ef5552"; + static const std::string orange = "f78b2b"; + static const std::string amber = "ffc105"; + static const std::string olive = "a5b72a"; + static const std::string green = "4cae4f"; + + if (percent < 60.0) { + return red; + } else if (percent < 70.0) { + return orange; + } else if (percent < 80.0) { + return amber; + } else if (percent < 90.0) { + return olive; + } else { + return green; + } +} + +const std::string& PlainMarkdownGenerator::icon(const double percent) { + assert(0.0 <= percent && percent <= 100.0); + + static const std::string icon0 = "โ—‹โ—‹โ—‹โ—‹"; + static const std::string icon1 = "โ—โ—‹โ—‹โ—‹"; + static const std::string icon2 = "โ—โ—โ—‹โ—‹"; + static const std::string icon3 = "โ—โ—โ—โ—‹"; + static const std::string icon4 = "โ—โ—โ—โ—"; + + if (percent < 60.0) { + return icon0; + } else if (percent < 70.0) { + return icon1; + } else if (percent < 80.0) { + return icon2; + } else if (percent < 90.0) { + return icon3; + } else { + return icon4; + } +} + +std::list PlainMarkdownGenerator::view( + const std::list& entities, const bool sort) { + auto pointer = [](const Entity& e) { + return &e; + }; + auto hide = [](const Entity* e) { + return !e->visible || e->hide; + }; + auto compare = [](const Entity* a, const Entity* b) { + return a->name < b->name; + }; + + std::list ptrs; + auto end = std::back_inserter(ptrs); + std::transform(entities.begin(), entities.end(), end, pointer); + ptrs.erase(std::remove_if(ptrs.begin(), ptrs.end(), hide), ptrs.end()); + if (sort) { + ptrs.sort(compare); + } + return ptrs; +} diff --git a/src/PlainMarkdownGenerator.hpp b/src/PlainMarkdownGenerator.hpp new file mode 100644 index 0000000..bf7c255 --- /dev/null +++ b/src/PlainMarkdownGenerator.hpp @@ -0,0 +1,181 @@ +#pragma once + +#include "doxide.hpp" +#include "Entity.hpp" + +/** + * Plain Markdown generator. For generating md files ready for GitHub web view. + * + * @ingroup developer + */ +class PlainMarkdownGenerator { +public: + /** + * Constructor. + * + * @param output Output directory. + */ + PlainMarkdownGenerator(const std::filesystem::path& output); + + /** + * Generate documentation. + * + * @param root Root entity. + * @param cov Include code coverage report? + */ + void generate(const Entity& root, const bool cov); + + /** + * Clean up after generation, removing files from old runs. Traverses the + * output directory, removing any Markdown files with 'generator: doxide' in + * their YAML frontmatter that were not generated by previous calls of + * `generate()`. + */ + void clean(); + +private: + /** + * Recursively generate documentation. + * + * @param output Output directory. + * @param entity Entity for which to generate documentation. + * @param cov Include code coverage report? + */ + void generate(const std::filesystem::path& output, const Entity& entity, + const bool cov); + + /** + * Recursively generate coverage. + * + * @param output Output directory. + * @param entity Entity for which to generate coverage. + */ + void coverage(const std::filesystem::path& output, const Entity& entity); + + /** + * Recursively generate coverage table data. + * + * @param entity Entity for which to generate coverage. + * @param root Root entity for the current page. This is used to determine + * which are rows should be visible initially. + * @param out Output stream. + */ + static void coverage_data(const Entity& entity, const Entity& root, + std::ofstream& out); + + /** + * Recursively generate coverage table footer. + * + * @param entity Entity for which to generate coverage. + * @param root Root entity for the current page. This is used to determine + * which are rows should be visible initially. + * @param out Output stream. + */ + static void coverage_foot(const Entity& entity, const Entity& root, + std::ofstream& out); + + /** + * Produce sunburst chart of code coverage for entity. + * + * @param entity Entity for which to generate sunburst. + * @param root Root entity for the current page. This is used to determine + * paths relative to the root. + * @param out Output stream. + */ + static void sunburst(const Entity& entity, const Entity& root, + std::ofstream& out); + + /** + * Produce data for sunburst chart of code coverage for entity. + * + * @param entity Entity for which to generate sunburst. + * @param root Root entity for the current page. This is used to determine + * paths relative to the root. + * @param out Output stream. + */ + static void sunburst_data(const Entity& entity, const Entity& root, + std::ofstream& out); + + /** + * Produce a relative path. + */ + static std::string relative(const std::filesystem::path& path, + const std::filesystem::path& base); + + /** + * Can the file be written? To be overwritten, the file must either not + * exist, or exists but has 'generator: doxide' in its YAML frontmatter. + */ + static bool can_write(const std::filesystem::path& path); + + /** + * Produce the YAML frontmatter for an entity. + */ + static std::string frontmatter(const Entity& entity); + + /** + * Produce title for an entity. + */ + static std::string title(const Entity& entity); + + /** + * Produce brief description for an entity. + */ + static std::string brief(const Entity& entity); + + /** + * Reduce to a single line. + */ + static std::string line(const std::string& str); + + /** + * Indent lines. + */ + static std::string indent(const std::string& str); + + /** + * Sanitize for a string, escaping double quotes and backslashes. + */ + static std::string stringify(const std::string& str); + + /** + * Sanitize for HTML, replacing special characters with entities. Also + * replaces some characters that might trigger Markdown formatting. + */ + static std::string htmlize(const std::string& str); + + /** + * Sanitize for a file name or internal anchor. + */ + static std::string sanitize(const std::string& str); + + /** + * Lookup color for given percentage. + */ + static const std::string& color(const double percent); + + /** + * Lookup icon for given percentage. + */ + static const std::string& icon(const double percent); + + /** + * Convert a list of entities to a list of pointers to entities, optionally + * sorting by name. + * + * @param entities List of entities. + * @param sort Sort by name? + */ + static std::list view(const std::list& entities, + const bool sort); + + /** + * Output directory. + */ + std::filesystem::path output; + + /** + * Set of files generated during the last call to generate(). + */ + std::unordered_set files; +}; diff --git a/src/doxide.cpp b/src/doxide.cpp index 6250d1b..e58873b 100644 --- a/src/doxide.cpp +++ b/src/doxide.cpp @@ -59,10 +59,13 @@ int main(int argc, char** argv) { app.add_option("--coverage", driver.coverage, "Code coverage file (.gcov or .json)."); app.set_version_flag("--version,-v", PACKAGE_VERSION, "Doxide version."); - app.add_subcommand("init", - "Initialize configuration files.")-> - fallthrough()-> - callback([&]() { driver.init(); }); + auto init_cmd = app.add_subcommand("init", + "Initialize configuration files."); + init_cmd->add_flag("--plain", + driver.plain, + "Initialize with no Mkdocs noise."); + init_cmd->fallthrough()-> + callback([&]() { driver.init(driver.plain); }); app.add_subcommand("build", "Build documentation in output directory.")-> fallthrough()->
, the first holding the + * line numbers and the second the code; the line number cell contains + * a further
 with one empty , then each line number
+       * in a separate  within it; to indicate code coverage we apply
+       * styles to those , which are readily selected by number using
+       * the CSS nth-child() pseudo-class */
+      const std::string& covered_color = color(100.0);
+      const std::string& uncovered_color = color(0.0);
+
+      /* style sheet to highlight lines according to code coverage */
+      out << "" << std::endl;
+      out << std::endl;
+
+      /* lines numbers and source code */
+      out << "```cpp linenums=\"1\"" << std::endl;
+      out << entity.decl << std::endl;
+      out << "```" << std::endl;
+      out << std::endl;
+    }
+  }
+
+  /* child pages */
+  std::filesystem::create_directories(output / name);
+  for (auto& child : view(entity.dirs, false)) {
+    coverage(output / name, *child);
+  }
+  for (auto& child : view(entity.files, false)) {
+    coverage(output / name, *child);
+  }
+}
+
+void PlainMarkdownGenerator::coverage_data(const Entity& entity,
+    const Entity& root, std::ofstream& out) {
+  /* icons */
+  static std::string material_file_outline("");
+  static std::string material_folder("");
+
+  /* 
includes data-sort (used by tablesort) for the names column to + * prefix directory names with "a." and file names with "b." for the + * purposes of sorting using the 'dotsep' sort order */ + + auto dirs = view(entity.dirs, true); + for (auto& child : dirs) { + std::string parent = sanitize(relative(child->path.parent_path(), root.path)); + std::string name = sanitize(child->name); + uint32_t lines_included = child->lines_included; + uint32_t lines_covered = child->lines_covered; + uint32_t lines_uncovered = lines_included - lines_covered; + double lines_percent = (lines_included > 0) ? + 100.0*lines_covered/lines_included : 100.0; + const std::string& lines_color = color(lines_percent); + std::string path = parent.empty() ? name : parent + '/' + name; + std::string style = parent.empty() ? "" : " style=\"display:none;\""; + + out << "
" << material_folder << " " << htmlize(child->name) << "" << lines_included << "" << lines_covered << "" << lines_uncovered << "" << std::fixed << std::setprecision(1) << lines_percent << "%
" << material_file_outline << " " << htmlize(child->name) << "" << lines_included << "" << lines_covered << "" << lines_uncovered << "" << std::fixed << std::setprecision(1) << lines_percent << "%
Summary" << lines_included << "" << lines_covered << "" << lines_uncovered << "" << std::fixed << std::setprecision(1) << lines_percent << "%