#include "MarkdownGenerator.hpp"
#include "Log.hpp"
#include "Regex.hpp"
#include "YAMLNode.hpp"
#include "YAMLParser.hpp"
#include <stdint.h>
#include <algorithm>
#include <cassert>
#include <compare>
#include <iomanip>
#include <iterator>
#include <regex>
#include <sstream>
#include <stdexcept>
#include <vector>
static const char* sunburst_html =
R""""(
<div id="coverage-sunburst" style="aspect-ratio:1/1;width:100%;max-width:600px;margin:auto;"></div>
)"""";
static const char* sunburst_js =
R""""(
var coverage_root = ""; // current root of coverage report
function update_coverage_table(params) {
if (typeof params.data.name === 'undefined') {
// occurs when the central circle is selected to go up one level
var path = coverage_root.substring(0, coverage_root.lastIndexOf('/'));
var is_dir = true;
} else {
var path = params.data.path;
var is_dir = params.data.type === 'dir';
}
let rows = document.querySelectorAll('[data-parent]');
if (is_dir) {
for (let row of rows) {
if (row.dataset.parent === path) {
row.style.display = '';
} else {
row.style.display = 'none';
}
}
} else {
for (let row of rows) {
if (row.id === path) {
row.style.display = '';
} else {
row.style.display = 'none';
}
}
}
coverage_root = path;
}
window.addEventListener("load", () => {
var coverage_sunburst = echarts.init(document.getElementById('coverage-sunburst'));
var option = {
series: {
type: 'sunburst',
data: data,
sort: null,
radius: ['5%', '95%'],
startAngle: 0,
clockwise: false,
itemStyle: {
borderWidth: 1
},
label: {
color: 'white',
fontSize: 10,
textBorderWidth: 1,
align: 'center',
rotate: 'radial',
width: 80,
minAngle: 4,
overflow: 'truncate',
formatter: function (params) {
if (params.data.icon) {
return params.name + '\n' + params.data.icon;
} else {
return params.name;
}
}
},
labelLayout: {
hideOverlap: true
},
levels: [
{
itemStyle: {
opacity: 0.2
}
}
]
}
};
coverage_sunburst.setOption(option);
coverage_sunburst.on('click', update_coverage_table);
window.addEventListener("resize", () => {
coverage_sunburst.resize();
});
});
)"""";
static const char* tablesort_js =
R""""(
window.addEventListener("load", () => {
var tables = document.querySelectorAll("article table:not([class])");
tables.forEach(function(table) {
new Tablesort(table);
});
});
)"""";
MarkdownGenerator::MarkdownGenerator(const std::filesystem::path& output,
const std::string& style) :
output(output),
style(style) {
//
}
void MarkdownGenerator::generate(const Entity& root, const bool cov) {
generate(output, root, cov);
if (cov) {
coverage(output, root);
}
}
void MarkdownGenerator::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<std::filesystem::path> 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 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 dirname = entity.type == EntityType::ROOT ? "" : name;
std::filesystem::create_directories(output / dirname);
std::filesystem::path file = output / dirname / "index.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 << "**`" << line(entity.decl) << "`**" << std::endl;
out << std::endl;
}
out << entity.docs << std::endl;
out << std::endl;
/* groups */
for (auto& child : view(entity.groups, false)) {
if (style == "mkdocs") {
out << ":material-format-section: [" << title(*child) << ']';
out << "(" << sanitize(child->name) << "/)" << std::endl;
out << ": " << line(brief(*child)) << std::endl;
out << std::endl;
} else if (style == "pandoc") {
out << "[" << title(*child) << ']';
out << "(" << sanitize(child->name) << "/)" << std::endl;
out << ": " << line(brief(*child)) << std::endl;
out << std::endl;
} else {
out << " - [" << title(*child) << ']';
out << "(" << sanitize(child->name) << "/)" << std::endl;
out << " " << line(brief(*child)) << std::endl;
out << std::endl;
}
}
/* namespaces */
for (auto& child : view(entity.namespaces, true)) {
if (style == "mkdocs") {
out << ":material-package: [" << child->name << ']';
out << "(" << sanitize(child->name) << "/)" << std::endl;
out << ": " << line(brief(*child)) << std::endl;
out << std::endl;
} else if (style == "pandoc") {
out << "[" << child->name << ']';
out << "(" << sanitize(child->name) << "/)" << std::endl;
out << ": " << line(brief(*child)) << std::endl;
out << std::endl;
} else {
out << " - [" << child->name << ']';
out << "(" << sanitize(child->name) << "/)" << std::endl;
out << " " << line(brief(*child)) << std::endl;
out << std::endl;
}
}
/* code coverage */
if (entity.type == EntityType::ROOT && cov) {
if (style == "mkdocs") {
out << ":material-chart-pie: [Code Coverage](coverage/)" << std::endl;
out << ": " << std::endl;
out << std::endl;
} else if (style == "pandoc") {
out << "[Code Coverage](coverage/)" << std::endl;
out << ": " << std::endl;
out << std::endl;
} else {
out << " - [Code Coverage](coverage/)" << std::endl;
out << " " << 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 << "](" << sanitize(child->name) << "/) | ";
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) {
if (style == "mkdocs") {
out << "`" << child->decl << "`" << std::endl;
out << ": " << child->docs << std::endl;
out << std::endl;
} else if (style == "pandoc") {
out << "`" << child->decl << "`" << std::endl;
out << ": " << child->docs << std::endl;
out << std::endl;
} else {
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 << "<a name=\"" << sanitize(child->name) << "\"></a>" << std::endl;
out << std::endl;
if (style == "mkdocs") {
out << "!!! typedef \"" << htmlize(line(child->decl)) << '"' << std::endl;
} else if (style == "pandoc") {
out << "::: typedef" << std::endl;
out << "**`" << line(child->decl) << "`**" << std::endl;
} else {
out << "> **`" << line(child->decl) << "`**" << std::endl;
}
out << indent() << std::endl;
out << indent(child->docs) << std::endl;
if (style == "pandoc") {
out << ":::" << 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 << "<a name=\"" << sanitize(child->name) << "\"></a>" << std::endl;
out << std::endl;
if (style == "mkdocs") {
out << "!!! concept \"" << htmlize(line(child->decl)) << '"' << std::endl;
} else if (style == "pandoc") {
out << "::: concept" << std::endl;
out << "**`" << line(child->decl) << "`**" << std::endl;
} else {
out << "> **`" << line(child->decl) << "`**" << std::endl;
}
out << indent() << std::endl;
out << indent(child->docs) << std::endl;
if (style == "pandoc") {
out << ":::" << 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 << "<a name=\"" << sanitize(child->name) << "\"></a>" << std::endl;
out << std::endl;
if (style == "mkdocs") {
out << "!!! macro \"" << htmlize(line(child->decl)) << '"' << std::endl;
} else if (style == "pandoc") {
out << "::: macro" << std::endl;
out << "**`" << line(child->decl) << "`**" << std::endl;
} else {
out << "> **`" << line(child->decl) << "`**" << std::endl;
}
out << indent() << std::endl;
out << indent(child->docs) << std::endl;
if (style == "pandoc") {
out << ":::" << 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 << "<a name=\"" << sanitize(child->name) << "\"></a>" << std::endl;
out << std::endl;
if (style == "mkdocs") {
out << "!!! variable \"" << htmlize(line(child->decl)) << '"' << std::endl;
} else if (style == "pandoc") {
out << "::: variable" << std::endl;
out << "**`" << line(child->decl) << "`**" << std::endl;
} else {
out << "> **`" << line(child->decl) << "`**" << std::endl;
}
out << indent() << std::endl;
out << indent(child->docs) << std::endl;
if (style == "pandoc") {
out << ":::" << 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 << "<a name=\"" << sanitize(child->name) << "\"></a>" << std::endl;
out << std::endl;
}
if (style == "mkdocs") {
out << "!!! function \"" << htmlize(line(child->decl)) << '"' << std::endl;
} else if (style == "pandoc") {
out << "::: function" << std::endl;
out << "**`" << line(child->decl) << "`**" << std::endl;
} else {
out << "> **`" << line(child->decl) << "`**" << std::endl;
}
out << indent() << std::endl;
out << indent(child->docs) << std::endl;
if (style == "pandoc") {
out << ":::" << 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 << "<a name=\"" << sanitize(child->name) << "\"></a>" << std::endl;
}
if (style == "mkdocs") {
out << "!!! function \"" << htmlize(line(child->decl)) << '"' << std::endl;
} else if (style == "pandoc") {
out << "::: function" << std::endl;
out << "**`" << line(child->decl) << "`**" << std::endl;
} else {
out << "> **`" << line(child->decl) << "`**" << std::endl;
}
out << indent() << std::endl;
out << indent(child->docs) << std::endl;
if (style == "pandoc") {
out << ":::" << 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 MarkdownGenerator::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::filesystem::create_directories(output / name);
std::filesystem::path file = output / name / "index.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 << "<table>" << std::endl;
out << "<thead>" << std::endl;
out << "<tr>" << std::endl;
out << "<th style=\"text-align:left;\" data-sort-method=\"dotsep\">Name</th>" << std::endl;
out << "<th style=\"text-align:right;\" data-sort-method=\"number\">Lines</th>" << std::endl;
out << "<th style=\"text-align:right;\" data-sort-method=\"number\">Covered</th>" << std::endl;
out << "<th style=\"text-align:right;\" data-sort-method=\"number\">Uncovered</th>" << std::endl;
out << "<th style=\"text-align:right;\" data-sort-method=\"number\">Coverage</th>" << std::endl;
out << "</tr>" << std::endl;
out << "</thead>" << std::endl;
out << "<tbody>" << std::endl;
coverage_data(entity, entity, out);
out << "</tbody>" << std::endl;
out << "<tfoot>" << std::endl;
coverage_foot(entity, entity, out);
out << "</tfoot>" << std::endl;
out << "</table>" << std::endl;
out << std::endl;
} else if (entity.type == EntityType::FILE) {
/* for a file, output the whole contents; lines are highlighted with
* CSS; two styles are given, one that works with MkDocs output, one
* that works with pandoc output */
const std::string& covered_color = color(100.0);
const std::string& uncovered_color = color(0.0);
out << "<style>" << std::endl;
/* for MkDocs, line numbers are added with the Markdown notation
* `{linenums="1"}` below, which ultimately creates a HTML <table> of
* one row <tr>, with two cells <td>, the first holding the line
* numbers and the second the code; the line number cell contains a
* further <pre> with one empty <span></span>, then each line number
* in a separate <span> within it; to indicate code coverage we apply
* styles to those <span>, which are readily selected by number using
* the CSS nth-child() pseudo-class */
out << ".linenodiv pre span {";
out << "padding-left:4px;padding-right:4px";
out << "}" << std::endl;
for (uint32_t line = 0; line < entity.line_counts.size(); ++line) {
if (entity.line_counts[line] > 0) {
/* line is covered; line + 2 here to account for 0-base to 1-base
* conversion plus the extra empty <span></span> mentioned above */
out << ".linenodiv pre span:nth-child(" << (line + 2) << ") {";
out << "background-color: #" << covered_color << "dd;";
out << "color: white;";
out << "}" << std::endl;
out << ".linenodiv pre span:nth-child(" << (line + 2) << ")::before {";
out << "content: \"● \";";
out << "}" << std::endl;
} else if (entity.line_counts[line] == 0) {
/* line is uncovered; line + 2 here to account for 0-base to 1-base
* conversion plus the extra empty <span></span> mentioned above */
out << ".linenodiv pre span:nth-child(" << (line + 2) << ") {";
out << "background-color: #" << uncovered_color << "dd;";
out << "color: white;";
out << "}" << std::endl;
out << ".linenodiv pre span:nth-child(" << (line + 2) << ")::before {";
out << "content: \"○ \";";
out << "}" << std::endl;
}
}
/* for pandoc, line numbers are added through the CSS; there is a <span>
* per line that is readily selectable with nth-child(), and it contains
* an <a> at the start that is expanded to add the line number and
* colour it */
out << ".sourceCode pre span a {";
out << "padding-left:8px;padding-right:8px;text-decoration:none;";
out << "}" << std::endl;
for (uint32_t line = 0; line < entity.line_counts.size(); ++line) {
if (entity.line_counts[line] > 0) {
/* line is covered; line + 1 here to account for 0-base to 1-base
* conversion */
out << ".sourceCode pre span:nth-child(" << (line + 1) << ") a {";
out << "background-color: #" << covered_color << "dd;";
out << "color: white;";
out << "}" << std::endl;
out << ".sourceCode pre span:nth-child(" << (line + 1) << ") a::before {";
out << "content: \"● " << std::setw(4) << line << "\";";
out << "}" << std::endl;
} else if (entity.line_counts[line] == 0) {
/* line is uncovered; line + 1 here to account for 0-base to 1-base
* conversion */
out << ".sourceCode pre span:nth-child(" << (line + 1) << ") a {";
out << "background-color: #" << uncovered_color << "dd;";
out << "color: white;";
out << "}" << std::endl;
out << ".sourceCode pre span:nth-child(" << (line + 1) << ") a::before {";
out << "content: \"○ " << std::setw(4) << line << "\";";
out << "}" << std::endl;
} else {
/* line is skipped, just add line number */
out << ".sourceCode pre span:nth-child(" << (line + 1) << ") a {";
out << "color: currentColor;";
out << "}" << std::endl;
out << ".sourceCode pre span:nth-child(" << (line + 1) << ") a::before {";
out << "content: \" " << std::setw(4) << line << "\";";
out << "}" << std::endl;
}
}
out << "</style>" << 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 MarkdownGenerator::coverage_data(const Entity& entity,
const Entity& root, std::ofstream& out) {
/* icons */
static std::string material_file_outline("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zm4 18H6V4h7v5h5z\"/></svg>");
static std::string material_folder("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M10 4H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8z\"/></svg>");
/* <tr> includes a data-parent attribute to facilitate filtering the table
* dynamically based on the currently-selected directory */
/* <td> 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 << "<tr id=\"" << path << "\" data-parent=\"" << parent << "\"" << style << ">" << std::endl;
out << "<td style=\"text-align:left;\" data-sort=\"a." << name << "\">" << material_folder << " <a href=\"" << path << "/\">" << htmlize(child->name) << "</a></td>" << std::endl;
out << "<td style=\"text-align:right;\">" << lines_included << "</td>" << std::endl;
out << "<td style=\"text-align:right;\">" << lines_covered << "</td>" << std::endl;
out << "<td style=\"text-align:right;\">" << lines_uncovered << "</td>" << std::endl;
out << "<td style=\"text-align:right;box-shadow: -8px 0 0 0 #" << lines_color << "dd inset;\">" << std::fixed << std::setprecision(1) << lines_percent << "%</td>" << std::endl;
out << "</tr>" << 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 << "<tr id=\"" << path << "\" data-parent=\"" << parent << "\"" << style << ">" << std::endl;
out << "<td style=\"text-align:left;\" data-sort=\"b." << name << "\">" << material_file_outline << " <a href=\"" << path << "/\">" << htmlize(child->name) << "</a></td>" << std::endl;
out << "<td style=\"text-align:right;\">" << lines_included << "</td>" << std::endl;
out << "<td style=\"text-align:right;\">" << lines_covered << "</td>" << std::endl;
out << "<td style=\"text-align:right;\">" << lines_uncovered << "</td>" << std::endl;
out << "<td style=\"text-align:right;box-shadow: -8px 0 0 0 #" << lines_color << "dd inset;\">" << std::fixed << std::setprecision(1) << lines_percent << "%</td>" << std::endl;
out << "</tr>" << std::endl;
}
}
void MarkdownGenerator::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 << "<tr id=\"summary." << name << "\" data-parent=\"" << name << "\"" << style << ">" << std::endl;
out << "<td style=\"text-align:left;font-weight:bold;\">Summary</td>" << std::endl;
out << "<td style=\"text-align:right;font-weight:bold;\">" << lines_included << "</td>" << std::endl;
out << "<td style=\"text-align:right;font-weight:bold;\">" << lines_covered << "</td>" << std::endl;
out << "<td style=\"text-align:right;font-weight:bold;\">" << lines_uncovered << "</td>" << std::endl;
out << "<td style=\"text-align:right;font-weight:bold;box-shadow: -8px 0 0 0 #" << lines_color << "dd inset;\">" << std::fixed << std::setprecision(1) << lines_percent << "%</td>" << std::endl;
out << "</tr>" << std::endl;
}
void MarkdownGenerator::sunburst(const Entity& entity, const Entity& root,
std::ofstream& out) {
out << sunburst_html << std::endl;
out << "<script type=\"text/javascript\">" << std::endl;
out << "var data = [" << std::endl;
sunburst_data(entity, root, out);
out << ']' << std::endl;
out << sunburst_js << std::endl;
out << tablesort_js << std::endl;
out << "</script>" << std::endl;
}
void MarkdownGenerator::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 MarkdownGenerator::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 MarkdownGenerator::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 MarkdownGenerator::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 MarkdownGenerator::title(const Entity& entity) {
if (!entity.title.empty()) {
return entity.title;
} else {
return entity.name;
}
}
std::string MarkdownGenerator::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 MarkdownGenerator::line(const std::string& str) {
static const std::regex newline("\\s*\\n\\s*", REGEX_FLAGS);
return std::regex_replace(str, newline, " ");
}
std::string MarkdownGenerator::indent(const std::string& str) {
static const std::regex start("\\n", REGEX_FLAGS);
if (style == "mkdocs") {
return " " + std::regex_replace(str, start, "\n ");
} else if (style == "pandoc") {
return str;
} else {
return "> " + std::regex_replace(str, start, "\n> ");
}
}
std::string MarkdownGenerator::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 MarkdownGenerator::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 MarkdownGenerator::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& MarkdownGenerator::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& MarkdownGenerator::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<const Entity*> MarkdownGenerator::view(
const std::list<Entity>& 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<const Entity*> 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;
}