diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a0a745131..507372ebd4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -527,6 +527,44 @@ if (MRDOCS_BUILD_TESTS) ) endforeach () + #------------------------------------------------- + # Template-only generators + # + # Fixtures under test-files/template-only-generators/ ship an + # addon defining their own Handlebars generator. They live outside + # test-files/golden-tests so the xml/adoc/html runs do not walk + # into them and demand expected files in their own formats. + #------------------------------------------------- + set(MRDOCS_TEMPLATE_ONLY_ROOT "${PROJECT_SOURCE_DIR}/test-files/template-only-generators") + add_test(NAME mrdocs-golden-tests-mock-md + COMMAND + mrdocs-test + --unit=false + --action=test + "${MRDOCS_TEMPLATE_ONLY_ROOT}/mock-md" + "--addons=${CMAKE_SOURCE_DIR}/share/mrdocs/addons" + --generator=mock-md + "--stdlib-includes=${LIBCXX_DIR}" + "--libc-includes=${CMAKE_SOURCE_DIR}/share/mrdocs/headers/libc-stubs" + --log-level=warn + ) + foreach (action IN ITEMS test create update) + add_custom_target( + mrdocs-${action}-test-fixtures-mock-md + COMMAND + mrdocs-test + --unit=false + --action=${action} + "${MRDOCS_TEMPLATE_ONLY_ROOT}/mock-md" + "--addons=${CMAKE_SOURCE_DIR}/share/mrdocs/addons" + --generator=mock-md + "--stdlib-includes=${LIBCXX_DIR}" + "--libc-includes=${CMAKE_SOURCE_DIR}/share/mrdocs/headers/libc-stubs" + --log-level=warn + DEPENDS mrdocs-test + ) + endforeach () + #------------------------------------------------- # Self-documentation test (warn-as-error toggled by strict flag) #------------------------------------------------- diff --git a/docs/mrdocs.schema.json b/docs/mrdocs.schema.json index ec184dbee5..c6c407c2c4 100644 --- a/docs/mrdocs.schema.json +++ b/docs/mrdocs.schema.json @@ -252,13 +252,9 @@ }, "generator": { "default": "adoc", - "description": "The generator is responsible for creating the documentation from the extracted symbols. The generator uses the extracted symbols and the templates to create the documentation. The generator can create different types of documentation such as HTML, XML, and AsciiDoc.", - "enum": [ - "adoc", - "html", - "xml" - ], - "title": "Generator used to create the documentation" + "description": "The generator is responsible for creating the documentation from the extracted symbols. The generator uses the extracted symbols and the templates to create the documentation. The built-in generators include `adoc`, `html`, and `xml`; addon-defined generators can be added by dropping a template folder under /generator//.", + "title": "Generator used to create the documentation", + "type": "string" }, "global-namespace-index": { "default": true, diff --git a/include/mrdocs/Support/Handlebars.hpp b/include/mrdocs/Support/Handlebars.hpp index ccdc506423..fd1da66207 100644 --- a/include/mrdocs/Support/Handlebars.hpp +++ b/include/mrdocs/Support/Handlebars.hpp @@ -287,6 +287,19 @@ HTMLEscape( OutputRef& out, std::string_view str); +/** Character-to-entity table used by `HTMLEscape`. +*/ +inline constexpr std::pair +htmlEscapeEntities[] = { + {'&', "&"}, + {'<', "<"}, + {'>', ">"}, + {'"', """}, + {'\'', "'"}, + {'`', "`"}, + {'=', "="} +}; + /** \brief HTML escapes the specified string. * * This function HTML escapes the specified string, making it safe for diff --git a/src/lib/ConfigOptions.json b/src/lib/ConfigOptions.json index 79713b691f..533e8efa0d 100644 --- a/src/lib/ConfigOptions.json +++ b/src/lib/ConfigOptions.json @@ -397,13 +397,8 @@ { "name": "generator", "brief": "Generator used to create the documentation", - "details": "The generator is responsible for creating the documentation from the extracted symbols. The generator uses the extracted symbols and the templates to create the documentation. The generator can create different types of documentation such as HTML, XML, and AsciiDoc.", - "type": "enum", - "values": [ - "adoc", - "html", - "xml" - ], + "details": "The generator is responsible for creating the documentation from the extracted symbols. The generator uses the extracted symbols and the templates to create the documentation. The built-in generators include `adoc`, `html`, and `xml`; addon-defined generators can be added by dropping a template folder under /generator//.", + "type": "string", "default": "adoc" }, { diff --git a/src/lib/Gen/adoc/AdocEscape.cpp b/src/lib/Gen/adoc/AdocEscape.cpp deleted file mode 100644 index 29244a179f..0000000000 --- a/src/lib/Gen/adoc/AdocEscape.cpp +++ /dev/null @@ -1,100 +0,0 @@ -// -// This is a derivative work. originally part of the LLVM Project. -// Licensed under the Apache License v2.0 with LLVM Exceptions. -// See https://llvm.org/LICENSE.txt for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -// Copyright (c) 2023 Vinnie Falco (vinnie.falco@gmail.com) -// -// Official repository: https://github.com/cppalliance/mrdocs -// - -#include "AdocEscape.hpp" -#include -#include - -namespace mrdocs::adoc { - -namespace { - -constexpr -Optional -HTMLNamedEntity(char const c) -{ - // If c has a named entity, we use it - // Otherwise, we return std::nullopt - switch (c) - { - // There's no named entity for '~' (U+007E / ~) in HTML - // - "˜" represents a small tilde (U+02DC) - // - "∼" or "∼" represent the tilde operator (U+223C) - // The 'tilde operator' (U+223C) is not the same character as - // "tilde" (U+007E) although the same glyph might be used to - // represent both. - // case '~': return "˜"; - case '^': return "ˆ"; - case '_': return "_"; - case '*': return "*"; - case '`': return "`"; - case '#': return "#"; - case '[': return "["; - case ']': return "]"; - case '{': return "{"; - case '}': return "}"; - case '<': return "<"; - case '>': return ">"; - case '\\': return "\"; - case '|': return "|"; - case '-': return "‐"; - case '=': return "="; - case '&': return "&"; - case ';': return ";"; - case '+': return "+"; - case ':': return ":"; - case '.': return "."; - case '"': return """; - case '\'': return "'"; - case '/': return "/"; - default: - break; - } - return {}; -} - -} // (anon) - -void -AdocEscape(OutputRef& os, std::string_view str) -{ - static constexpr char reserved[] = R"(~^_*`#[]{}<>\|-=&;+:."\'/)"; - for (char c: str) - { - if (std::ranges::find(reserved, c) != std::end(reserved)) - { - // https://docs.asciidoctor.org/asciidoc/latest/subs/replacements/ - if (auto e = HTMLNamedEntity(c)) - { - os << *e; - } - else - { - os << "&#" << static_cast(c) << ';'; - } - } - else - { - os << c; - } - } -} - -std::string -AdocEscape(std::string_view str) { - std::string res; - OutputRef os(res); - AdocEscape(os, str); - return res; -} - - -} // mrdocs::adoc diff --git a/src/lib/Gen/adoc/AdocEscape.hpp b/src/lib/Gen/adoc/AdocEscape.hpp deleted file mode 100644 index ef98bb133b..0000000000 --- a/src/lib/Gen/adoc/AdocEscape.hpp +++ /dev/null @@ -1,34 +0,0 @@ -// -// This is a derivative work. originally part of the LLVM Project. -// Licensed under the Apache License v2.0 with LLVM Exceptions. -// See https://llvm.org/LICENSE.txt for license information. -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// -// Copyright (c) 2023 Vinnie Falco (vinnie.falco@gmail.com) -// Copyright (c) 2024 Alan de Freitas (alandefreitas@gmail.com) -// -// Official repository: https://github.com/cppalliance/mrdocs -// - -#ifndef MRDOCS_LIB_GEN_ADOC_ADOCESCAPE_HPP -#define MRDOCS_LIB_GEN_ADOC_ADOCESCAPE_HPP - -#include -#include -#include - -namespace mrdocs::adoc { - -/** Escape a string for use in AsciiDoc -*/ -MRDOCS_DECL -void -AdocEscape(OutputRef& os, std::string_view str); - -MRDOCS_DECL -std::string -AdocEscape(std::string_view str); - -} // mrdocs::adoc - -#endif diff --git a/src/lib/Gen/adoc/AdocGenerator.cpp b/src/lib/Gen/adoc/AdocGenerator.cpp index 3a74512a4b..d81c08e4fc 100644 --- a/src/lib/Gen/adoc/AdocGenerator.cpp +++ b/src/lib/Gen/adoc/AdocGenerator.cpp @@ -5,22 +5,89 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // // Copyright (c) 2023 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Gennaro Prota (gennaro.prota@gmail.com) // // Official repository: https://github.com/cppalliance/mrdocs // #include "AdocGenerator.hpp" -#include "AdocEscape.hpp" -#include +#include +#include namespace mrdocs { namespace adoc { -void +namespace { + +// Return the HTML named entity for `c`, or an empty view if there +// is no suitable named entity. +// +// https://docs.asciidoctor.org/asciidoc/latest/subs/replacements/ +constexpr std::string_view +namedEntity(char const c) noexcept +{ + switch (c) + { + // There's no named entity for '~' (U+007E / ~) in HTML + // - "˜" represents a small tilde (U+02DC) + // - "∼" or "∼" represent the tilde operator (U+223C) + // The 'tilde operator' (U+223C) is not the same character as + // "tilde" (U+007E) although the same glyph might be used to + // represent both. + // case '~': return "˜"; + case '^': return "ˆ"; + case '_': return "_"; + case '*': return "*"; + case '`': return "`"; + case '#': return "#"; + case '[': return "["; + case ']': return "]"; + case '{': return "{"; + case '}': return "}"; + case '<': return "<"; + case '>': return ">"; + case '\\': return "\"; + case '|': return "|"; + case '-': return "‐"; + case '=': return "="; + case '&': return "&"; + case ';': return ";"; + case '+': return "+"; + case ':': return ":"; + case '.': return "."; + case '"': return """; + case '\'': return "'"; + case '/': return "/"; + default: return {}; + } +} + +} // (anon) + AdocGenerator:: -escape(OutputRef& os, std::string_view const str) const +AdocGenerator() + : HandlebarsGenerator("adoc", "adoc", "Asciidoc") { - AdocEscape(os, str); + // Reserved characters that AsciiDoc treats specially in body text. + // Each one maps to its HTML named entity when one exists; the + // small set without a named entity (currently just `~`) falls + // back to the numeric form `&#NNN;` constructed from the + // character's code point. + static constexpr char reserved[] = R"(~^_*`#[]{}<>\|-=&;+:."\'/)"; + for (char const c : reserved) + { + std::string_view const named = namedEntity(c); + if (!named.empty()) + { + escapeMap_.set(c, named); + } + else + { + escapeMap_.set( + c, + std::format("&#{};", static_cast(static_cast(c)))); + } + } } } // adoc diff --git a/src/lib/Gen/adoc/AdocGenerator.hpp b/src/lib/Gen/adoc/AdocGenerator.hpp index 382e231c50..a33006fc1b 100644 --- a/src/lib/Gen/adoc/AdocGenerator.hpp +++ b/src/lib/Gen/adoc/AdocGenerator.hpp @@ -13,9 +13,7 @@ #ifndef MRDOCS_LIB_GEN_ADOC_ADOCGENERATOR_HPP #define MRDOCS_LIB_GEN_ADOC_ADOCGENERATOR_HPP -#include #include -#include namespace mrdocs::adoc { @@ -23,27 +21,7 @@ class AdocGenerator final : public hbs::HandlebarsGenerator { public: - std::string_view - id() const noexcept override - { - return "adoc"; - } - - std::string_view - fileExtension() const noexcept override - { - return "adoc"; - } - - - std::string_view - displayName() const noexcept override - { - return "Asciidoc"; - } - - void - escape(OutputRef& os, std::string_view str) const override; + AdocGenerator(); }; } // mrdocs::adoc diff --git a/src/lib/Gen/hbs/AddonGenerators.cpp b/src/lib/Gen/hbs/AddonGenerators.cpp new file mode 100644 index 0000000000..8ecb94b173 --- /dev/null +++ b/src/lib/Gen/hbs/AddonGenerators.cpp @@ -0,0 +1,222 @@ +// +// Licensed under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Copyright (c) 2026 Gennaro Prota (gennaro.prota@gmail.com) +// +// Official repository: https://github.com/cppalliance/mrdocs +// + +#include "AddonGenerators.hpp" +#include "AddonPaths.hpp" +#include "HandlebarsGenerator.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mrdocs::hbs { + +namespace { + +constexpr std::string_view metadataFileName = "mrdocs-generator.yml"; + +// Populate `map` from a YAML mapping whose entries are single-character +// keys mapped to replacement strings. A multi-character key is a hard +// error. +Expected +populateEscapeFromMapping( + llvm::yaml::MappingNode& node, + EscapeMap& map, + std::string_view yamlPath) +{ + for (llvm::yaml::KeyValueNode& entry : node) + { + llvm::yaml::ScalarNode* keyNode = + llvm::dyn_cast_or_null(entry.getKey()); + llvm::yaml::ScalarNode* valNode = + llvm::dyn_cast_or_null(entry.getValue()); + if (!keyNode || !valNode) + { + return Unexpected(formatError( + "{}: each 'escape' entry must be a scalar->scalar mapping", + yamlPath)); + } + llvm::SmallString<8> keyBuf; + llvm::SmallString<32> valBuf; + llvm::StringRef const keyStr = keyNode->getValue(keyBuf); + llvm::StringRef const valStr = valNode->getValue(valBuf); + if (keyStr.size() != 1) + { + return Unexpected(formatError( + "{}: escape key \"{}\" must be exactly one character", + yamlPath, keyStr.str())); + } + map.set(keyStr[0], std::string_view(valStr.data(), valStr.size())); + } + return {}; +} + +// Decide whether `dir` is an addon-defined generator and, if so, install +// a corresponding HandlebarsGenerator into the global registry. +Expected +maybeRegister(std::filesystem::path const& dir) +{ + std::string const name = dir.filename().string(); + if (findGenerator(name)) + { + return {}; + } + std::string const dirPath = dir.string(); + if (!hasLayoutTemplate(dirPath, name)) + { + return {}; + } + + EscapeMap escapeMap; + std::string const yamlPath = + files::appendPath(dirPath, std::string(metadataFileName)); + if (files::exists(yamlPath)) + { + MRDOCS_TRY(escapeMap, loadGeneratorMetadata(yamlPath)); + } + + return installGenerator( + std::make_unique( + name, name, name, std::move(escapeMap))); +} + +// Scan a single /generator/ directory. +Expected +scanGeneratorDir(std::string_view generatorDir) +{ + namespace fs = std::filesystem; + std::error_code iterEc; + fs::directory_iterator const end{}; + for (fs::directory_iterator it(generatorDir, iterEc); + !iterEc && it != end; + it.increment(iterEc)) + { + std::error_code typeEc; + if (!it->is_directory(typeEc)) + { + continue; + } + MRDOCS_TRY(maybeRegister(it->path())); + } + return {}; +} + +} // (anon) + +bool +hasLayoutTemplate( + std::string_view dirPath, + std::string_view name) +{ + std::string const layoutsDir = files::appendPath(dirPath, "layouts"); + if (!files::exists(layoutsDir)) + { + return false; + } + std::string const suffix = std::format(".{}.hbs", name); + namespace fs = std::filesystem; + std::error_code iterEc; + fs::directory_iterator const end{}; + for (fs::directory_iterator it(layoutsDir, iterEc); + !iterEc && it != end; + it.increment(iterEc)) + { + std::error_code typeEc; + if (!it->is_regular_file(typeEc)) + { + continue; + } + std::string const fileName = it->path().filename().string(); + if (fileName.size() > suffix.size() && fileName.ends_with(suffix)) + { + return true; + } + } + return false; +} + +Expected +loadGeneratorMetadata(std::string_view yamlPath) +{ + MRDOCS_TRY(std::string text, files::getFileText(yamlPath)); + llvm::SourceMgr sm; + llvm::yaml::Stream stream(text, sm); + + EscapeMap map; + llvm::yaml::document_iterator docIt = stream.begin(); + if (docIt == stream.end()) + { + return map; + } + llvm::yaml::Node* const rootNode = docIt->getRoot(); + if (rootNode == nullptr || + llvm::isa(rootNode)) + { + // Empty document: file with no content, only comments, or a + // literal `null`. All of these mean "no rules". + return map; + } + llvm::yaml::MappingNode* const root = + llvm::dyn_cast(rootNode); + if (!root) + { + return Unexpected(formatError( + "{}: top-level YAML node must be a mapping", yamlPath)); + } + + for (llvm::yaml::KeyValueNode& pair : *root) + { + llvm::yaml::ScalarNode* const keyNode = + llvm::dyn_cast_or_null(pair.getKey()); + if (!keyNode) + { + continue; + } + llvm::SmallString<16> keyBuf; + if (keyNode->getValue(keyBuf) != "escape") + { + continue; + } + llvm::yaml::MappingNode* const escNode = + llvm::dyn_cast_or_null(pair.getValue()); + if (!escNode) + { + return Unexpected(formatError( + "{}: 'escape' must be a mapping", yamlPath)); + } + MRDOCS_TRY(populateEscapeFromMapping(*escNode, map, yamlPath)); + } + return map; +} + +Expected +discoverAddonGenerators(Config::Settings const& settings) +{ + std::vector const roots = addon_paths::addonRoots(settings); + for (std::string const& root : roots) + { + std::string const dir = files::appendPath(root, "generator"); + if (!files::exists(dir)) + { + continue; + } + MRDOCS_TRY(scanGeneratorDir(dir)); + } + return {}; +} + +} // namespace mrdocs::hbs diff --git a/src/lib/Gen/hbs/AddonGenerators.hpp b/src/lib/Gen/hbs/AddonGenerators.hpp new file mode 100644 index 0000000000..fa437d5098 --- /dev/null +++ b/src/lib/Gen/hbs/AddonGenerators.hpp @@ -0,0 +1,69 @@ +// +// Licensed under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Copyright (c) 2026 Gennaro Prota (gennaro.prota@gmail.com) +// +// Official repository: https://github.com/cppalliance/mrdocs +// + +#ifndef MRDOCS_LIB_GEN_HBS_ADDONGENERATORS_HPP +#define MRDOCS_LIB_GEN_HBS_ADDONGENERATORS_HPP + +#include +#include +#include +#include + +namespace mrdocs::hbs { + +/** Discover addon-defined Handlebars generators and install them. + + For each configured addon root, walk the immediate subdirectories of + /generator/. A subdirectory is treated as an + addon-defined generator when: + + 1. No generator with id `` is already registered (so the + built-in `html` and `adoc` generators take precedence over their + addon directories of the same name). + + 2. Its layouts/ subdirectory contains at least one Handlebars + template named *..hbs. This is what distinguishes a + generator directory from a shared-assets one such as common/, + which only ships CSS. + + For each accepted directory, a `HandlebarsGenerator` is constructed + with `id`, `fileExtension`, and `displayName` all set to `` and + installed into the global registry. Optional escape rules are read + from /mrdocs-generator.yml (see the file format documentation). + + Should be called once after the configuration is resolved and before + a generator is looked up by id. +*/ +Expected +discoverAddonGenerators(Config::Settings const& settings); + +/** Test whether /layouts/ contains at least one *..hbs + template. This is the discriminator between generator directories + and shared-assets directories such as common/. +*/ +bool +hasLayoutTemplate( + std::string_view dirPath, + std::string_view name); + +/** Load mrdocs-generator.yml and return the resulting `EscapeMap`. + + The file is expected to contain a top-level mapping. The optional + 'escape:' key holds a sub-mapping from single-character string keys + to replacement strings. A multi-character key is a hard error. + Unknown top-level keys are ignored so future schema additions are + non-breaking. +*/ +Expected +loadGeneratorMetadata(std::string_view yamlPath); + +} // namespace mrdocs::hbs + +#endif diff --git a/src/lib/Gen/hbs/AddonPaths.hpp b/src/lib/Gen/hbs/AddonPaths.hpp index cdf5ca42af..b7d9e91a23 100644 --- a/src/lib/Gen/hbs/AddonPaths.hpp +++ b/src/lib/Gen/hbs/AddonPaths.hpp @@ -24,24 +24,24 @@ namespace mrdocs::hbs::addon_paths { This function collects all valid addon root paths by checking the primary addons directory and any supplemental addon directories - specified in the configuration. + specified in the settings. - @param config The configuration containing addon path settings. + @param settings The configuration settings containing addon paths. @return A vector of existing addon root directory paths. The primary addons directory (if it exists) appears first, followed by any existing supplemental addon directories in their configured order. */ inline std::vector -addonRoots(Config const& config) +addonRoots(Config::Settings const& settings) { std::vector roots; - roots.reserve(1 + config->addonsSupplemental.size()); + roots.reserve(1 + settings.addonsSupplemental.size()); - if (files::exists(config->addons)) - roots.push_back(config->addons); + if (files::exists(settings.addons)) + roots.push_back(settings.addons); - for (auto const& supplemental : config->addonsSupplemental) + for (auto const& supplemental : settings.addonsSupplemental) { if (files::exists(supplemental)) roots.push_back(supplemental); @@ -161,7 +161,7 @@ findFile( std::string_view subdir, std::string_view filename) { - auto roots = addonRoots(config); + auto roots = addonRoots(config.settings()); for (auto it = roots.rbegin(); it != roots.rend(); ++it) { std::string candidate = files::appendPath(*it, "generator", generator, subdir, filename); diff --git a/src/lib/Gen/hbs/Builder.cpp b/src/lib/Gen/hbs/Builder.cpp index feb1a90925..91b8536fe2 100644 --- a/src/lib/Gen/hbs/Builder.cpp +++ b/src/lib/Gen/hbs/Builder.cpp @@ -381,7 +381,7 @@ Builder( namespace fs = std::filesystem; auto const& config = domCorpus->config; - auto const roots = addon_paths::addonRoots(config); + auto const roots = addon_paths::addonRoots(config.settings()); auto const partialDirs = addon_paths::partialDirs(roots, domCorpus.fileExtension); auto const helperDirs = addon_paths::helperDirs(roots, domCorpus.fileExtension); auto const layoutDirs = addon_paths::layoutDirs(roots, domCorpus.fileExtension); diff --git a/src/lib/Gen/hbs/HandlebarsGenerator.cpp b/src/lib/Gen/hbs/HandlebarsGenerator.cpp index f749b50681..92849f9c74 100644 --- a/src/lib/Gen/hbs/HandlebarsGenerator.cpp +++ b/src/lib/Gen/hbs/HandlebarsGenerator.cpp @@ -103,6 +103,19 @@ createExecutors( // //------------------------------------------------ +HandlebarsGenerator:: +HandlebarsGenerator( + std::string const& id, + std::string const& fileExtension, + std::string const& displayName, + EscapeMap escapeMap) + : escapeMap_(std::move(escapeMap)) + , id_(id) + , fileExtension_(fileExtension) + , displayName_(displayName) +{ +} + Expected HandlebarsGenerator:: build( @@ -238,11 +251,30 @@ buildOne( }); } +void +EscapeMap:: +apply(OutputRef& out, std::string_view str) const +{ + for (char c : str) + { + std::string const& r = + replacements_[static_cast(c)]; + if (r.empty()) + { + out << c; + } + else + { + out << r; + } + } +} + void HandlebarsGenerator:: escape(OutputRef& out, std::string_view str) const { - out << str; + escapeMap_.apply(out, str); } std::string diff --git a/src/lib/Gen/hbs/HandlebarsGenerator.hpp b/src/lib/Gen/hbs/HandlebarsGenerator.hpp index 3996e34135..62e943c605 100644 --- a/src/lib/Gen/hbs/HandlebarsGenerator.hpp +++ b/src/lib/Gen/hbs/HandlebarsGenerator.hpp @@ -16,6 +16,9 @@ #include #include #include +#include +#include +#include #include namespace mrdocs { @@ -24,6 +27,39 @@ class OutputRef; namespace hbs { +/** Character-replacement table used to escape rendered output values. + + Each character that appears in a source string is replaced by its + registered string in the rendered output; characters without an entry + pass through unchanged. The table is indexed by the unsigned char + value of the character, so it covers all 8-bit code points. + + A `HandlebarsGenerator` populates its map in its constructor and the + base class's `escape()` walks that map; subclasses do not override + `escape()` directly. +*/ +class EscapeMap +{ + std::array replacements_; + +public: + /** Replace `c` with `replacement` whenever it appears in escaped text. + + @param c The character to replace. + @param replacement The string to emit in its place. + */ + void + set(char c, std::string_view replacement) + { + replacements_[static_cast(c)] = replacement; + } + + /** Append the escaped form of `str` to `out`. + */ + void + apply(OutputRef& out, std::string_view str) const; +}; + class HandlebarsGenerator : public Generator { @@ -46,10 +82,57 @@ class HandlebarsGenerator bool hasDefaultStyles = false; }; +protected: + /** Escape table for rendered output. Subclasses populate it in + their constructor; the base class drives `escape()` from it. + */ + EscapeMap escapeMap_; + private: + std::string id_; + std::string fileExtension_; + std::string displayName_; + Expected prepareStylesheets(Config const& config) const; public: + /** Construct a Handlebars-based generator from data. + + Used both by the built-in subclasses (which pass their fixed + identity strings and populate `escapeMap_` in the body) and by + the addon-discovery path, which can build a generator entirely + from a template directory without writing a new C++ subclass. + + @param id Stable identifier (matches `mrdocs.yml`'s `generator:`). + @param fileExtension Output file extension (e.g. "html", "adoc"). + @param displayName Human-readable name shown in messages. + @param escapeMap Character-replacement table; empty means + rendered output passes through unchanged. + */ + HandlebarsGenerator( + std::string const& id, + std::string const& fileExtension, + std::string const& displayName, + EscapeMap escapeMap = {}); + + std::string_view + id() const noexcept override + { + return id_; + } + + std::string_view + fileExtension() const noexcept override + { + return fileExtension_; + } + + std::string_view + displayName() const noexcept override + { + return displayName_; + } + Expected build( std::string_view outputPath, @@ -74,9 +157,13 @@ class HandlebarsGenerator std::string_view fileName, Corpus const& corpus) const; - /** Output an escaped string to the output stream. + /** Append the escaped form of `str` to `os`. + + Drives the result from the generator's `escapeMap_`, which is + populated in the subclass constructor. To customize escaping, + configure the map (typically via `escapeMap_.set(...)`) rather + than overriding this function. */ - virtual void escape(OutputRef& os, std::string_view str) const; diff --git a/src/lib/Gen/html/HTMLGenerator.cpp b/src/lib/Gen/html/HTMLGenerator.cpp index f2cfe4ebc0..7a84214dec 100644 --- a/src/lib/Gen/html/HTMLGenerator.cpp +++ b/src/lib/Gen/html/HTMLGenerator.cpp @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // // Copyright (c) 2023 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Gennaro Prota (gennaro.prota@gmail.com) // // Official repository: https://github.com/cppalliance/mrdocs // @@ -15,11 +16,14 @@ namespace mrdocs { namespace html { -void HTMLGenerator:: -escape(OutputRef& os, std::string_view str) const +HTMLGenerator() + : HandlebarsGenerator("html", "html", "HTML") { - HTMLEscape(os, str); + for (auto const& [c, replacement] : htmlEscapeEntities) + { + escapeMap_.set(c, replacement); + } } } // html diff --git a/src/lib/Gen/html/HTMLGenerator.hpp b/src/lib/Gen/html/HTMLGenerator.hpp index 5d95944193..4416f49342 100644 --- a/src/lib/Gen/html/HTMLGenerator.hpp +++ b/src/lib/Gen/html/HTMLGenerator.hpp @@ -23,26 +23,7 @@ class HTMLGenerator final : public hbs::HandlebarsGenerator { public: - std::string_view - id() const noexcept override - { - return "html"; - } - - std::string_view - fileExtension() const noexcept override - { - return "html"; - } - - std::string_view - displayName() const noexcept override - { - return "HTML"; - } - - void - escape(OutputRef& os, std::string_view str) const override; + HTMLGenerator(); }; } // mrdocs::html diff --git a/src/lib/Support/Handlebars.cpp b/src/lib/Support/Handlebars.cpp index 41ccb9bf72..417e6038c5 100644 --- a/src/lib/Support/Handlebars.cpp +++ b/src/lib/Support/Handlebars.cpp @@ -303,18 +303,10 @@ HTMLEscape( OutputRef& out, std::string_view str) { + // Entity table lives in the public header so the HTML generator's + // `EscapeMap` can share it. Source convention follows handlebars.js: // https://github.com/handlebars-lang/handlebars.js/blob/master/lib/handlebars/utils.js - static constexpr std::pair - escapeMap[] = { - {'&', "&"}, - {'<', "<"}, - {'>', ">"}, - {'"', """}, - {'\'', "'"}, - {'`', "`"}, - {'=', "="} - }; - static constexpr auto badChars = std::views::keys(escapeMap); + static constexpr auto badChars = std::views::keys(htmlEscapeEntities); for (auto c : str) { if (auto it = std::ranges::find(badChars, c); it != badChars.end()) diff --git a/src/lib/Support/Path.cpp b/src/lib/Support/Path.cpp index 38fc0adbf9..f35861ae29 100644 --- a/src/lib/Support/Path.cpp +++ b/src/lib/Support/Path.cpp @@ -206,7 +206,7 @@ getFileText( std::istreambuf_iterator it(file); std::istreambuf_iterator const end; std::string text(it, end); - if(! file.good()) + if(file.fail() && ! file.eof()) return Unexpected(formatError("getFileText(\"{}\") returned \"{}\"", pathName, std::error_code(errno, std::generic_category()))); return text; diff --git a/src/test/Support/TestLayout.cpp b/src/test/Support/TestLayout.cpp index fc0e3d7b69..399195d58e 100644 --- a/src/test/Support/TestLayout.cpp +++ b/src/test/Support/TestLayout.cpp @@ -28,26 +28,46 @@ pathWithExtension( } // (anon) -/** Build the per-file layout and normalized settings with mode validation. */ -Expected -resolveTestLayout( +/** Read any per-file mrdocs.yml on top of the directory-level settings. */ +Expected +loadTestSettings( llvm::StringRef filePath, Config::Settings const& dirSettings, - llvm::StringRef generatorExtension, - ReferenceDirectories const& dirs, - Action action) + ReferenceDirectories const& dirs) { - Config::Settings fileSettings = dirSettings; - auto configPath = files::withExtension(filePath, "yml"); - bool const hasFileConfig = files::exists(configPath); - if (hasFileConfig) + LoadedTestSettings result; + result.settings = dirSettings; + result.dirMultipage = dirSettings.multipage; + std::string const configPath = files::withExtension(filePath, "yml"); + result.hasFileConfig = files::exists(configPath); + if (result.hasFileConfig) { - if (auto exp = Config::Settings::load_file(fileSettings, configPath, dirs); !exp) + Expected const exp = Config::Settings::load_file( + result.settings, configPath, dirs); + if (!exp) { return Unexpected(exp.error()); } } + return result; +} +/** Build the layout, prepare multipage outputs, and normalize settings. + The split from loadTestSettings lets the caller run addon-generator + discovery in between, so the chosen generator's file extension is + known when paths are computed. +*/ +Expected +buildTestLayout( + llvm::StringRef filePath, + LoadedTestSettings loaded, + llvm::StringRef generatorExtension, + ReferenceDirectories const& dirs, + Action action) +{ + bool const dirMultipage = loaded.dirMultipage; + Config::Settings fileSettings = std::move(loaded.settings); + bool const hasFileConfig = loaded.hasFileConfig; bool const hasTagfileOverride = !fileSettings.tagfile.empty(); TestLayout layout; @@ -118,7 +138,7 @@ resolveTestLayout( return Unexpected(Error("multipage tests require a per-file mrdocs.yml with multipage: true")); } - if (dirSettings.multipage) + if (dirMultipage) { return Unexpected(Error("multipage defaults must remain disabled at the directory level")); } diff --git a/src/test/Support/TestLayout.hpp b/src/test/Support/TestLayout.hpp index 6fab65b6b9..0d3784aa4b 100644 --- a/src/test/Support/TestLayout.hpp +++ b/src/test/Support/TestLayout.hpp @@ -43,11 +43,42 @@ struct ResolvedLayout TestLayout layout; }; -/** Resolve per-test settings + layout, enforcing single vs multipage rules. */ -Expected -resolveTestLayout( +/** Settings produced by loadTestSettings before the layout is built. +*/ +struct LoadedTestSettings +{ + Config::Settings settings; + /// True if a per-file mrdocs.yml was found and merged. + bool hasFileConfig = false; + /// Snapshot of the directory-level multipage flag before merging. + /// Used to enforce that multipage may only be enabled at the + /// per-file level. + bool dirMultipage = false; +}; + +/** Load any per-file mrdocs.yml on top of the directory-level settings. + + No layout work is done here: the per-file settings are needed before + the test's generator is known, so addon discovery can run against + the merged addons paths. +*/ +Expected +loadTestSettings( llvm::StringRef filePath, Config::Settings const& dirSettings, + ReferenceDirectories const& dirs); + +/** Build the per-file layout from already-loaded settings. + + Computes expected-output paths, applies multipage handling (creating + the temporary output directory and adjusting the settings' output and + tagfile fields), normalizes the settings, and validates the + single vs multipage invariants. +*/ +Expected +buildTestLayout( + llvm::StringRef filePath, + LoadedTestSettings loaded, llvm::StringRef generatorExtension, ReferenceDirectories const& dirs, Action action); diff --git a/src/test/TestRunner.cpp b/src/test/TestRunner.cpp index c3694e2fa5..d8dd34eade 100644 --- a/src/test/TestRunner.cpp +++ b/src/test/TestRunner.cpp @@ -17,6 +17,7 @@ #include "Support/Comparison.hpp" #include #include +#include #include #include #include @@ -37,9 +38,10 @@ namespace mrdocs { TestRunner:: TestRunner(std::string_view generator) - : gen_(findGenerator(generator)) + : genId_(generator) { - MRDOCS_ASSERT(gen_ != nullptr); + // The generator is looked up per-test in handleFile, after that + // test's mrdocs.yml has been loaded and addon discovery has run. } namespace { @@ -165,8 +167,30 @@ handleFile( if (!ensureRegularCpp(filePath)) return; - auto resolved = resolveTestLayout( - filePath, dirSettings, gen_->fileExtension(), dirs_, testArgs.action); + // Load the per-file mrdocs.yml first so addon-defined generators + // contributed via addons-supplemental are visible to discovery + // before the chosen generator is looked up. + Expected loaded = + loadTestSettings(filePath, dirSettings, dirs_); + if (!loaded) + { + return report::error("{}: \"{}\"", loaded.error(), filePath); + } + Expected discovered = + hbs::discoverAddonGenerators(loaded->settings); + if (!discovered) + { + return report::error("{}: \"{}\"", discovered.error(), filePath); + } + Generator const* gen = findGenerator(genId_); + if (!gen) + { + return report::error( + "{}: the Generator \"{}\" was not found", filePath, genId_); + } + + Expected resolved = buildTestLayout( + filePath, *std::move(loaded), gen->fileExtension(), dirs_, testArgs.action); if (!resolved) { return report::error("{}: \"{}\"", resolved.error(), filePath); @@ -194,7 +218,7 @@ handleFile( db, config, defaultIncludePaths); - handleCompilationDatabase(filePath, compilations, config, layout); + handleCompilationDatabase(filePath, *gen, compilations, config, layout); }; runWith({ "clang", "-std=c++23" }); @@ -204,6 +228,7 @@ handleFile( void TestRunner::handleCompilationDatabase( llvm::StringRef filePath, + Generator const& gen, MrDocsCompilationDatabase const& compilations, std::shared_ptr const& config, TestLayout const& layout) @@ -219,7 +244,7 @@ TestRunner::handleCompilationDatabase( { test_support::SinglePageArgs args{ layout, - *gen_, + gen, **corpus, filePath, testArgs.action, @@ -237,7 +262,7 @@ TestRunner::handleCompilationDatabase( { test_support::MultipageArgs args{ layout, - *gen_, + gen, **corpus, testArgs.action, testArgs.forceOption.getValue(), diff --git a/src/test/TestRunner.hpp b/src/test/TestRunner.hpp index b7a02809d4..d9fd937529 100644 --- a/src/test/TestRunner.hpp +++ b/src/test/TestRunner.hpp @@ -55,7 +55,10 @@ struct TestResults class TestRunner { ThreadPool threadPool_; - Generator const* gen_; + /// Id of the chosen generator. Resolved per-test (after each test's + /// settings load) so that addon-defined generators contributed via + /// addons-supplemental are picked up correctly. + std::string genId_; ReferenceDirectories dirs_; /** Run a single .cpp test file with inherited directory settings. */ @@ -80,6 +83,7 @@ class TestRunner void handleCompilationDatabase( llvm::StringRef filePath, + Generator const& gen, MrDocsCompilationDatabase const& compilations, std::shared_ptr const& config, TestLayout const& layout); diff --git a/src/test/lib/Gen/hbs/AddonGenerators.cpp b/src/test/lib/Gen/hbs/AddonGenerators.cpp new file mode 100644 index 0000000000..e139afe4cb --- /dev/null +++ b/src/test/lib/Gen/hbs/AddonGenerators.cpp @@ -0,0 +1,290 @@ +// +// Licensed under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Copyright (c) 2026 Gennaro Prota (gennaro.prota@gmail.com) +// +// Official repository: https://github.com/cppalliance/mrdocs +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mrdocs::hbs { + +namespace { + +// Write `content` verbatim to `path`. Pre-existing files are truncated. +void +writeFile(std::string_view path, std::string_view content) +{ + std::ofstream os(std::string{path}, std::ios::binary | std::ios::trunc); + os.write(content.data(), + static_cast(content.size())); +} + +// Create a directory (and any missing parents). +void +makeDir(std::string_view path) +{ + std::error_code ec; + std::filesystem::create_directories(std::filesystem::path(path), ec); +} + +// Apply `map` to `input` and return the escaped result, so tests can +// observe an `EscapeMap`'s contents through its public surface. +std::string +applyEscape(EscapeMap const& map, std::string_view input) +{ + std::string out; + OutputRef ref(out); + map.apply(ref, input); + return out; +} + +} // (anon) + +struct AddonGeneratorsTest +{ + // + // loadGeneratorMetadata + // + + void + testLoadEmptyFile() + { + ScopedTempDirectory td("mrdocs-addongen"); + BOOST_TEST(td); + std::string const path = + files::appendPath(td.path(), "g.yml"); + writeFile(path, ""); + + Expected result = loadGeneratorMetadata(path); + BOOST_TEST(result.has_value()); + if (result) + { + // Empty map: every char passes through. + BOOST_TEST(applyEscape(*result, "abc*_") == "abc*_"); + } + } + + void + testLoadNoEscapeKey() + { + ScopedTempDirectory td("mrdocs-addongen"); + BOOST_TEST(td); + std::string const path = + files::appendPath(td.path(), "g.yml"); + // Top-level mapping with an unknown key is fine: the schema + // explicitly tolerates extra keys for forward compatibility. + writeFile(path, "displayName: Markdown\n"); + + Expected result = loadGeneratorMetadata(path); + BOOST_TEST(result.has_value()); + if (result) + { + BOOST_TEST(applyEscape(*result, "abc") == "abc"); + } + } + + void + testLoadValidEscape() + { + ScopedTempDirectory td("mrdocs-addongen"); + BOOST_TEST(td); + std::string const path = + files::appendPath(td.path(), "g.yml"); + // Single-quoted YAML scalars treat backslash literally, so the + // value '\*' is the two-character string \*. + writeFile(path, + "escape:\n" + " '*': '\\*'\n" + " '_': '\\_'\n"); + + Expected result = loadGeneratorMetadata(path); + BOOST_TEST(result.has_value()); + if (result) + { + BOOST_TEST(applyEscape(*result, "*foo_bar*") == "\\*foo\\_bar\\*"); + BOOST_TEST(applyEscape(*result, "no specials") == "no specials"); + } + } + + void + testLoadNonMappingTopLevel() + { + ScopedTempDirectory td("mrdocs-addongen"); + BOOST_TEST(td); + std::string const path = + files::appendPath(td.path(), "g.yml"); + // Top-level scalar is rejected. + writeFile(path, "just a string\n"); + + Expected result = loadGeneratorMetadata(path); + BOOST_TEST(!result.has_value()); + } + + void + testLoadNonMappingEscape() + { + ScopedTempDirectory td("mrdocs-addongen"); + BOOST_TEST(td); + std::string const path = + files::appendPath(td.path(), "g.yml"); + // 'escape:' must be a mapping, not a scalar. + writeFile(path, "escape: nope\n"); + + Expected result = loadGeneratorMetadata(path); + BOOST_TEST(!result.has_value()); + } + + void + testLoadMultiCharKey() + { + ScopedTempDirectory td("mrdocs-addongen"); + BOOST_TEST(td); + std::string const path = + files::appendPath(td.path(), "g.yml"); + writeFile(path, + "escape:\n" + " 'ab': 'x'\n"); + + Expected result = loadGeneratorMetadata(path); + BOOST_TEST(!result.has_value()); + } + + void + testLoadEmptyKey() + { + ScopedTempDirectory td("mrdocs-addongen"); + BOOST_TEST(td); + std::string const path = + files::appendPath(td.path(), "g.yml"); + writeFile(path, + "escape:\n" + " '': 'x'\n"); + + Expected result = loadGeneratorMetadata(path); + BOOST_TEST(!result.has_value()); + } + + void + testLoadMissingFile() + { + ScopedTempDirectory td("mrdocs-addongen"); + BOOST_TEST(td); + std::string const path = + files::appendPath(td.path(), "does-not-exist.yml"); + + Expected result = loadGeneratorMetadata(path); + BOOST_TEST(!result.has_value()); + } + + // + // hasLayoutTemplate + // + + void + testNoLayoutsDir() + { + ScopedTempDirectory td("mrdocs-addongen"); + BOOST_TEST(td); + BOOST_TEST(!hasLayoutTemplate(td.path(), "md")); + } + + void + testEmptyLayoutsDir() + { + ScopedTempDirectory td("mrdocs-addongen"); + BOOST_TEST(td); + makeDir(files::appendPath(td.path(), "layouts")); + BOOST_TEST(!hasLayoutTemplate(td.path(), "md")); + } + + void + testIndexTemplatePresent() + { + ScopedTempDirectory td("mrdocs-addongen"); + BOOST_TEST(td); + std::string const layouts = + files::appendPath(td.path(), "layouts"); + makeDir(layouts); + writeFile(files::appendPath(layouts, "index.md.hbs"), ""); + BOOST_TEST(hasLayoutTemplate(td.path(), "md")); + } + + void + testWrapperTemplatePresent() + { + ScopedTempDirectory td("mrdocs-addongen"); + BOOST_TEST(td); + std::string const layouts = + files::appendPath(td.path(), "layouts"); + makeDir(layouts); + writeFile(files::appendPath(layouts, "wrapper.md.hbs"), ""); + BOOST_TEST(hasLayoutTemplate(td.path(), "md")); + } + + void + testWrongExtension() + { + ScopedTempDirectory td("mrdocs-addongen"); + BOOST_TEST(td); + std::string const layouts = + files::appendPath(td.path(), "layouts"); + makeDir(layouts); + // index.html.hbs is not a layout for the "md" generator. + writeFile(files::appendPath(layouts, "index.html.hbs"), ""); + BOOST_TEST(!hasLayoutTemplate(td.path(), "md")); + } + + void + testBareNameHbsRejected() + { + ScopedTempDirectory td("mrdocs-addongen"); + BOOST_TEST(td); + std::string const layouts = + files::appendPath(td.path(), "layouts"); + makeDir(layouts); + // A bare md.hbs is not what Builder loads (it expects + // index..hbs / wrapper..hbs); the discriminator + // matches that convention. + writeFile(files::appendPath(layouts, "md.hbs"), ""); + BOOST_TEST(!hasLayoutTemplate(td.path(), "md")); + } + + void + run() + { + testLoadEmptyFile(); + testLoadNoEscapeKey(); + testLoadValidEscape(); + testLoadNonMappingTopLevel(); + testLoadNonMappingEscape(); + testLoadMultiCharKey(); + testLoadEmptyKey(); + testLoadMissingFile(); + + testNoLayoutsDir(); + testEmptyLayoutsDir(); + testIndexTemplatePresent(); + testWrapperTemplatePresent(); + testWrongExtension(); + testBareNameHbsRejected(); + } +}; + +TEST_SUITE( + AddonGeneratorsTest, + "clang.mrdocs.hbs.AddonGenerators"); + +} // namespace mrdocs::hbs diff --git a/src/tool/GenerateAction.cpp b/src/tool/GenerateAction.cpp index 3bf50b9bbf..080aead969 100644 --- a/src/tool/GenerateAction.cpp +++ b/src/tool/GenerateAction.cpp @@ -6,6 +6,7 @@ // // Copyright (c) 2023 Vinnie Falco (vinnie.falco@gmail.com) // Copyright (c) 2024 Alan de Freitas (alandefreitas@gmail.com) +// Copyright (c) 2026 Gennaro Prota (gennaro.prota@gmail.com) // // Official repository: https://github.com/cppalliance/mrdocs // @@ -14,6 +15,7 @@ #include "ToolCompilationDatabase.hpp" #include #include +#include #include #include #include @@ -45,6 +47,17 @@ DoGenerateAction( std::shared_ptr config, ConfigImpl::load(publicSettings, dirs, threadPool)); + // -------------------------------------------------------------- + // + // Discover addon-defined generators + // + // -------------------------------------------------------------- + // Each /generator// directory that ships its own + // Handlebars layouts is registered as an additional generator + // (subject to id and layout-template checks) before the user- + // requested generator is looked up below. + MRDOCS_TRY(hbs::discoverAddonGenerators(config->settings())); + // -------------------------------------------------------------- // // Load generator @@ -53,10 +66,10 @@ DoGenerateAction( auto& settings = config->settings(); MRDOCS_TRY( Generator const& generator, - findGenerator(to_string(settings.generator)), + findGenerator(settings.generator), formatError( "the Generator \"{}\" was not found", - to_string(config->settings().generator))); + settings.generator)); // -------------------------------------------------------------- // diff --git a/test-files/template-only-generators/mock-md/addons/generator/mock-md/layouts/index.mock-md.hbs b/test-files/template-only-generators/mock-md/addons/generator/mock-md/layouts/index.mock-md.hbs new file mode 100644 index 0000000000..300198a752 --- /dev/null +++ b/test-files/template-only-generators/mock-md/addons/generator/mock-md/layouts/index.mock-md.hbs @@ -0,0 +1 @@ +{{symbol.name}} diff --git a/test-files/template-only-generators/mock-md/addons/generator/mock-md/layouts/wrapper.mock-md.hbs b/test-files/template-only-generators/mock-md/addons/generator/mock-md/layouts/wrapper.mock-md.hbs new file mode 100644 index 0000000000..2a631060d4 --- /dev/null +++ b/test-files/template-only-generators/mock-md/addons/generator/mock-md/layouts/wrapper.mock-md.hbs @@ -0,0 +1 @@ +{{{contents}}} diff --git a/test-files/template-only-generators/mock-md/addons/generator/mock-md/mrdocs-generator.yml b/test-files/template-only-generators/mock-md/addons/generator/mock-md/mrdocs-generator.yml new file mode 100644 index 0000000000..97fd41c2ab --- /dev/null +++ b/test-files/template-only-generators/mock-md/addons/generator/mock-md/mrdocs-generator.yml @@ -0,0 +1,2 @@ +escape: + '_': '\_' diff --git a/test-files/template-only-generators/mock-md/mrdocs.yml b/test-files/template-only-generators/mock-md/mrdocs.yml new file mode 100644 index 0000000000..af837fa4e7 --- /dev/null +++ b/test-files/template-only-generators/mock-md/mrdocs.yml @@ -0,0 +1,7 @@ +addons-supplemental: + - addons +generator: mock-md +multipage: false +show-namespaces: false +warn-if-undocumented: false +source-root: . diff --git a/test-files/template-only-generators/mock-md/simple.cpp b/test-files/template-only-generators/mock-md/simple.cpp new file mode 100644 index 0000000000..4fa78a2cd8 --- /dev/null +++ b/test-files/template-only-generators/mock-md/simple.cpp @@ -0,0 +1,5 @@ +// A trivial input that exercises the addon-defined mock-md generator. +// The function name contains an underscore so the escape rule supplied +// by mrdocs-generator.yml ('_' -> '\_') has something to act on. + +void my_function(); diff --git a/test-files/template-only-generators/mock-md/simple.mock-md b/test-files/template-only-generators/mock-md/simple.mock-md new file mode 100644 index 0000000000..a4048066d7 --- /dev/null +++ b/test-files/template-only-generators/mock-md/simple.mock-md @@ -0,0 +1,2 @@ +my\_function + diff --git a/util/generate-config-info.py b/util/generate-config-info.py index c9ecb3004c..90c8154e92 100644 --- a/util/generate-config-info.py +++ b/util/generate-config-info.py @@ -63,7 +63,6 @@ def get_flat_suboptions(option_name, options): def get_valid_enum_categories(): valid_enum_cats = { - 'generator': ["adoc", "html", "xml"], 'log-level': ["trace", "debug", "info", "warn", "error", "fatal"], 'base-member-inheritance': ["never", "reference", "copy-dependencies", "copy-all"], 'sort-symbol-by': ["name", "location"]