#include "Driver.hpp"
#include "CppParser.hpp"
#include "GcovCounter.hpp"
#include "JSONCounter.hpp"
#include "JSONGenerator.hpp"
#include "YAMLParser.hpp"
#include "Log.hpp"
#include "MarkdownGenerator.hpp"
#include "SourceWatcher.hpp"
#include "doxide.hpp"
#include <glob/glob.hpp>
#include <regex>
#include <thread>
#include <tuple>
#include <utility>
#include <vector>
/**
* Contents of initial `doxide.yaml` file.
*
* @ingroup developer
*/
static const char* init_doxide_yaml =
R""""(title:
description:
style:
files:
- "*.hpp"
- "**/*.hpp"
- "*.h"
- "**/*.h"
)"""";
/**
* Contents of initial `mkdocs.yaml` file.
*
* @ingroup developer
*/
static const char* init_mkdocs_yaml =
R""""(site_name:
site_description:
theme:
name: material
custom_dir: docs/overrides
features:
- navigation.indexes
palette:
# Palette toggle for light mode
- scheme: default
primary: red
accent: red
toggle:
icon: material/brightness-7
name: Switch to dark mode
# Palette toggle for dark mode
- scheme: slate
primary: red
accent: red
toggle:
icon: material/brightness-4
name: Switch to light mode
markdown_extensions:
- def_list
- attr_list
- admonition
- pymdownx.details
- pymdownx.superfences
- pymdownx.arithmatex:
generic: true
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
plugins:
- search
extra_css:
- stylesheets/doxide.css
extra_javascript:
- https://cdn.jsdelivr.net/npm/mathjax@4/tex-mml-chtml.js
- https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js
- https://cdn.jsdelivr.net/npm/[email protected]/src/tablesort.min.js
- https://cdn.jsdelivr.net/npm/[email protected]/src/sorts/tablesort.number.min.js
)"""";
/**
* Contents of initial `docs/stylesheets/doxide.css` file.
*
* @ingroup developer
*/
static const char* init_docs_stylesheets_doxide_css =
R""""(:root {
--md-admonition-icon--variable: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20.41 3c1.39 2.71 1.94 5.84 1.59 9-.2 3.16-1.3 6.29-3.17 9l-1.53-1c1.61-2.43 2.55-5.2 2.7-8 .34-2.8-.11-5.57-1.3-8l1.71-1M5.17 3 6.7 4C5.09 6.43 4.15 9.2 4 12c-.34 2.8.12 5.57 1.3 8l-1.69 1c-1.4-2.71-1.96-5.83-1.61-9 .2-3.16 1.3-6.29 3.17-9m6.91 7.68 2.32-3.23h2.53l-3.78 5 2.2 4.92h-2.26L11.71 14l-2.43 3.33H6.76l3.9-5.12-2.13-4.76h2.27l1.28 3.23Z"/></svg>');
--md-admonition-icon--function: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.6 5.29c-1.1-.1-2.07.71-2.17 1.82L13.18 10H16v2h-3l-.44 5.07a3.986 3.986 0 0 1-4.33 3.63 4.007 4.007 0 0 1-3.06-1.87l1.5-1.5c.24.74.9 1.31 1.73 1.38 1.1.1 2.07-.71 2.17-1.82L11 12H8v-2h3.17l.27-3.07c.19-2.2 2.13-3.83 4.33-3.63 1.31.11 2.41.84 3.06 1.87l-1.5 1.5c-.24-.74-.9-1.31-1.73-1.38Z"/></svg>');
--md-admonition-icon--typedef: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M 13.43 7.11 L 13.18 10 H 17 V 12 H 13 L 12.56 17.07 A 1 1 0 0 0 15.287 17.261 L 17.028 17.48 A 1 1 0 0 1 10.57 16.89 L 11 12 H 8 V 10 H 11.17 L 11.44 6.93 Z"/></svg>');
--md-admonition-icon--concept: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3.75 3.5a.25.25 0 0 0-.25.25v2.062a.75.75 0 1 1-1.5 0V3.75C2 2.783 2.783 2 3.75 2h2.062a.75.75 0 1 1 0 1.5Zm13.688-.75a.75.75 0 0 1 .75-.75h2.062c.966 0 1.75.783 1.75 1.75v2.062a.75.75 0 1 1-1.5 0V3.75a.25.25 0 0 0-.25-.25h-2.062a.75.75 0 0 1-.75-.75ZM2.75 17.438a.75.75 0 0 1 .75.75v2.062c0 .138.112.25.25.25h2.062a.75.75 0 1 1 0 1.5H3.75A1.75 1.75 0 0 1 2 20.25v-2.062a.75.75 0 0 1 .75-.75Zm18.5 0a.75.75 0 0 1 .75.75v2.062A1.75 1.75 0 0 1 20.25 22h-2.062a.75.75 0 1 1 0-1.5h2.062a.25.25 0 0 0 .25-.25v-2.062a.75.75 0 0 1 .75-.75Zm-18.5-8.25a.75.75 0 0 1 .75.75v4.124a.75.75 0 1 1-1.5 0V9.938a.75.75 0 0 1 .75-.75ZM9.188 2.75a.75.75 0 0 1 .75-.75h4.124a.75.75 0 1 1 0 1.5H9.938a.75.75 0 0 1-.75-.75Zm0 18.5a.75.75 0 0 1 .75-.75h4.124a.75.75 0 1 1 0 1.5H9.938a.75.75 0 0 1-.75-.75ZM21.25 9.188a.75.75 0 0 1 .75.75v4.124a.75.75 0 1 1-1.5 0V9.938a.75.75 0 0 1 .75-.75ZM3.75 8.25a.75.75 0 0 1 .75-.75h2a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1-.75-.75Zm5.5 0A.75.75 0 0 1 10 7.5h2A.75.75 0 0 1 12 9h-2a.75.75 0 0 1-.75-.75Zm-1-4.5A.75.75 0 0 1 9 4.5v2a.75.75 0 0 1-1.5 0v-2a.75.75 0 0 1 .75-.75Zm0 5.5A.75.75 0 0 1 9 10v2a.75.75 0 0 1-1.5 0v-2a.75.75 0 0 1 .75-.75Zm0 4.75a.75.75 0 0 1 .75.75v4a.75.75 0 0 1-1.5 0v-4a.75.75 0 0 1 .75-.75ZM14 8.25a.75.75 0 0 1 .75-.75h4a.75.75 0 0 1 0 1.5h-4a.75.75 0 0 1-.75-.75Z"/></svg>');
--md-admonition-icon--macro: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m5.41 21 .71-4h-4l.35-2h4l1.06-6h-4l.35-2h4l.71-4h2l-.71 4h6l.71-4h2l-.71 4h4l-.35 2h-4l-1.06 6h4l-.35 2h-4l-.71 4h-2l.71-4h-6l-.71 4h-2M9.53 9l-1.06 6h6l1.06-6h-6Z"/></svg>');
}
.md-typeset .admonition.variable, .md-typeset details.variable,
.md-typeset .admonition.function, .md-typeset details.function,
.md-typeset .admonition.typedef, .md-typeset details.typedef,
.md-typeset .admonition.concept, .md-typeset details.concept,
.md-typeset .admonition.macro, .md-typeset details.macro {
border-color: var(--md-default-fg-color--lighter);
}
.md-typeset .variable > .admonition-title, .md-typeset .variable > summary,
.md-typeset .function > .admonition-title, .md-typeset .function > summary,
.md-typeset .typedef > .admonition-title, .md-typeset .typedef > summary,
.md-typeset .concept > .admonition-title, .md-typeset .concept > summary,
.md-typeset .macro > .admonition-title, .md-typeset .macro > summary {
background-color: var(--md-default-bg-color);
}
.md-typeset .variable > .admonition-title::before,
.md-typeset .variable > summary::before {
background-color: var(--md-default-fg-color--light);
-webkit-mask-image: var(--md-admonition-icon--variable);
mask-image: var(--md-admonition-icon--variable);
}
.md-typeset .function > .admonition-title::before,
.md-typeset .function > summary::before {
background-color: var(--md-default-fg-color--light);
-webkit-mask-image: var(--md-admonition-icon--function);
mask-image: var(--md-admonition-icon--function);
}
.md-typeset .typedef > .admonition-title::before,
.md-typeset .typedef > summary::before {
background-color: var(--md-default-fg-color--light);
-webkit-mask-image: var(--md-admonition-icon--typedef);
mask-image: var(--md-admonition-icon--typedef);
}
.md-typeset .concept > .admonition-title::before,
.md-typeset .concept > summary::before {
background-color: var(--md-default-fg-color--light);
-webkit-mask-image: var(--md-admonition-icon--concept);
mask-image: var(--md-admonition-icon--concept);
}
.md-typeset .macro > .admonition-title::before,
.md-typeset .macro > summary::before {
background-color: var(--md-default-fg-color--light);
-webkit-mask-image: var(--md-admonition-icon--macro);
mask-image: var(--md-admonition-icon--macro);
}
)"""";
/**
* Contents of initial `docs/overrides/partials/copyright.html` file.
*
* @ingroup developer
*/
static const char* init_docs_overrides_partials_copyright_html =
R""""(<div class="md-copyright">
{% if config.copyright %}
<div class="md-copyright__highlight">
{{ config.copyright }}
</div>
{% endif %}
{% if not config.extra.generator == false %}
Made with
<a href="https://doxide.org" target="_blank" rel="noopener">
Doxide
</a>
and
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
Material for MkDocs
</a>
{% endif %}
</div>
)"""";
Driver::Driver() :
style("mkdocs"),
title("Untitled"),
output("docs") {
//
}
void Driver::init() {
std::string doxide_yaml = init_doxide_yaml;
std::string mkdocs_yaml = init_mkdocs_yaml;
doxide_yaml = std::regex_replace(doxide_yaml, std::regex("title:"),
"title: " + title);
doxide_yaml = std::regex_replace(doxide_yaml, std::regex("description:"),
"description: " + description);
doxide_yaml = std::regex_replace(doxide_yaml, std::regex("style:"),
"style: " + style);
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");
if (style == "mkdocs") {
write_file_prompt(mkdocs_yaml, "mkdocs.yaml");
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() {
parse();
count();
MarkdownGenerator generator(output, style);
generator.generate(root, !coverage.empty());
generator.clean();
}
void Driver::watch() {
build();
std::cout << "Start watching" << std::endl;
SourceWatcher config_watcher = SourceWatcher(config_file.string());
SourceWatcher watcher = SourceWatcher(files_patterns);
CppParser parser;
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;
root.clear();
build();
config_watcher = SourceWatcher(config_file.string());
watcher = SourceWatcher(files_patterns);
std::cout << "Done" << std::endl;
continue;
}
auto [added_files, changed_files, deleted_files] = watcher.diff();
if (!added_files.empty() || !changed_files.empty() || !deleted_files.empty()){
std::cout << std::endl << "Detected changes:" << std::endl;
for (const auto& filename: added_files) {
std::cout << "Added: " << filename << std::endl;
}
for (const auto& filename: changed_files) {
std::cout << "Changed: " << filename << std::endl;
}
for (const auto& filename: deleted_files) {
std::cout << "Deleted: " << filename << std::endl;
}
std::cout << "Rebuilding documentation..." << std::endl;
filenames = watcher.filenames();
for (const auto& filename: deleted_files) {
root.delete_by_predicate([filename](const Entity& e) {return e.path == filename; });
}
for (const auto& filename: changed_files) {
root.delete_by_predicate([filename](const Entity& e) {return e.path == filename; });
}
for (const auto& filename: changed_files) {
parser.parse(filename, style, defines, root);
}
for (const auto& filename: added_files) {
parser.parse(filename, style, defines, root);
}
count();
MarkdownGenerator generator(output, style);
generator.generate(root, !coverage.empty());
generator.clean();
std::cout << "Done" << std::endl;
}
}
}
void Driver::cover() {
parse();
count();
JSONGenerator generator;
generator.generate(root);
}
void Driver::clean() {
/* can use MarkdownGenerator::clean() for this just by not calling
* generate() first; this will remove all files with `generator: doxide` in
* their YAML frontmatter */
MarkdownGenerator generator(output, style);
generator.clean();
}
void Driver::config() {
/* find the configuration file */
if (std::filesystem::exists("doxide.yaml")) {
config_file = "doxide.yaml";
} else if (std::filesystem::exists("doxide.yml")) {
config_file = "doxide.yml";
} else if (std::filesystem::exists("doxide.json")) {
config_file = "doxide.json";
} else {
/* not required to use a config file */
return;
}
/* parse build configuration file */
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");
} else {
warn("'title' must be a value in configuration.");
}
}
if (yaml.has("description")) {
if (yaml.isValue("description")) {
description = yaml.value("description");
} else {
warn("'description' must be a value in configuration.");
}
}
if (yaml.has("coverage")) {
if (yaml.isValue("coverage")) {
coverage = yaml.value("coverage");
} else {
warn("'coverage' must be a value in configuration.");
}
}
if (yaml.has("output")) {
if (yaml.isValue("output")) {
output = yaml.value("output");
} else {
warn("'output' must be a value in configuration.");
}
}
if (yaml.has("defines")) {
if (yaml.isMapping("defines")) {
const auto& map = yaml.mapping("defines");
for (auto& [key, value] : map) {
if (value->isValue()) {
defines[key] = value->value();
}
}
} else {
warn("'defines' must be a mapping in configuration.");
}
}
/* expand file patterns in file list */
filenames.clear();
if (yaml.isSequence("files")) {
for (auto& node : yaml.sequence("files")) {
if (node->isValue()) {
auto& pattern = node->value();
files_patterns.push_back(pattern);
auto paths = glob::rglob(pattern);
if (paths.empty()) {
/* print warning if pattern does not contain a wildcard '*' */
if (pattern.find('*') == std::string::npos) {
warn("no file matching '" + pattern + "' in configuration.");
}
} else for (auto path : paths) {
auto result = filenames.insert(path.string());
if (!result.second) {
warn("file " << path << " appears more than once in configuration.");
}
}
}
}
}
/* validation */
if (style != "mkdocs" && style != "pandoc" && style != "plain") {
warn("'style' must be either 'mkdocs', 'pandoc', or 'plain'. Using 'mkdocs'.");
style = "mkdocs";
}
/* initialize root entity */
groups(yaml, root);
root.title = title;
root.docs = description;
}
void Driver::parse() {
CppParser parser;
for (const auto& filename: filenames) {
parser.parse(filename, style, defines, root);
}
}
void Driver::count() {
if (!coverage.empty()) {
try {
auto ext = coverage.extension();
if (ext == ".gcov") {
GcovCounter counter;
counter.count(coverage, root);
} else if (ext == ".json") {
JSONCounter counter;
counter.count(coverage, root);
} else {
throw std::runtime_error("unrecognized file extension on " +
coverage.string());
}
} catch (const std::runtime_error& e) {
warn(e.what() << ", coverage will be reported as zero");
}
}
}
void Driver::groups(YAMLNode& parentNode, Entity& parentEntity) {
if (parentNode.isSequence("groups")) {
for (auto& node : parentNode.sequence("groups")) {
Entity entity;
entity.type = EntityType::GROUP;
entity.visible = true;
if (node->isValue("name")) {
entity.name = node->value("name");
if (node->isValue("title")) {
entity.title = node->value("title");
}
if (node->isValue("description")) {
entity.docs = node->value("description");
}
} else {
warn("a group is missing a name in the configuration file.")
}
groups(*node, entity);
parentEntity.add(std::move(entity));
}
}
}