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.algorithm : canFind, countUntil, map; 15 import std.array; 16 import std.digest.md; 17 import std.format : formattedWrite; 18 import std.string : startsWith, toLower; 19 import std.traits : EnumMembers; 20 import std.variant; 21 import vibe.core.log; 22 import vibe.core.file; 23 import vibe.core.stream; 24 import vibe.data.json; 25 import vibe.inet.path; 26 import vibe.http.server; 27 import vibe.stream.wrapper : StreamOutputRange; 28 import diet.html; 29 import diet.traits : dietTraits; 30 31 32 /* 33 structure: 34 /index.html 35 /pack1/pack2/module1.html 36 /pack1/pack2/module1/member.html 37 /pack1/pack2/module1/member.submember.html 38 */ 39 40 version (Windows) version = CaseInsensitiveFS; 41 else version (OSX) version = CaseInsensitiveFS; 42 43 void generateHtmlDocs(Path dst_path, Package root, GeneratorSettings settings = null) 44 { 45 import std.algorithm : splitter; 46 import vibe.web.common : adjustMethodStyle; 47 48 if( !settings ) settings = new GeneratorSettings; 49 50 version (CaseInsensitiveFS) { 51 final switch (settings.fileNameStyle) with (MethodStyle) { 52 case unaltered, camelCase, pascalCase: 53 logWarn("On Windows and OS X, file names that differ only in their case " 54 ~ "are treated as equal by default. Use one of the " 55 ~ "lower/upper case styles with the --file-name-style " 56 ~ "option to avoid missing files in the generated output."); 57 break; 58 case lowerCase, upperCase, lowerUnderscored, upperUnderscored: 59 break; 60 } 61 } 62 63 string[string] file_hashes; 64 string[string] new_file_hashes; 65 66 const hash_file_name = dst_path ~ "file_hashes.json"; 67 if (existsFile(hash_file_name)) { 68 auto hfi = getFileInfo(hash_file_name); 69 auto hf = readFileUTF8(hash_file_name); 70 file_hashes = deserializeJson!(string[string])(hf); 71 } 72 73 string linkTo(in Entity ent_, size_t level) 74 { 75 import std.typecons : Rebindable; 76 77 auto dst = appender!string(); 78 Rebindable!(const(Entity)) ent = ent_; 79 80 if( level ) foreach( i; 0 .. level ) dst.put("../"); 81 else dst.put("./"); 82 83 if( ent !is null ){ 84 if( !ent.parent ){ 85 dst.put("index.html"); 86 return dst.data(); 87 } 88 89 auto dp = cast(const(VariableDeclaration))ent; 90 auto dfn = ent.parent ? cast(const(FunctionDeclaration))ent.parent : null; 91 if( dp && dfn ) ent = ent.parent; 92 93 const(Entity)[] nodes; 94 size_t mod_idx = 0; 95 while( ent ){ 96 if( cast(const(Module))ent ) mod_idx = nodes.length; 97 nodes ~= ent.get; 98 ent = ent.parent; 99 } 100 foreach_reverse(i, n; nodes[mod_idx .. $-1]){ 101 dst.put(n.name[]); 102 if( i > 0 ) dst.put('/'); 103 } 104 if( mod_idx == 0 ) dst.put(".html"); 105 else { 106 dst.put('/'); 107 foreach_reverse(n; nodes[0 .. mod_idx]){ 108 dst.put(adjustMethodStyle(n.name, settings.fileNameStyle)); 109 dst.put('.'); 110 } 111 dst.put("html"); 112 } 113 114 // FIXME: must also work for multiple function overloads in separate doc groups! 115 if( dp && dfn ){ 116 dst.put('#'); 117 dst.put(dp.name[]); 118 } 119 } 120 121 return dst.data(); 122 } 123 124 void collectChildren(Entity parent, ref DocGroup[][string] pages) 125 { 126 Declaration[] members; 127 if (!settings.enumMemberPages && cast(EnumDeclaration)parent) 128 return; 129 130 if (auto mod = cast(Module)parent) members = mod.members; 131 else if (auto ctd = cast(CompositeTypeDeclaration)parent) members = ctd.members; 132 else if (auto td = cast(TemplateDeclaration)parent) members = td.members; 133 134 foreach (decl; members) { 135 if (decl.parent !is parent) continue; // exclude inherited members (issue #120) 136 auto style = settings.fileNameStyle; // workaround for invalid value when directly used inside lamba 137 auto name = decl.nestedName.splitter(".").map!(n => adjustMethodStyle(n, style)).join("."); 138 auto pl = name in pages; 139 if (pl && !canFind(*pl, decl.docGroup)) *pl ~= decl.docGroup; 140 else if (!pl) pages[name] = [decl.docGroup]; 141 142 collectChildren(decl, pages); 143 } 144 } 145 146 void writeHashedFile(Path filename, scope void delegate(OutputStream) del) 147 { 148 import vibe.stream.memory; 149 assert(filename.startsWith(dst_path)); 150 151 auto str = createMemoryOutputStream(); 152 del(str); 153 auto h = md5Of(str.data).toHexString.idup; 154 auto relfilename = filename[dst_path.length .. $].toString(); 155 auto ph = relfilename in file_hashes; 156 if (!ph || *ph != h) { 157 //logInfo("do write %s", filename); 158 writeFile(filename, str.data); 159 } 160 new_file_hashes[relfilename] = h; 161 } 162 163 void visitModule(Module mod, Path pack_path) 164 { 165 auto modpath = pack_path ~ PathEntry(mod.name); 166 if (!existsFile(modpath)) createDirectory(modpath); 167 logInfo("Generating module: %s", mod.qualifiedName); 168 writeHashedFile(pack_path ~ PathEntry(mod.name~".html"), (stream) { 169 generateModulePage(stream, root, mod, settings, ent => linkTo(ent, pack_path.length-dst_path.length)); 170 }); 171 172 DocGroup[][string] pages; 173 collectChildren(mod, pages); 174 foreach (name, decls; pages) 175 writeHashedFile(modpath ~ PathEntry(name~".html"), (stream) { 176 generateDeclPage(stream, root, mod, name, decls, settings, ent => linkTo(ent, modpath.length-dst_path.length)); 177 }); 178 } 179 180 void visitPackage(Package p, Path path) 181 { 182 auto packpath = p.parent ? path ~ PathEntry(p.name) : path; 183 if( !packpath.empty && !existsFile(packpath) ) createDirectory(packpath); 184 foreach( sp; p.packages ) visitPackage(sp, packpath); 185 foreach( m; p.modules ) visitModule(m, packpath); 186 } 187 188 dst_path.normalize(); 189 190 if( !dst_path.empty && !existsFile(dst_path) ) createDirectory(dst_path); 191 192 writeHashedFile(dst_path ~ PathEntry("index.html"), (stream) { 193 generateApiIndex(stream, root, settings, ent => linkTo(ent, 0)); 194 }); 195 196 writeHashedFile(dst_path ~ "symbols.js", (stream) { 197 generateSymbolsJS(stream, root, settings, ent => linkTo(ent, 0)); 198 }); 199 200 writeHashedFile(dst_path ~ PathEntry("sitemap.xml"), (stream) { 201 generateSitemap(stream, root, settings, ent => linkTo(ent, 0)); 202 }); 203 204 visitPackage(root, dst_path); 205 206 // delete obsolete files 207 foreach (f; file_hashes.byKey) 208 if (f !in new_file_hashes) { 209 try removeFile(dst_path ~ Path(f)); 210 catch (Exception e) logWarn("Failed to remove obsolete file '%s': %s", f, e.msg); 211 } 212 213 // write new file hash list 214 writeFileUTF8(hash_file_name, new_file_hashes.serializeToJsonString()); 215 } 216 217 class DocPageInfo { 218 string delegate(in Entity ent) linkTo; 219 GeneratorSettings settings; 220 Package rootPackage; 221 Entity node; 222 Module mod; 223 DocGroup[] docGroups; // for multiple doc groups with the same name 224 string nestedName; 225 226 @property NavigationType navigationType() const { return settings.navigationType; } 227 string formatType(CachedType tp, bool include_code_tags = true) { return .formatType(tp, linkTo, include_code_tags); } 228 void renderTemplateArgs(R)(R output, Declaration decl) { .renderTemplateArgs(output, decl, linkTo); } 229 string formatDoc(DocGroup group, int hlevel, bool delegate(string) display_section) 230 { 231 if (!group) return null; 232 // TODO: memoize the DocGroupContext 233 return group.comment.renderSections(new DocGroupContext(group, linkTo, settings), display_section, hlevel); 234 } 235 } 236 237 @dietTraits 238 struct DdoxDietTraits(HTMLOutputStyle htmlStyle) { 239 // fields and functions must be static atm., see https://github.com/rejectedsoftware/diet-ng/issues/33 240 enum HTMLOutputStyle htmlOutputStyle = htmlStyle; 241 } 242 243 void generateSitemap(OutputStream dst, Package root_package, GeneratorSettings settings, string delegate(in Entity) link_to, HTTPServerRequest req = null) 244 { 245 dst.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); 246 dst.write("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n"); 247 248 void writeEntry(string[] parts...){ 249 dst.write("<url><loc>"); 250 foreach( p; parts ) 251 dst.write(p); 252 dst.write("</loc></url>\n"); 253 } 254 255 void writeEntityRec(Entity ent){ 256 import std.string; 257 if( !cast(Package)ent || ent is root_package ){ 258 auto link = link_to(ent); 259 if( indexOf(link, '#') < 0 ) // ignore URLs with anchors 260 writeEntry((settings.siteUrl ~ Path(link)).toString()); 261 } 262 ent.iterateChildren((ch){ writeEntityRec(ch); return true; }); 263 } 264 265 writeEntityRec(root_package); 266 267 dst.write("</urlset>\n"); 268 dst.flush(); 269 } 270 271 void generateSymbolsJS(OutputStream dst, Package root_package, GeneratorSettings settings, string delegate(in Entity) link_to) 272 { 273 import std.typecons : Tuple, tuple; 274 275 bool[Tuple!(Entity, CachedString)] visited; 276 277 auto rng = StreamOutputRange(dst); 278 279 void writeEntry(Entity ent) { 280 auto key = tuple(ent.parent, ent.name); 281 if (cast(Package)ent || cast(TemplateParameterDeclaration)ent) return; 282 if (key in visited) return; 283 visited[key] = true; 284 285 string kind = ent.classinfo.name.split(".")[$-1].toLower; 286 const(CachedString)[] cattributes; 287 if (auto fdecl = cast(FunctionDeclaration)ent) cattributes = fdecl.attributes; 288 else if (auto adecl = cast(AliasDeclaration)ent) cattributes = adecl.attributes; 289 else if (auto tdecl = cast(TypedDeclaration)ent) cattributes = tdecl.type.attributes; 290 auto attributes = cattributes.map!(a => a.str.startsWith("@") ? a[1 .. $] : a); 291 (&rng).formattedWrite(`{name: '%s', kind: "%s", path: '%s', attributes: %s},`, ent.qualifiedName, kind, link_to(ent), attributes); 292 rng.put('\n'); 293 } 294 295 void writeEntryRec(Entity ent) { 296 writeEntry(ent); 297 if (cast(FunctionDeclaration)ent) return; 298 ent.iterateChildren((ch) { writeEntryRec(ch); return true; }); 299 } 300 301 rng.put("// symbol index generated by DDOX - do not edit\n"); 302 rng.put("var symbols = [\n"); 303 writeEntryRec(root_package); 304 rng.put("];\n"); 305 } 306 307 void generateApiIndex(OutputStream dst, Package root_package, GeneratorSettings settings, string delegate(in Entity) link_to, HTTPServerRequest req = null) 308 { 309 auto info = new DocPageInfo; 310 info.linkTo = link_to; 311 info.settings = settings; 312 info.rootPackage = root_package; 313 info.node = root_package; 314 315 auto rng = StreamOutputRange(dst); 316 final switch (settings.htmlOutputStyle) 317 { 318 foreach (htmlOutputStyle; EnumMembers!HTMLOutputStyle) 319 case htmlOutputStyle: 320 { 321 rng.compileHTMLDietFile!("ddox.overview.dt", req, info, DdoxDietTraits!(htmlOutputStyle)); 322 return; 323 } 324 } 325 } 326 327 void generateModulePage(OutputStream dst, Package root_package, Module mod, GeneratorSettings settings, string delegate(in Entity) link_to, HTTPServerRequest req = null) 328 { 329 auto info = new DocPageInfo; 330 info.linkTo = link_to; 331 info.settings = settings; 332 info.rootPackage = root_package; 333 info.mod = mod; 334 info.node = mod; 335 info.docGroups = null; 336 337 auto rng = StreamOutputRange(dst); 338 final switch (settings.htmlOutputStyle) 339 { 340 foreach (htmlOutputStyle; EnumMembers!HTMLOutputStyle) 341 case htmlOutputStyle: 342 { 343 rng.compileHTMLDietFile!("ddox.module.dt", req, info, DdoxDietTraits!(htmlOutputStyle)); 344 return; 345 } 346 } 347 } 348 349 void generateDeclPage(OutputStream dst, Package root_package, Module mod, string nested_name, DocGroup[] docgroups, GeneratorSettings settings, string delegate(in Entity) link_to, HTTPServerRequest req = null) 350 { 351 import std.algorithm : sort; 352 353 auto info = new DocPageInfo; 354 info.linkTo = link_to; 355 info.settings = settings; 356 info.rootPackage = root_package; 357 info.mod = mod; 358 info.node = mod; 359 info.docGroups = docgroups;//docGroups(mod.lookupAll!Declaration(nested_name)); 360 sort!((a, b) => cmpKind(a.members[0], b.members[0]))(info.docGroups); 361 info.nestedName = nested_name; 362 363 auto rng = StreamOutputRange(dst); 364 final switch (settings.htmlOutputStyle) 365 { 366 foreach (htmlOutputStyle; EnumMembers!HTMLOutputStyle) 367 case htmlOutputStyle: 368 { 369 rng.compileHTMLDietFile!("ddox.docpage.dt", req, info, DdoxDietTraits!(htmlOutputStyle)); 370 return; 371 } 372 } 373 } 374 375 private bool cmpKind(in Entity a, in Entity b) 376 { 377 static immutable kinds = [ 378 DeclarationKind.Variable, 379 DeclarationKind.Function, 380 DeclarationKind.Struct, 381 DeclarationKind.Union, 382 DeclarationKind.Class, 383 DeclarationKind.Interface, 384 DeclarationKind.Enum, 385 DeclarationKind.EnumMember, 386 DeclarationKind.Template, 387 DeclarationKind.TemplateParameter, 388 DeclarationKind.Alias 389 ]; 390 391 auto ad = cast(const(Declaration))a; 392 auto bd = cast(const(Declaration))b; 393 394 if (!ad && !bd) return false; 395 if (!ad) return false; 396 if (!bd) return true; 397 398 auto ak = kinds.countUntil(ad.kind); 399 auto bk = kinds.countUntil(bd.kind); 400 401 if (ak < 0 && bk < 0) return false; 402 if (ak < 0) return false; 403 if (bk < 0) return true; 404 405 return ak < bk; 406 }