///===----------------------------------------------------------------------===// // // Part of the LLVM Project, 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 // //===----------------------------------------------------------------------===// /// /// \file /// This file contains the implementation of the MustacheHTMLGenerator class, /// which is Clang-Doc generator for HTML using Mustache templates. /// //===----------------------------------------------------------------------===// #include "Generators.h" #include "Representation.h" #include "support/File.h" #include "llvm/Support/Error.h" #include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/Mustache.h" #include "llvm/Support/Path.h" #include "llvm/Support/TimeProfiler.h" using namespace llvm; using namespace llvm::json; using namespace llvm::mustache; namespace clang { namespace doc { static Error createFileOpenError(StringRef FileName, std::error_code EC) { return createFileError("cannot open file " + FileName, EC); } class MustacheHTMLGenerator : public Generator { public: static const char *Format; Error generateDocs(StringRef RootDir, StringMap> Infos, const ClangDocContext &CDCtx) override; Error createResources(ClangDocContext &CDCtx) override; Error generateDocForInfo(Info *I, raw_ostream &OS, const ClangDocContext &CDCtx) override; }; class MustacheTemplateFile : public Template { public: static Expected> createMustacheFile(StringRef FileName) { ErrorOr> BufferOrError = MemoryBuffer::getFile(FileName); if (auto EC = BufferOrError.getError()) return createFileOpenError(FileName, EC); std::unique_ptr Buffer = std::move(BufferOrError.get()); StringRef FileContent = Buffer->getBuffer(); return std::make_unique(FileContent); } Error registerPartialFile(StringRef Name, StringRef FileName) { ErrorOr> BufferOrError = MemoryBuffer::getFile(FileName); if (auto EC = BufferOrError.getError()) return createFileOpenError(FileName, EC); std::unique_ptr Buffer = std::move(BufferOrError.get()); StringRef FileContent = Buffer->getBuffer(); registerPartial(Name.str(), FileContent.str()); return Error::success(); } MustacheTemplateFile(StringRef TemplateStr) : Template(TemplateStr) {} }; static std::unique_ptr NamespaceTemplate = nullptr; static std::unique_ptr RecordTemplate = nullptr; static Error setupTemplate(std::unique_ptr &Template, StringRef TemplatePath, std::vector> Partials) { auto T = MustacheTemplateFile::createMustacheFile(TemplatePath); if (Error Err = T.takeError()) return Err; Template = std::move(T.get()); for (const auto &[Name, FileName] : Partials) if (auto Err = Template->registerPartialFile(Name, FileName)) return Err; return Error::success(); } static Error setupTemplateFiles(const clang::doc::ClangDocContext &CDCtx) { // Template files need to use the native path when they're opened, // but have to be used in POSIX style when used in HTML. auto ConvertToNative = [](std::string &&Path) -> std::string { SmallString<128> PathBuf(Path); llvm::sys::path::native(PathBuf); return PathBuf.str().str(); }; std::string NamespaceFilePath = ConvertToNative(CDCtx.MustacheTemplates.lookup("namespace-template")); std::string ClassFilePath = ConvertToNative(CDCtx.MustacheTemplates.lookup("class-template")); std::string CommentFilePath = ConvertToNative(CDCtx.MustacheTemplates.lookup("comment-template")); std::string FunctionFilePath = ConvertToNative(CDCtx.MustacheTemplates.lookup("function-template")); std::string EnumFilePath = ConvertToNative(CDCtx.MustacheTemplates.lookup("enum-template")); std::vector> Partials = { {"Comments", CommentFilePath}, {"FunctionPartial", FunctionFilePath}, {"EnumPartial", EnumFilePath}}; if (Error Err = setupTemplate(NamespaceTemplate, NamespaceFilePath, Partials)) return Err; if (Error Err = setupTemplate(RecordTemplate, ClassFilePath, Partials)) return Err; return Error::success(); } Error MustacheHTMLGenerator::generateDocs( StringRef RootDir, StringMap> Infos, const clang::doc::ClangDocContext &CDCtx) { { llvm::TimeTraceScope TS("Setup Templates"); if (auto Err = setupTemplateFiles(CDCtx)) return Err; } // Track which directories we already tried to create. StringSet<> CreatedDirs; // Collect all output by file name and create the necessary directories. StringMap> FileToInfos; for (const auto &Group : Infos) { llvm::TimeTraceScope TS("setup directories"); doc::Info *Info = Group.getValue().get(); SmallString<128> Path; sys::path::native(RootDir, Path); sys::path::append(Path, Info->getRelativeFilePath("")); if (!CreatedDirs.contains(Path)) { if (std::error_code EC = sys::fs::create_directories(Path)) return createStringError(EC, "failed to create directory '%s'.", Path.c_str()); CreatedDirs.insert(Path); } sys::path::append(Path, Info->getFileBaseName() + ".html"); FileToInfos[Path].push_back(Info); } { llvm::TimeTraceScope TS("Generate Docs"); for (const auto &Group : FileToInfos) { llvm::TimeTraceScope TS("Info to Doc"); std::error_code FileErr; raw_fd_ostream InfoOS(Group.getKey(), FileErr, sys::fs::OF_None); if (FileErr) return createFileOpenError(Group.getKey(), FileErr); for (const auto &Info : Group.getValue()) if (Error Err = generateDocForInfo(Info, InfoOS, CDCtx)) return Err; } } return Error::success(); } static json::Value extractValue(const Location &L, std::optional RepositoryUrl = std::nullopt) { Object Obj = Object(); // TODO: Consider using both Start/End line numbers to improve location report Obj.insert({"LineNumber", L.StartLineNumber}); Obj.insert({"Filename", L.Filename}); if (!L.IsFileInRootDir || !RepositoryUrl) return Obj; SmallString<128> FileURL(*RepositoryUrl); sys::path::append(FileURL, sys::path::Style::posix, L.Filename); FileURL += "#" + std::to_string(L.StartLineNumber); Obj.insert({"FileURL", FileURL}); return Obj; } static json::Value extractValue(const Reference &I, StringRef CurrentDirectory) { SmallString<64> Path = I.getRelativeFilePath(CurrentDirectory); sys::path::append(Path, I.getFileBaseName() + ".html"); sys::path::native(Path, sys::path::Style::posix); Object Obj = Object(); Obj.insert({"Link", Path}); Obj.insert({"Name", I.Name}); Obj.insert({"QualName", I.QualName}); Obj.insert({"ID", toHex(toStringRef(I.USR))}); return Obj; } static json::Value extractValue(const TypedefInfo &I) { // Not Supported return nullptr; } static json::Value extractValue(const CommentInfo &I) { Object Obj = Object(); json::Value ChildVal = Object(); Object &Child = *ChildVal.getAsObject(); json::Value ChildArr = Array(); auto &CARef = *ChildArr.getAsArray(); CARef.reserve(I.Children.size()); for (const auto &C : I.Children) CARef.emplace_back(extractValue(*C)); switch (I.Kind) { case CommentKind::CK_TextComment: { Obj.insert({commentKindToString(I.Kind), I.Text}); return Obj; } case CommentKind::CK_BlockCommandComment: { Child.insert({"Command", I.Name}); Child.insert({"Children", ChildArr}); Obj.insert({commentKindToString(I.Kind), ChildVal}); return Obj; } case CommentKind::CK_InlineCommandComment: { json::Value ArgsArr = Array(); auto &ARef = *ArgsArr.getAsArray(); ARef.reserve(I.Args.size()); for (const auto &Arg : I.Args) ARef.emplace_back(Arg); Child.insert({"Command", I.Name}); Child.insert({"Args", ArgsArr}); Child.insert({"Children", ChildArr}); Obj.insert({commentKindToString(I.Kind), ChildVal}); return Obj; } case CommentKind::CK_ParamCommandComment: case CommentKind::CK_TParamCommandComment: { Child.insert({"ParamName", I.ParamName}); Child.insert({"Direction", I.Direction}); Child.insert({"Explicit", I.Explicit}); Child.insert({"Children", ChildArr}); Obj.insert({commentKindToString(I.Kind), ChildVal}); return Obj; } case CommentKind::CK_VerbatimBlockComment: { Child.insert({"Text", I.Text}); if (!I.CloseName.empty()) Child.insert({"CloseName", I.CloseName}); Child.insert({"Children", ChildArr}); Obj.insert({commentKindToString(I.Kind), ChildVal}); return Obj; } case CommentKind::CK_VerbatimBlockLineComment: case CommentKind::CK_VerbatimLineComment: { Child.insert({"Text", I.Text}); Child.insert({"Children", ChildArr}); Obj.insert({commentKindToString(I.Kind), ChildVal}); return Obj; } case CommentKind::CK_HTMLStartTagComment: { json::Value AttrKeysArray = json::Array(); json::Value AttrValuesArray = json::Array(); auto &KeyArr = *AttrKeysArray.getAsArray(); auto &ValArr = *AttrValuesArray.getAsArray(); KeyArr.reserve(I.AttrKeys.size()); ValArr.reserve(I.AttrValues.size()); for (const auto &K : I.AttrKeys) KeyArr.emplace_back(K); for (const auto &V : I.AttrValues) ValArr.emplace_back(V); Child.insert({"Name", I.Name}); Child.insert({"SelfClosing", I.SelfClosing}); Child.insert({"AttrKeys", AttrKeysArray}); Child.insert({"AttrValues", AttrValuesArray}); Child.insert({"Children", ChildArr}); Obj.insert({commentKindToString(I.Kind), ChildVal}); return Obj; } case CommentKind::CK_HTMLEndTagComment: { Child.insert({"Name", I.Name}); Child.insert({"Children", ChildArr}); Obj.insert({commentKindToString(I.Kind), ChildVal}); return Obj; } case CommentKind::CK_FullComment: case CommentKind::CK_ParagraphComment: { Child.insert({"Children", ChildArr}); Obj.insert({commentKindToString(I.Kind), ChildVal}); return Obj; } case CommentKind::CK_Unknown: { Obj.insert({commentKindToString(I.Kind), I.Text}); return Obj; } } llvm_unreachable("Unknown comment kind encountered."); } static void maybeInsertLocation(std::optional Loc, const ClangDocContext &CDCtx, Object &Obj) { if (!Loc) return; Location L = *Loc; Obj.insert({"Location", extractValue(L, CDCtx.RepositoryUrl)}); } static void extractDescriptionFromInfo(ArrayRef Descriptions, json::Object &EnumValObj) { if (Descriptions.empty()) return; json::Value DescArr = Array(); json::Array &DescARef = *DescArr.getAsArray(); DescARef.reserve(Descriptions.size()); for (const CommentInfo &Child : Descriptions) DescARef.emplace_back(extractValue(Child)); EnumValObj.insert({"EnumValueComments", DescArr}); } static json::Value extractValue(const FunctionInfo &I, StringRef ParentInfoDir, const ClangDocContext &CDCtx) { Object Obj = Object(); Obj.insert({"Name", I.Name}); Obj.insert({"ID", toHex(toStringRef(I.USR))}); Obj.insert({"Access", getAccessSpelling(I.Access).str()}); Obj.insert({"ReturnType", extractValue(I.ReturnType.Type, ParentInfoDir)}); json::Value ParamArr = Array(); json::Array &ParamARef = *ParamArr.getAsArray(); ParamARef.reserve(I.Params.size()); for (const auto Val : enumerate(I.Params)) { json::Value V = Object(); auto &VRef = *V.getAsObject(); VRef.insert({"Name", Val.value().Name}); VRef.insert({"Type", Val.value().Type.Name}); VRef.insert({"End", Val.index() + 1 == I.Params.size()}); ParamARef.emplace_back(V); } Obj.insert({"Params", ParamArr}); maybeInsertLocation(I.DefLoc, CDCtx, Obj); return Obj; } static json::Value extractValue(const EnumInfo &I, const ClangDocContext &CDCtx) { Object Obj = Object(); std::string EnumType = I.Scoped ? "enum class " : "enum "; EnumType += I.Name; bool HasComment = llvm::any_of( I.Members, [](const EnumValueInfo &M) { return !M.Description.empty(); }); Obj.insert({"EnumName", EnumType}); Obj.insert({"HasComment", HasComment}); Obj.insert({"ID", toHex(toStringRef(I.USR))}); json::Value EnumArr = Array(); json::Array &EnumARef = *EnumArr.getAsArray(); EnumARef.reserve(I.Members.size()); for (const EnumValueInfo &M : I.Members) { json::Value EnumValue = Object(); auto &EnumValObj = *EnumValue.getAsObject(); EnumValObj.insert({"Name", M.Name}); if (!M.ValueExpr.empty()) EnumValObj.insert({"ValueExpr", M.ValueExpr}); else EnumValObj.insert({"Value", M.Value}); extractDescriptionFromInfo(M.Description, EnumValObj); EnumARef.emplace_back(EnumValue); } Obj.insert({"EnumValues", EnumArr}); extractDescriptionFromInfo(I.Description, Obj); maybeInsertLocation(I.DefLoc, CDCtx, Obj); return Obj; } static void extractScopeChildren(const ScopeChildren &S, Object &Obj, StringRef ParentInfoDir, const ClangDocContext &CDCtx) { json::Value NamespaceArr = Array(); json::Array &NamespaceARef = *NamespaceArr.getAsArray(); NamespaceARef.reserve(S.Namespaces.size()); for (const Reference &Child : S.Namespaces) NamespaceARef.emplace_back(extractValue(Child, ParentInfoDir)); if (!NamespaceARef.empty()) Obj.insert({"Namespace", Object{{"Links", NamespaceArr}}}); json::Value RecordArr = Array(); json::Array &RecordARef = *RecordArr.getAsArray(); RecordARef.reserve(S.Records.size()); for (const Reference &Child : S.Records) RecordARef.emplace_back(extractValue(Child, ParentInfoDir)); if (!RecordARef.empty()) Obj.insert({"Record", Object{{"Links", RecordArr}}}); json::Value FunctionArr = Array(); json::Array &FunctionARef = *FunctionArr.getAsArray(); FunctionARef.reserve(S.Functions.size()); json::Value PublicFunctionArr = Array(); json::Array &PublicFunctionARef = *PublicFunctionArr.getAsArray(); PublicFunctionARef.reserve(S.Functions.size()); json::Value ProtectedFunctionArr = Array(); json::Array &ProtectedFunctionARef = *ProtectedFunctionArr.getAsArray(); ProtectedFunctionARef.reserve(S.Functions.size()); for (const FunctionInfo &Child : S.Functions) { json::Value F = extractValue(Child, ParentInfoDir, CDCtx); AccessSpecifier Access = Child.Access; if (Access == AccessSpecifier::AS_public) PublicFunctionARef.emplace_back(F); else if (Access == AccessSpecifier::AS_protected) ProtectedFunctionARef.emplace_back(F); else FunctionARef.emplace_back(F); } if (!FunctionARef.empty()) Obj.insert({"Function", Object{{"Obj", FunctionArr}}}); if (!PublicFunctionARef.empty()) Obj.insert({"PublicFunction", Object{{"Obj", PublicFunctionArr}}}); if (!ProtectedFunctionARef.empty()) Obj.insert({"ProtectedFunction", Object{{"Obj", ProtectedFunctionArr}}}); json::Value EnumArr = Array(); auto &EnumARef = *EnumArr.getAsArray(); EnumARef.reserve(S.Enums.size()); for (const EnumInfo &Child : S.Enums) EnumARef.emplace_back(extractValue(Child, CDCtx)); if (!EnumARef.empty()) Obj.insert({"Enums", Object{{"Obj", EnumArr}}}); json::Value TypedefArr = Array(); auto &TypedefARef = *TypedefArr.getAsArray(); TypedefARef.reserve(S.Typedefs.size()); for (const TypedefInfo &Child : S.Typedefs) TypedefARef.emplace_back(extractValue(Child)); if (!TypedefARef.empty()) Obj.insert({"Typedefs", Object{{"Obj", TypedefArr}}}); } static json::Value extractValue(const NamespaceInfo &I, const ClangDocContext &CDCtx) { Object NamespaceValue = Object(); std::string InfoTitle = I.Name.empty() ? "Global Namespace" : (Twine("namespace ") + I.Name).str(); SmallString<64> BasePath = I.getRelativeFilePath(""); NamespaceValue.insert({"NamespaceTitle", InfoTitle}); NamespaceValue.insert({"NamespacePath", BasePath}); extractDescriptionFromInfo(I.Description, NamespaceValue); extractScopeChildren(I.Children, NamespaceValue, BasePath, CDCtx); return NamespaceValue; } static json::Value extractValue(const RecordInfo &I, const ClangDocContext &CDCtx) { Object RecordValue = Object(); extractDescriptionFromInfo(I.Description, RecordValue); RecordValue.insert({"Name", I.Name}); RecordValue.insert({"FullName", I.FullName}); RecordValue.insert({"RecordType", getTagType(I.TagType)}); maybeInsertLocation(I.DefLoc, CDCtx, RecordValue); SmallString<64> BasePath = I.getRelativeFilePath(""); extractScopeChildren(I.Children, RecordValue, BasePath, CDCtx); json::Value PublicMembers = Array(); json::Array &PubMemberRef = *PublicMembers.getAsArray(); PubMemberRef.reserve(I.Members.size()); json::Value ProtectedMembers = Array(); json::Array &ProtMemberRef = *ProtectedMembers.getAsArray(); ProtMemberRef.reserve(I.Members.size()); json::Value PrivateMembers = Array(); json::Array &PrivMemberRef = *PrivateMembers.getAsArray(); PrivMemberRef.reserve(I.Members.size()); for (const MemberTypeInfo &Member : I.Members) { json::Value MemberValue = Object(); auto &MVRef = *MemberValue.getAsObject(); MVRef.insert({"Name", Member.Name}); MVRef.insert({"Type", Member.Type.Name}); extractDescriptionFromInfo(Member.Description, MVRef); if (Member.Access == AccessSpecifier::AS_public) PubMemberRef.emplace_back(MemberValue); else if (Member.Access == AccessSpecifier::AS_protected) ProtMemberRef.emplace_back(MemberValue); else if (Member.Access == AccessSpecifier::AS_private) ProtMemberRef.emplace_back(MemberValue); } if (!PubMemberRef.empty()) RecordValue.insert({"PublicMembers", Object{{"Obj", PublicMembers}}}); if (!ProtMemberRef.empty()) RecordValue.insert({"ProtectedMembers", Object{{"Obj", ProtectedMembers}}}); if (!PrivMemberRef.empty()) RecordValue.insert({"PrivateMembers", Object{{"Obj", PrivateMembers}}}); return RecordValue; } static Error setupTemplateValue(const ClangDocContext &CDCtx, json::Value &V, Info *I) { V.getAsObject()->insert({"ProjectName", CDCtx.ProjectName}); json::Value StylesheetArr = Array(); auto InfoPath = I->getRelativeFilePath(""); SmallString<128> RelativePath = computeRelativePath("", InfoPath); sys::path::native(RelativePath, sys::path::Style::posix); auto *SSA = StylesheetArr.getAsArray(); SSA->reserve(CDCtx.UserStylesheets.size()); for (const auto &FilePath : CDCtx.UserStylesheets) { SmallString<128> StylesheetPath = RelativePath; sys::path::append(StylesheetPath, sys::path::Style::posix, sys::path::filename(FilePath)); SSA->emplace_back(StylesheetPath); } V.getAsObject()->insert({"Stylesheets", StylesheetArr}); json::Value ScriptArr = Array(); auto *SCA = ScriptArr.getAsArray(); SCA->reserve(CDCtx.JsScripts.size()); for (auto Script : CDCtx.JsScripts) { SmallString<128> JsPath = RelativePath; sys::path::append(JsPath, sys::path::Style::posix, sys::path::filename(Script)); SCA->emplace_back(JsPath); } V.getAsObject()->insert({"Scripts", ScriptArr}); return Error::success(); } Error MustacheHTMLGenerator::generateDocForInfo(Info *I, raw_ostream &OS, const ClangDocContext &CDCtx) { switch (I->IT) { case InfoType::IT_namespace: { json::Value V = extractValue(*static_cast(I), CDCtx); if (auto Err = setupTemplateValue(CDCtx, V, I)) return Err; assert(NamespaceTemplate && "NamespaceTemplate is nullptr."); NamespaceTemplate->render(V, OS); break; } case InfoType::IT_record: { json::Value V = extractValue(*static_cast(I), CDCtx); if (auto Err = setupTemplateValue(CDCtx, V, I)) return Err; // Serialize the JSON value to the output stream in a readable format. RecordTemplate->render(V, OS); break; } case InfoType::IT_enum: OS << "IT_enum\n"; break; case InfoType::IT_function: OS << "IT_Function\n"; break; case InfoType::IT_typedef: OS << "IT_typedef\n"; break; case InfoType::IT_concept: break; case InfoType::IT_variable: case InfoType::IT_friend: break; case InfoType::IT_default: return createStringError(inconvertibleErrorCode(), "unexpected InfoType"); } return Error::success(); } Error MustacheHTMLGenerator::createResources(ClangDocContext &CDCtx) { for (const auto &FilePath : CDCtx.UserStylesheets) if (Error Err = copyFile(FilePath, CDCtx.OutDirectory)) return Err; for (const auto &FilePath : CDCtx.JsScripts) if (Error Err = copyFile(FilePath, CDCtx.OutDirectory)) return Err; return Error::success(); } const char *MustacheHTMLGenerator::Format = "mustache"; static GeneratorRegistry::Add MHTML(MustacheHTMLGenerator::Format, "Generator for mustache HTML output."); // This anchor is used to force the linker to link in the generated object // file and thus register the generator. volatile int MHTMLGeneratorAnchorSource = 0; } // namespace doc } // namespace clang