1 /** 2 Generates offline documentation in the form of HTML files. 3 4 Copyright: © 2012 RejectedSoftware e.K. 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig 7 */ 8 module ddox.htmlgenerator; 9 10 import ddox.api; 11 import ddox.entities; 12 import ddox.settings; 13 14 import std.array; 15 import std.format : formattedWrite; 16 import std.string : startsWith, toLower; 17 import std.variant; 18 import vibe.core.log; 19 import vibe.core.file; 20 import vibe.core.stream; 21 import vibe.inet.path; 22 import vibe.http.server; 23 import vibe.templ.diet; 24 25 26 /* 27 structure: 28 /index.html 29 /pack1/pack2/module1.html 30 /pack1/pack2/module1/member.html 31 /pack1/pack2/module1/member.submember.html 32 */ 33 34 void generateHtmlDocs(Path dst_path, Package root, GeneratorSettings settings = null) 35 { 36 import std.algorithm : splitter; 37 import vibe.web.common : adjustMethodStyle; 38 39 if( !settings ) settings = new GeneratorSettings; 40 41 string linkTo(Entity ent, size_t level) 42 { 43 auto dst = appender!string(); 44 45 if( level ) foreach( i; 0 .. level ) dst.put("../"); 46 else dst.put("./"); 47 48 if( ent !is null ){ 49 if( !ent.parent ){ 50 dst.put("index.html"); 51 return dst.data(); 52 } 53 54 auto dp = cast(VariableDeclaration)ent; 55 auto dfn = ent.parent ? cast(FunctionDeclaration)ent.parent : null; 56 if( dp && dfn ) ent = ent.parent; 57 58 Entity[] nodes; 59 size_t mod_idx = 0; 60 while( ent ){ 61 if( cast(Module)ent ) mod_idx = nodes.length; 62 nodes ~= ent; 63 ent = ent.parent; 64 } 65 foreach_reverse(i, n; nodes[mod_idx .. $-1]){ 66 dst.put(n.name); 67 if( i > 0 ) dst.put('/'); 68 } 69 if( mod_idx == 0 ) dst.put(".html"); 70 else { 71 dst.put('/'); 72 foreach_reverse(n; nodes[0 .. mod_idx]){ 73 dst.put(adjustMethodStyle(n.name, settings.fileNameStyle)); 74 dst.put('.'); 75 } 76 dst.put("html"); 77 } 78 79 // FIXME: must also work for multiple function overloads in separate doc groups! 80 if( dp && dfn ){ 81 dst.put('#'); 82 dst.put(dp.name); 83 } 84 } 85 86 return dst.data(); 87 } 88 89 void collectChildren(Entity parent, ref DocGroup[][string] pages) 90 { 91 Declaration[] members; 92 if (auto mod = cast(Module)parent) members = mod.members; 93 else if (auto ctd = cast(CompositeTypeDeclaration)parent) members = ctd.members; 94 else if (auto td = cast(TemplateDeclaration)parent) members = td.members; 95 96 foreach (decl; members) { 97 auto style = settings.fileNameStyle; // workaround for invalid value when directly used inside lamba 98 auto name = decl.nestedName.splitter(".").map!(n => adjustMethodStyle(n, style)).join("."); 99 auto pl = name in pages; 100 if (pl && !canFind(*pl, decl.docGroup)) *pl ~= decl.docGroup; 101 else if (!pl) pages[name] = [decl.docGroup]; 102 103 collectChildren(decl, pages); 104 } 105 } 106 107 void visitModule(Module mod, Path pack_path) 108 { 109 auto modpath = pack_path ~ PathEntry(mod.name); 110 if (!existsFile(modpath)) createDirectory(modpath); 111 logInfo("Generating module: %s", mod.qualifiedName); 112 auto file = openFile(pack_path ~ PathEntry(mod.name~".html"), FileMode.createTrunc); 113 scope(exit) file.close(); 114 generateModulePage(file, root, mod, settings, ent => linkTo(ent, pack_path.length-dst_path.length)); 115 116 DocGroup[][string] pages; 117 collectChildren(mod, pages); 118 foreach (name, decls; pages) { 119 auto file = openFile(modpath ~ PathEntry(name~".html"), FileMode.createTrunc); 120 scope(exit) file.close(); 121 generateDeclPage(file, root, mod, name, decls, settings, ent => linkTo(ent, modpath.length-dst_path.length)); 122 } 123 } 124 125 void visitPackage(Package p, Path path) 126 { 127 auto packpath = p.parent ? path ~ PathEntry(p.name) : path; 128 if( !packpath.empty && !existsFile(packpath) ) createDirectory(packpath); 129 foreach( sp; p.packages ) visitPackage(sp, packpath); 130 foreach( m; p.modules ) visitModule(m, packpath); 131 } 132 133 dst_path.normalize(); 134 135 if( !dst_path.empty && !existsFile(dst_path) ) createDirectory(dst_path); 136 137 { 138 auto idxfile = openFile(dst_path ~ PathEntry("index.html"), FileMode.createTrunc); 139 scope(exit) idxfile.close(); 140 generateApiIndex(idxfile, root, settings, ent => linkTo(ent, 0)); 141 } 142 143 { 144 auto symfile = openFile(dst_path ~ "symbols.js", FileMode.createTrunc); 145 scope(exit) symfile.close(); 146 generateSymbolsJS(symfile, root, settings, ent => linkTo(ent, 0)); 147 } 148 149 { 150 auto smfile = openFile(dst_path ~ PathEntry("sitemap.xml"), FileMode.createTrunc); 151 scope(exit) smfile.close(); 152 generateSitemap(smfile, root, settings, ent => linkTo(ent, 0)); 153 } 154 155 visitPackage(root, dst_path); 156 } 157 158 class DocPageInfo { 159 string delegate(Entity ent) linkTo; 160 GeneratorSettings settings; 161 Package rootPackage; 162 Entity node; 163 Module mod; 164 DocGroup[] docGroups; // for multiple doc groups with the same name 165 string nestedName; 166 167 @property NavigationType navigationType() const { return settings.navigationType; } 168 string formatType(Type tp, bool include_code_tags = true) { return .formatType(tp, linkTo, include_code_tags); } 169 string formatDoc(DocGroup group, int hlevel, bool delegate(string) display_section) 170 { 171 return group.comment.renderSections(new DocGroupContext(group, linkTo), display_section, hlevel); 172 } 173 } 174 175 void generateSitemap(OutputStream dst, Package root_package, GeneratorSettings settings, string delegate(Entity) link_to, HTTPServerRequest req = null) 176 { 177 dst.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); 178 dst.write("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n"); 179 180 void writeEntry(string[] parts...){ 181 dst.write("<url><loc>"); 182 foreach( p; parts ) 183 dst.write(p); 184 dst.write("</loc></url>\n"); 185 } 186 187 void writeEntityRec(Entity ent){ 188 import std.string; 189 if( !cast(Package)ent || ent is root_package ){ 190 auto link = link_to(ent); 191 if( indexOf(link, '#') < 0 ) // ignore URLs with anchors 192 writeEntry((settings.siteUrl ~ Path(link)).toString()); 193 } 194 ent.iterateChildren((ch){ writeEntityRec(ch); return true; }); 195 } 196 197 writeEntityRec(root_package); 198 199 dst.write("</urlset>\n"); 200 dst.flush(); 201 } 202 203 void generateSymbolsJS(OutputStream dst, Package root_package, GeneratorSettings settings, string delegate(Entity) link_to) 204 { 205 bool[string] visited; 206 207 void writeEntry(Entity ent) { 208 if (cast(Package)ent || cast(TemplateParameterDeclaration)ent) return; 209 if (ent.qualifiedName in visited) return; 210 visited[ent.qualifiedName] = true; 211 212 string kind = ent.classinfo.name.split(".")[$-1].toLower; 213 string[] attributes; 214 if (auto fdecl = cast(FunctionDeclaration)ent) attributes = fdecl.attributes; 215 else if (auto adecl = cast(AliasDeclaration)ent) attributes = adecl.attributes; 216 else if (auto tdecl = cast(TypedDeclaration)ent) attributes = tdecl.type.attributes; 217 attributes = attributes.map!(a => a.startsWith("@") ? a[1 .. $] : a).array; 218 dst.formattedWrite(`{name: "%s", kind: "%s", path: "%s", attributes: %s},`, ent.qualifiedName, kind, link_to(ent), attributes); 219 dst.put('\n'); 220 } 221 222 void writeEntryRec(Entity ent) { 223 writeEntry(ent); 224 if (cast(FunctionDeclaration)ent) return; 225 ent.iterateChildren((ch) { writeEntryRec(ch); return true; }); 226 } 227 228 dst.write("// symbol index generated by DDOX - do not edit\n"); 229 dst.write("var symbols = [\n"); 230 writeEntryRec(root_package); 231 dst.write("];\n"); 232 } 233 234 void generateApiIndex(OutputStream dst, Package root_package, GeneratorSettings settings, string delegate(Entity) link_to, HTTPServerRequest req = null) 235 { 236 auto info = new DocPageInfo; 237 info.linkTo = link_to; 238 info.settings = settings; 239 info.rootPackage = root_package; 240 info.node = root_package; 241 242 dst.parseDietFileCompat!("ddox.overview.dt", 243 HTTPServerRequest, "req", 244 DocPageInfo, "info") 245 (Variant(req), Variant(info)); 246 } 247 248 void generateModulePage(OutputStream dst, Package root_package, Module mod, GeneratorSettings settings, string delegate(Entity) link_to, HTTPServerRequest req = null) 249 { 250 auto info = new DocPageInfo; 251 info.linkTo = link_to; 252 info.settings = settings; 253 info.rootPackage = root_package; 254 info.mod = mod; 255 info.node = mod; 256 info.docGroups = null; 257 258 dst.parseDietFile!("ddox.module.dt", req, info); 259 } 260 261 void generateDeclPage(OutputStream dst, Package root_package, Module mod, string nested_name, DocGroup[] docgroups, GeneratorSettings settings, string delegate(Entity) link_to, HTTPServerRequest req = null) 262 { 263 import std.algorithm : sort; 264 265 auto info = new DocPageInfo; 266 info.linkTo = link_to; 267 info.settings = settings; 268 info.rootPackage = root_package; 269 info.mod = mod; 270 info.node = mod; 271 info.docGroups = docgroups;//docGroups(mod.lookupAll!Declaration(nested_name)); 272 sort!((a, b) => cmpKind(a.members[0], b.members[0]))(info.docGroups); 273 info.nestedName = nested_name; 274 275 dst.parseDietFile!("ddox.docpage.dt", req, info); 276 } 277 278 private bool cmpKind(Entity a, Entity b) 279 { 280 static immutable kinds = [ 281 DeclarationKind.Variable, 282 DeclarationKind.Function, 283 DeclarationKind.Struct, 284 DeclarationKind.Union, 285 DeclarationKind.Class, 286 DeclarationKind.Interface, 287 DeclarationKind.Enum, 288 DeclarationKind.EnumMember, 289 DeclarationKind.Template, 290 DeclarationKind.TemplateParameter, 291 DeclarationKind.Alias 292 ]; 293 294 auto ad = cast(Declaration)a; 295 auto bd = cast(Declaration)b; 296 297 if (!ad && !bd) return false; 298 if (!ad) return false; 299 if (!bd) return true; 300 301 auto ak = kinds.countUntil(ad.kind); 302 auto bk = kinds.countUntil(bd.kind); 303 304 if (ak < 0 && bk < 0) return false; 305 if (ak < 0) return false; 306 if (bk < 0) return true; 307 308 return ak < bk; 309 }