diff --git a/docs/mrdocs.schema.json b/docs/mrdocs.schema.json index 5a3645549..a2fa5bcab 100644 --- a/docs/mrdocs.schema.json +++ b/docs/mrdocs.schema.json @@ -244,6 +244,12 @@ "title": "System include paths", "type": "array" }, + "tagfile": { + "default": "/reference.tag.xml", + "description": "Specifies the full path (filename) where the generated tagfile should be saved. If left empty, no tagfile will be generated.", + "title": "Path for the tagfile", + "type": "string" + }, "use-system-stdlib": { "default": false, "description": "True if the compiler has to use just the system standard library. When set to true, the compiler uses the system standard library instead of the standard library provided by the compiler.", diff --git a/include/mrdocs/Corpus.hpp b/include/mrdocs/Corpus.hpp index 5eedb0fe9..e48befbe4 100644 --- a/include/mrdocs/Corpus.hpp +++ b/include/mrdocs/Corpus.hpp @@ -143,6 +143,7 @@ class MRDOCS_VISIBLE The type of the first argument is determined by the `InfoKind` of the `Info` object. + @param I The Info to visit. @param info The Info to visit. @param f The function to invoke. @param args The arguments to pass to the function. @@ -202,6 +203,14 @@ class MRDOCS_VISIBLE getFullyQualifiedName( const Info& I, std::string& temp) const; + + std::string + getFullyQualifiedName(const Info& I) const + { + std::string temp; + return getFullyQualifiedName(I, temp); + } + }; //------------------------------------------------ diff --git a/include/mrdocs/Generator.hpp b/include/mrdocs/Generator.hpp index c869c026f..7127c0b26 100644 --- a/include/mrdocs/Generator.hpp +++ b/include/mrdocs/Generator.hpp @@ -182,6 +182,29 @@ class MRDOCS_VISIBLE Corpus const& corpus) const; }; +/** Return the full path for single page output. + + This function determines the full path for a single-page output file + based on the provided `outputPath` and file `extension`. + + If the `outputPath` already exists: + - If it is a directory, appends the default file name with the provided extension. + - If it is a file, uses the provided `outputPath` directly. + + If the `outputPath` does not exist: + - If it ends with a '/', assumes it is a directory and appends the default file name. + - Otherwise, it returns an error because the path is ambiguous. + + @return The full path or an error if the `outputPath` is ambiguous. + + @param outputPath The specified output path, which can be a directory or file. + @param extension The file extension to use for single-page output. +*/ +Expected +getSinglePageFullPath( + std::string_view outputPath, + std::string_view extension); + } // mrdocs } // clang diff --git a/src/lib/Gen/hbs/HandlebarsGenerator.cpp b/src/lib/Gen/hbs/HandlebarsGenerator.cpp index d2f55234b..e065b5878 100644 --- a/src/lib/Gen/hbs/HandlebarsGenerator.cpp +++ b/src/lib/Gen/hbs/HandlebarsGenerator.cpp @@ -15,7 +15,17 @@ #include "Builder.hpp" #include "MultiPageVisitor.hpp" #include "SinglePageVisitor.hpp" + +#include "lib/Lib/TagfileWriter.hpp" +#include "lib/Support/RawOstream.hpp" + +#include +#include +#include + #include + +#include #include namespace clang { @@ -79,7 +89,21 @@ build( { if (!corpus.config->multipage) { - return Generator::build(outputPath, corpus); + auto e = Generator::build(outputPath, corpus); + if (e.has_value() && !corpus.config->tagfile.empty()) + { + // Generate tagfile if specified + auto const singlePagePath = getSinglePageFullPath(outputPath, fileExtension()); + MRDOCS_CHECK_OR(singlePagePath, Unexpected(singlePagePath.error())); + HandlebarsCorpus hbsCorpus = createDomCorpus(*this, corpus); + MRDOCS_TRY(auto tagFileWriter, TagfileWriter::create( + hbsCorpus, + corpus.config->tagfile, + files::getFileName(*singlePagePath))); + tagFileWriter.build(); + } + + return e; } // Create corpus and executors @@ -93,6 +117,16 @@ build( // Wait for all executors to finish and check errors auto errors = ex.wait(); MRDOCS_CHECK_OR(errors.empty(), Unexpected(errors)); + + if (! corpus.config->tagfile.empty()) + { + MRDOCS_TRY(auto tagFileWriter, TagfileWriter::create( + domCorpus, + corpus.config->tagfile, + outputPath)); + tagFileWriter.build(); + } + return {}; } diff --git a/src/lib/Gen/xml/XMLTags.cpp b/src/lib/Gen/xml/XMLTags.cpp index 780ddffa7..04f90394c 100644 --- a/src/lib/Gen/xml/XMLTags.cpp +++ b/src/lib/Gen/xml/XMLTags.cpp @@ -193,6 +193,9 @@ void XMLTags:: nest(int levels) { + if (!nesting_) + return; + if(levels > 0) { indent_.append(levels * 2, ' '); diff --git a/src/lib/Gen/xml/XMLTags.hpp b/src/lib/Gen/xml/XMLTags.hpp index 31d1b6348..a1cfe9a7f 100644 --- a/src/lib/Gen/xml/XMLTags.hpp +++ b/src/lib/Gen/xml/XMLTags.hpp @@ -186,6 +186,7 @@ class XMLTags public: std::string indent_; llvm::raw_ostream& os_; + bool nesting_ = true; explicit XMLTags( @@ -201,6 +202,7 @@ class XMLTags void write(dom::String const&, llvm::StringRef value = {}, Attributes = {}); void close(dom::String const&); + void nesting(bool enable) noexcept { nesting_ = enable; } void nest(int levels); }; diff --git a/src/lib/Lib/ConfigOptions.json b/src/lib/Lib/ConfigOptions.json index 214b1c1a0..cd9ebb430 100644 --- a/src/lib/Lib/ConfigOptions.json +++ b/src/lib/Lib/ConfigOptions.json @@ -148,6 +148,15 @@ "default": "/share/mrdocs/addons", "relativeto": "" }, + { + "name": "tagfile", + "brief": "Path for the tagfile", + "details": "Specifies the full path (filename) where the generated tagfile should be saved. If left empty, no tagfile will be generated.", + "type": "file-path", + "default": "/reference.tag.xml", + "relativeto": "", + "must-exist": false + }, { "name": "legible-names", "brief": "Use legible names", diff --git a/src/lib/Lib/TagfileWriter.cpp b/src/lib/Lib/TagfileWriter.cpp new file mode 100644 index 000000000..5ef5fe017 --- /dev/null +++ b/src/lib/Lib/TagfileWriter.cpp @@ -0,0 +1,281 @@ +// +// 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) 2024 Fernando Pelliccioni (fpelliccioni@gmail.com) +// +// Official repository: https://github.com/cppalliance/mrdocs +// + +#include "../Gen/xml/CXXTags.hpp" +#include "TagfileWriter.hpp" +#include "lib/Lib/ConfigImpl.hpp" +#include "lib/Support/RawOstream.hpp" + +#include + +#include + +namespace clang { +namespace mrdocs { + +//------------------------------------------------ +// +// TagfileWriter +// +//------------------------------------------------ + +TagfileWriter:: +TagfileWriter( + hbs::HandlebarsCorpus const& corpus, + os_ptr os, + std::string_view const defaultFilename + ) noexcept + : corpus_(corpus) + , os_(std::move(os)) + , tags_(*os_) + , defaultFilename_(defaultFilename) +{ + tags_.nesting(false); +} + +Expected +TagfileWriter:: +create( + hbs::HandlebarsCorpus const& corpus, + std::string_view tagfile, + std::string_view defaultFilename) +{ + std::error_code ec; + + auto os = std::make_unique( + tagfile.data(), + ec, + llvm::sys::fs::OF_None); + + if (ec) + { + return Unexpected(formatError("llvm::raw_fd_ostream(\"{}\") failed with error: {}", tagfile, ec.message())); + } + return TagfileWriter(corpus, std::move(os), defaultFilename); +} + +void +TagfileWriter:: +build() +{ + initialize(); + operator()(corpus_->globalNamespace()); + finalize(); +} + +void +TagfileWriter:: +initialize() +{ + (*os_) << "\n"; + (*os_) << "\n"; +} + +void +TagfileWriter:: +finalize() +{ + (*os_) << "\n"; +} + + +template +void +TagfileWriter:: +operator()(T const& I) +{ + if constexpr (T::isNamespace()) + { + // Namespaces are compound elements with members + writeNamespace(I); + } + else if (!T::isFunction()) + { + // Functions are described as namespace members in the + // scoped they belong to. + // Everything else is described as a compound element of + // type "class" because it's the type doxygen supports. + writeClassLike(I); + } +} + +#define INFO(Type) template void TagfileWriter::operator()(Type##Info const&); +#include + +void +TagfileWriter:: +writeNamespace( + NamespaceInfo const& I) +{ + // Check if this namespace contains only other namespaces + bool onlyNamespaces = true; + corpus_->traverse(I, [&](Info const& I) + { + if (I.Kind != InfoKind::Namespace) + { + onlyNamespaces = false; + return false; + } + return true; + }); + + // Write the compound element for this namespace + if (!onlyNamespaces) + { + tags_.open("compound", { + { "kind", "namespace" } + }); + + tags_.write("name", corpus_->getFullyQualifiedName(I)); + tags_.write("filename", generateFilename(I)); + + // Write the class-like members of this namespace + corpus_->traverse(I, [this](U const& J) + { + if (!U::isNamespace() && !U::isFunction()) + { + tags_.write( + "class", + corpus_->getFullyQualifiedName(J), + {{"kind", "class"}}); + } + }); + + // Write the function-like members of this namespace + corpus_->traverse(I, [this](U const& J) + { + if constexpr (U::isFunction()) + { + writeFunctionMember(J); + } + }); + + tags_.close("compound"); + } + + // Write compound elements for the members of this namespace + corpus_->traverse(I, [this](U const& J) + { + this->operator()(J); + }); +} + +template +void +TagfileWriter:: +writeClassLike( + T const& I +) +{ + tags_.open("compound", { + { "kind", "class" } + }); + tags_.write("name", corpus_->getFullyQualifiedName(I)); + tags_.write("filename", generateFilename(I)); + if constexpr (T::isRecord()) + { + // Write the function-like members of this record + corpus_->traverse(I, [this](U const& J) + { + if constexpr (U::isFunction()) + { + writeFunctionMember(J); + } + }); + } + tags_.close("compound"); +} + +void +TagfileWriter:: +writeFunctionMember(FunctionInfo const& I) +{ + tags_.open("member", {{"kind", "function"}}); + tags_.write("type", toString(*I.ReturnType)); + tags_.write("name", I.Name); + auto [anchorFile, anchor] = generateFileAndAnchor(I); + tags_.write("anchorfile", anchorFile); + tags_.write("anchor", anchor); + std::string arglist = "("; + for(auto const& J : I.Params) + { + arglist += toString(*J.Type); + arglist += " "; + arglist += J.Name; + arglist += ", "; + } + if (arglist.size() > 2) { + arglist.resize(arglist.size() - 2); + } + arglist += ")"; + tags_.write("arglist", arglist); + tags_.close("member"); +} + + +template +std::string +TagfileWriter:: +generateFilename(T const& I) +{ + std::string url = corpus_.getURL(I); + // In single page mode, getURL always returns an + // anchor. + if (!corpus_->config->multipage) { + // filename is # + if (!url.starts_with('#')) + { + return defaultFilename_ + '#' + url; + } + return defaultFilename_ + url; + } + // In multipage mode, getURL returns a file path + // starting with a slash, and it may contain an anchor + if (url.starts_with('/')) + { + url.erase(0, 1); + } + return url; +} + +template +std::pair +TagfileWriter:: +generateFileAndAnchor(T const& I) +{ + std::string url = corpus_.getURL(I); + // Make relative to output dir + if (url.starts_with('/')) + { + url.erase(0, 1); + } + // In single page mode, getURL always returns an + // anchor. So the filename is the default and the + // anchor is the URL. + if (!corpus_->config->multipage) { + if (url.starts_with('#')) + { + url.erase(0, 1); + } + return {defaultFilename_, url}; + } + // In multipage mode, getURL returns a file path + // starting with a slash, and it may contain an anchor + auto const pos = url.find('#'); + if (pos == std::string::npos) + { + return {url, ""}; + } + return {url.substr(0, pos), url.substr(pos + 1)}; +} + +} // mrdocs +} // clang \ No newline at end of file diff --git a/src/lib/Lib/TagfileWriter.hpp b/src/lib/Lib/TagfileWriter.hpp new file mode 100644 index 000000000..0cb678a87 --- /dev/null +++ b/src/lib/Lib/TagfileWriter.hpp @@ -0,0 +1,118 @@ +// +// 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) 2024 Fernando Pelliccioni (fpelliccioni@gmail.com) +// +// Official repository: https://github.com/cppalliance/mrdocs +// + +#ifndef MRDOCS_LIB_TAGFILEWRITER_HPP +#define MRDOCS_LIB_TAGFILEWRITER_HPP + +#include "../Gen/xml/XMLTags.hpp" +#include +#include "lib/Gen/hbs/HandlebarsCorpus.hpp" +#include + +#include +#include + +namespace clang { +namespace mrdocs { + +struct SimpleWriterTag {}; //Tag dispatch for simple writers + +class jit_indenter; + +/** A writer which outputs Tagfiles. +*/ +class TagfileWriter +{ + using os_ptr = std::unique_ptr; + + hbs::HandlebarsCorpus const& corpus_; + os_ptr os_; + xml::XMLTags tags_; + std::string defaultFilename_; + + TagfileWriter( + hbs::HandlebarsCorpus const& corpus, + os_ptr os, + std::string_view defaultFilename) noexcept; + +public: + /** Create a TagfileWriter instance. + + This static function creates a TagfileWriter instance using the provided + HandlebarsCorpus, tagfile, and default filename. + + @param corpus The HandlebarsCorpus to use for the writer. + @param tagfile The name of the tagfile to write to. + @param defaultFilename The default filename to use for a symbol if none + is provided. Typically, the relative path to a single page output file. + This parameter is ignored in multipage mode. + @return An Expected object containing the TagfileWriter instance or an error. + */ + static + Expected + create( + hbs::HandlebarsCorpus const& corpus, + std::string_view tagfile, + std::string_view defaultFilename); + + /** Build the tagfile. + + This function builds the tagfile by initializing the output, + traversing the global namespace of the corpus, and finalizing + the output. + */ + void + build(); + +private: + // ================================================== + // Build + // ================================================== + void + initialize(); + + template + void + operator()(T const&); + + void + finalize(); + + // ================================================== + // Write + // ================================================== + void + writeNamespace(NamespaceInfo const&); + + template + void + writeClassLike(T const& I); + + void + writeFunctionMember(FunctionInfo const& I); + + // ================================================== + // URLs + // ================================================== + + template + std::string + generateFilename(T const& I); + + template + std::pair + generateFileAndAnchor(T const& I); +}; + +} // mrdocs +} // clang + +#endif \ No newline at end of file diff --git a/src/lib/Support/Generator.cpp b/src/lib/Support/Generator.cpp index ce438f391..6f932c501 100644 --- a/src/lib/Support/Generator.cpp +++ b/src/lib/Support/Generator.cpp @@ -37,22 +37,9 @@ build( std::string_view outputPath, Corpus const& corpus) const { - namespace path = llvm::sys::path; - - using SmallString = llvm::SmallString<0>; - - SmallString ext("."); - ext += fileExtension(); - - SmallString fileName(outputPath); - if(path::extension(outputPath).compare_insensitive(ext) != 0) - { - // directory specified - path::append(fileName, "reference"); - path::replace_extension(fileName, ext); - } - - return buildOne(fileName.str(), corpus); + auto const fileName = getSinglePageFullPath(outputPath, fileExtension()); + MRDOCS_CHECK_OR(fileName, Unexpected(fileName.error())); + return buildOne(*fileName, corpus); } Expected @@ -130,5 +117,44 @@ buildOneString( } } +Expected +getSinglePageFullPath( + std::string_view outputPath, + std::string_view extension) +{ + namespace path = llvm::sys::path; + using SmallString = llvm::SmallString<0>; + + SmallString ext("."); + ext += extension; + + if (files::exists(outputPath)) + { + // If the path exists and is a directory, append the default file + if (files::isDirectory(outputPath)) + { + SmallString fileName(outputPath); + path::append(fileName, "reference"); + path::replace_extension(fileName, ext); + return fileName.str().str(); + } + // If the path exists and is a file, use it directly + return std::string(outputPath); + } + + // If the path does not exist, check if it has an extension + SmallString fileName(outputPath); + if (!path::extension(fileName).empty()) + { + // Path has an extension, treat it as a file + return fileName.str().str(); + } + + // Path does not have an extension, assume it's a directory + path::append(fileName, "reference"); + path::replace_extension(fileName, ext); + return fileName.str().str(); +} + } // mrdocs } // clang