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(NativePath 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 Entity nested; 90 if ( 91 // link parameters to their function 92 (cast(FunctionDeclaration)ent.parent !is null && 93 (nested = cast(VariableDeclaration)ent) !is null) || 94 // link enum members to their enum 95 (!settings.enumMemberPages && 96 cast(EnumDeclaration)ent.parent !is null && 97 (nested = cast(EnumMemberDeclaration)ent) !is null)) 98 ent = ent.parent; 99 100 const(Entity)[] nodes; 101 size_t mod_idx = 0; 102 while( ent ){ 103 if( cast(const(Module))ent ) mod_idx = nodes.length; 104 nodes ~= ent.get; 105 ent = ent.parent; 106 } 107 foreach_reverse(i, n; nodes[mod_idx .. $-1]){ 108 dst.put(n.name[]); 109 if( i > 0 ) dst.put('/'); 110 } 111 if( mod_idx == 0 ) dst.put(".html"); 112 else { 113 dst.put('/'); 114 foreach_reverse(n; nodes[0 .. mod_idx]){ 115 dst.put(adjustMethodStyle(n.name, settings.fileNameStyle)); 116 dst.put('.'); 117 } 118 dst.put("html"); 119 } 120 121 // FIXME: conflicting ids with parameters occurring in multiple overloads 122 // link nested elements to anchor in parent, e.g. params, enum members 123 if( nested ){ 124 dst.put('#'); 125 dst.put(nested.name[]); 126 } 127 } 128 129 return dst.data(); 130 } 131 132 void collectChildren(Entity parent, ref DocGroup[][string] pages) 133 { 134 Declaration[] members; 135 if (!settings.enumMemberPages && cast(EnumDeclaration)parent) 136 return; 137 138 if (auto mod = cast(Module)parent) members = mod.members; 139 else if (auto ctd = cast(CompositeTypeDeclaration)parent) members = ctd.members; 140 else if (auto td = cast(TemplateDeclaration)parent) members = td.members; 141 142 foreach (decl; members) { 143 if (decl.parent !is parent) continue; // exclude inherited members (issue #120) 144 auto style = settings.fileNameStyle; // workaround for invalid value when directly used inside lamba 145 auto name = decl.nestedName.splitter(".").map!(n => adjustMethodStyle(n, style)).join("."); 146 auto pl = name in pages; 147 if (pl && !canFind(*pl, decl.docGroup)) *pl ~= decl.docGroup; 148 else if (!pl) pages[name] = [decl.docGroup]; 149 150 collectChildren(decl, pages); 151 } 152 } 153 154 void writeHashedFile(NativePath filename, scope void delegate(OutputStream) del) 155 { 156 import std.range : drop, walkLength; 157 import vibe.stream.memory; 158 159 assert(filename.startsWith(dst_path)); 160 161 auto str = createMemoryOutputStream(); 162 del(str); 163 auto h = md5Of(str.data).toHexString.idup; 164 version (Have_vibe_core) 165 auto relfilename = NativePath(filename.bySegment.drop(dst_path.bySegment.walkLength)).toString(); 166 else 167 auto relfilename = NativePath(filename.bySegment.drop(dst_path.bySegment.walkLength), false).toString(); 168 auto ph = relfilename in file_hashes; 169 if (!ph || *ph != h) { 170 //logInfo("do write %s", filename); 171 writeFile(filename, str.data); 172 } 173 new_file_hashes[relfilename] = h; 174 } 175 176 void visitModule(Module mod, NativePath pack_path) 177 { 178 import std.range : walkLength; 179 180 auto modpath = pack_path ~ NativePath.Segment(mod.name); 181 if (!existsFile(modpath)) createDirectory(modpath); 182 logInfo("Generating module: %s", mod.qualifiedName); 183 writeHashedFile(pack_path ~ NativePath.Segment(mod.name~".html"), (stream) { 184 generateModulePage(stream, root, mod, settings, ent => linkTo(ent, pack_path.bySegment.walkLength-dst_path.bySegment.walkLength)); 185 }); 186 187 DocGroup[][string] pages; 188 collectChildren(mod, pages); 189 foreach (name, decls; pages) 190 writeHashedFile(modpath ~ NativePath.Segment(name~".html"), (stream) { 191 generateDeclPage(stream, root, mod, name, decls, settings, ent => linkTo(ent, modpath.bySegment.walkLength-dst_path.bySegment.walkLength)); 192 }); 193 } 194 195 void visitPackage(Package p, NativePath path) 196 { 197 auto packpath = p.parent ? path ~ NativePath.Segment(p.name) : path; 198 if( !packpath.empty && !existsFile(packpath) ) createDirectory(packpath); 199 foreach( sp; p.packages ) visitPackage(sp, packpath); 200 foreach( m; p.modules ) visitModule(m, packpath); 201 } 202 203 dst_path.normalize(); 204 205 if( !dst_path.empty && !existsFile(dst_path) ) createDirectory(dst_path); 206 207 writeHashedFile(dst_path ~ NativePath.Segment("index.html"), (stream) { 208 generateApiIndex(stream, root, settings, ent => linkTo(ent, 0)); 209 }); 210 211 writeHashedFile(dst_path ~ "symbols.js", (stream) { 212 generateSymbolsJS(stream, root, settings, ent => linkTo(ent, 0)); 213 }); 214 215 writeHashedFile(dst_path ~ NativePath.Segment("sitemap.xml"), (stream) { 216 generateSitemap(stream, root, settings, ent => linkTo(ent, 0)); 217 }); 218 219 visitPackage(root, dst_path); 220 221 // delete obsolete files 222 foreach (f; file_hashes.byKey) 223 if (f !in new_file_hashes) { 224 try removeFile(dst_path ~ NativePath(f)); 225 catch (Exception e) logWarn("Failed to remove obsolete file '%s': %s", f, e.msg); 226 } 227 228 // write new file hash list 229 writeFileUTF8(hash_file_name, new_file_hashes.serializeToJsonString()); 230 } 231 232 class DocPageInfo { 233 string delegate(in Entity ent) linkTo; 234 GeneratorSettings settings; 235 Package rootPackage; 236 Entity node; 237 Module mod; 238 DocGroup[] docGroups; // for multiple doc groups with the same name 239 string nestedName; 240 241 @property NavigationType navigationType() const { return settings.navigationType; } 242 string formatType(CachedType tp, bool include_code_tags = true) { return .formatType(tp, linkTo, include_code_tags); } 243 void renderTemplateArgs(R)(R output, Declaration decl) { .renderTemplateArgs(output, decl, linkTo); } 244 string formatDoc(DocGroup group, int hlevel, bool delegate(string) display_section) 245 { 246 if (!group) return null; 247 // TODO: memoize the DocGroupContext 248 return group.comment.renderSections(new DocGroupContext(group, linkTo, settings), display_section, hlevel); 249 } 250 } 251 252 @dietTraits 253 struct DdoxDietTraits(HTMLOutputStyle htmlStyle) { 254 // fields and functions must be static atm., see https://github.com/rejectedsoftware/diet-ng/issues/33 255 enum HTMLOutputStyle htmlOutputStyle = htmlStyle; 256 } 257 258 void generateSitemap(OutputStream)(OutputStream dst, Package root_package, GeneratorSettings settings, string delegate(in Entity) link_to, HTTPServerRequest req = null) 259 if (isOutputStream!OutputStream) 260 { 261 dst.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); 262 dst.write("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n"); 263 264 void writeEntry(string[] parts...){ 265 dst.write("<url><loc>"); 266 foreach( p; parts ) 267 dst.write(p); 268 dst.write("</loc></url>\n"); 269 } 270 271 void writeEntityRec(Entity ent){ 272 import std..string; 273 if (!cast(Package)ent || ent is root_package) { 274 auto link = link_to(ent); 275 if (indexOf(link, '#') < 0) { // ignore URLs with anchors 276 auto p = InetPath(link); 277 p.normalize(); 278 writeEntry((settings.siteUrl ~ p).toString()); 279 } 280 } 281 ent.iterateChildren((ch){ writeEntityRec(ch); return true; }); 282 } 283 284 writeEntityRec(root_package); 285 286 dst.write("</urlset>\n"); 287 dst.flush(); 288 } 289 290 void generateSymbolsJS(OutputStream)(OutputStream dst, Package root_package, GeneratorSettings settings, string delegate(in Entity) link_to) 291 if (isOutputStream!OutputStream) 292 { 293 import std.typecons : Tuple, tuple; 294 295 bool[Tuple!(Entity, CachedString)] visited; 296 297 auto rng = streamOutputRange(dst); 298 299 void writeEntry(Entity ent) { 300 auto key = tuple(ent.parent, ent.name); 301 if (cast(Package)ent || cast(TemplateParameterDeclaration)ent) return; 302 if (key in visited) return; 303 visited[key] = true; 304 305 string kind = ent.classinfo.name.split(".")[$-1].toLower; 306 const(CachedString)[] cattributes; 307 if (auto fdecl = cast(FunctionDeclaration)ent) cattributes = fdecl.attributes; 308 else if (auto adecl = cast(AliasDeclaration)ent) cattributes = adecl.attributes; 309 else if (auto tdecl = cast(TypedDeclaration)ent) cattributes = tdecl.type.attributes; 310 auto attributes = cattributes.map!(a => a.str.startsWith("@") ? a[1 .. $] : a); 311 (&rng).formattedWrite(`{name: '%s', kind: "%s", path: '%s', attributes: %s},`, ent.qualifiedName, kind, link_to(ent), attributes); 312 rng.put('\n'); 313 } 314 315 void writeEntryRec(Entity ent) { 316 writeEntry(ent); 317 if (cast(FunctionDeclaration)ent) return; 318 ent.iterateChildren((ch) { writeEntryRec(ch); return true; }); 319 } 320 321 rng.put("// symbol index generated by DDOX - do not edit\n"); 322 rng.put("var symbols = [\n"); 323 writeEntryRec(root_package); 324 rng.put("];\n"); 325 } 326 327 void generateApiIndex(OutputStream)(OutputStream dst, Package root_package, GeneratorSettings settings, string delegate(in Entity) link_to, HTTPServerRequest req = null) 328 if (isOutputStream!OutputStream) 329 { 330 auto info = new DocPageInfo; 331 info.linkTo = link_to; 332 info.settings = settings; 333 info.rootPackage = root_package; 334 info.node = root_package; 335 336 auto rng = streamOutputRange(dst); 337 final switch (settings.htmlOutputStyle) 338 { 339 foreach (htmlOutputStyle; EnumMembers!HTMLOutputStyle) 340 case htmlOutputStyle: 341 { 342 rng.compileHTMLDietFile!("ddox.overview.dt", req, info, DdoxDietTraits!(htmlOutputStyle)); 343 return; 344 } 345 } 346 } 347 348 void generateModulePage(OutputStream)(OutputStream dst, Package root_package, Module mod, GeneratorSettings settings, string delegate(in Entity) link_to, HTTPServerRequest req = null) 349 if (isOutputStream!OutputStream) 350 { 351 auto info = new DocPageInfo; 352 info.linkTo = link_to; 353 info.settings = settings; 354 info.rootPackage = root_package; 355 info.mod = mod; 356 info.node = mod; 357 info.docGroups = null; 358 359 auto rng = streamOutputRange(dst); 360 final switch (settings.htmlOutputStyle) 361 { 362 foreach (htmlOutputStyle; EnumMembers!HTMLOutputStyle) 363 case htmlOutputStyle: 364 { 365 rng.compileHTMLDietFile!("ddox.module.dt", req, info, DdoxDietTraits!(htmlOutputStyle)); 366 return; 367 } 368 } 369 } 370 371 void generateDeclPage(OutputStream)(OutputStream dst, Package root_package, Module mod, string nested_name, DocGroup[] docgroups, GeneratorSettings settings, string delegate(in Entity) link_to, HTTPServerRequest req = null) 372 if (isOutputStream!OutputStream) 373 { 374 import std.algorithm : sort; 375 376 auto info = new DocPageInfo; 377 info.linkTo = link_to; 378 info.settings = settings; 379 info.rootPackage = root_package; 380 info.mod = mod; 381 info.node = mod; 382 info.docGroups = docgroups;//docGroups(mod.lookupAll!Declaration(nested_name)); 383 sort!((a, b) => cmpKind(a.members[0], b.members[0]))(info.docGroups); 384 info.nestedName = nested_name; 385 386 auto rng = streamOutputRange(dst); 387 final switch (settings.htmlOutputStyle) 388 { 389 foreach (htmlOutputStyle; EnumMembers!HTMLOutputStyle) 390 case htmlOutputStyle: 391 { 392 rng.compileHTMLDietFile!("ddox.docpage.dt", req, info, DdoxDietTraits!(htmlOutputStyle)); 393 return; 394 } 395 } 396 } 397 398 private bool cmpKind(in Entity a, in Entity b) 399 { 400 static immutable kinds = [ 401 DeclarationKind.Variable, 402 DeclarationKind.Function, 403 DeclarationKind.Struct, 404 DeclarationKind.Union, 405 DeclarationKind.Class, 406 DeclarationKind.Interface, 407 DeclarationKind.Enum, 408 DeclarationKind.EnumMember, 409 DeclarationKind.Template, 410 DeclarationKind.TemplateParameter, 411 DeclarationKind.Alias 412 ]; 413 414 auto ad = cast(const(Declaration))a; 415 auto bd = cast(const(Declaration))b; 416 417 if (!ad && !bd) return false; 418 if (!ad) return false; 419 if (!bd) return true; 420 421 auto ak = kinds.countUntil(ad.kind); 422 auto bk = kinds.countUntil(bd.kind); 423 424 if (ak < 0 && bk < 0) return false; 425 if (ak < 0) return false; 426 if (bk < 0) return true; 427 428 return ak < bk; 429 }