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 // mysql-native hack 242 @property auto defaultMacros() { import ddox.ddoc; return s_defaultMacros; } 243 @property auto overrideMacros() { import ddox.ddoc; return s_overrideMacros; } 244 245 @property NavigationType navigationType() const { return settings.navigationType; } 246 string formatType(CachedType tp, bool include_code_tags = true) { return .formatType(tp, linkTo, include_code_tags); } 247 void renderTemplateArgs(R)(R output, Declaration decl) { .renderTemplateArgs(output, decl, linkTo); } 248 string formatDoc(DocGroup group, int hlevel, bool delegate(string) display_section) 249 { 250 if (!group) return null; 251 // TODO: memoize the DocGroupContext 252 return group.comment.renderSections(new DocGroupContext(group, linkTo, settings), display_section, hlevel); 253 } 254 } 255 256 @dietTraits 257 struct DdoxDietTraits(HTMLOutputStyle htmlStyle) { 258 // fields and functions must be static atm., see https://github.com/rejectedsoftware/diet-ng/issues/33 259 enum HTMLOutputStyle htmlOutputStyle = htmlStyle; 260 } 261 262 void generateSitemap(OutputStream)(OutputStream dst, Package root_package, GeneratorSettings settings, string delegate(in Entity) link_to, HTTPServerRequest req = null) 263 if (isOutputStream!OutputStream) 264 { 265 dst.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); 266 dst.write("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n"); 267 268 void writeEntry(string[] parts...){ 269 dst.write("<url><loc>"); 270 foreach( p; parts ) 271 dst.write(p); 272 dst.write("</loc></url>\n"); 273 } 274 275 void writeEntityRec(Entity ent){ 276 import std.string; 277 if (!cast(Package)ent || ent is root_package) { 278 auto link = link_to(ent); 279 if (indexOf(link, '#') < 0) { // ignore URLs with anchors 280 auto p = InetPath(link); 281 p.normalize(); 282 writeEntry((settings.siteUrl ~ p).toString()); 283 } 284 } 285 ent.iterateChildren((ch){ writeEntityRec(ch); return true; }); 286 } 287 288 writeEntityRec(root_package); 289 290 dst.write("</urlset>\n"); 291 dst.flush(); 292 } 293 294 void generateSymbolsJS(OutputStream)(OutputStream dst, Package root_package, GeneratorSettings settings, string delegate(in Entity) link_to) 295 if (isOutputStream!OutputStream) 296 { 297 import std.typecons : Tuple, tuple; 298 299 bool[Tuple!(Entity, CachedString)] visited; 300 301 auto rng = streamOutputRange(dst); 302 303 void writeEntry(Entity ent) { 304 auto key = tuple(ent.parent, ent.name); 305 if (cast(Package)ent || cast(TemplateParameterDeclaration)ent) return; 306 if (key in visited) return; 307 visited[key] = true; 308 309 string kind = ent.classinfo.name.split(".")[$-1].toLower; 310 const(CachedString)[] cattributes; 311 if (auto fdecl = cast(FunctionDeclaration)ent) cattributes = fdecl.attributes; 312 else if (auto adecl = cast(AliasDeclaration)ent) cattributes = adecl.attributes; 313 else if (auto tdecl = cast(TypedDeclaration)ent) cattributes = tdecl.type.attributes; 314 auto attributes = cattributes.map!(a => a.str.startsWith("@") ? a[1 .. $] : a); 315 (&rng).formattedWrite(`{name: '%s', kind: "%s", path: '%s', attributes: %s},`, ent.qualifiedName, kind, link_to(ent), attributes); 316 rng.put('\n'); 317 } 318 319 void writeEntryRec(Entity ent) { 320 writeEntry(ent); 321 if (cast(FunctionDeclaration)ent) return; 322 ent.iterateChildren((ch) { writeEntryRec(ch); return true; }); 323 } 324 325 rng.put("// symbol index generated by DDOX - do not edit\n"); 326 rng.put("var symbols = [\n"); 327 writeEntryRec(root_package); 328 rng.put("];\n"); 329 } 330 331 void generateApiIndex(OutputStream)(OutputStream dst, Package root_package, GeneratorSettings settings, string delegate(in Entity) link_to, HTTPServerRequest req = null) 332 if (isOutputStream!OutputStream) 333 { 334 auto info = new DocPageInfo; 335 info.linkTo = link_to; 336 info.settings = settings; 337 info.rootPackage = root_package; 338 info.node = root_package; 339 340 auto rng = streamOutputRange(dst); 341 final switch (settings.htmlOutputStyle) 342 { 343 foreach (htmlOutputStyle; EnumMembers!HTMLOutputStyle) 344 case htmlOutputStyle: 345 { 346 rng.compileHTMLDietFile!("ddox.overview.dt", req, info, DdoxDietTraits!(htmlOutputStyle)); 347 return; 348 } 349 } 350 } 351 352 void generateModulePage(OutputStream)(OutputStream dst, Package root_package, Module mod, GeneratorSettings settings, string delegate(in Entity) link_to, HTTPServerRequest req = null) 353 if (isOutputStream!OutputStream) 354 { 355 auto info = new DocPageInfo; 356 info.linkTo = link_to; 357 info.settings = settings; 358 info.rootPackage = root_package; 359 info.mod = mod; 360 info.node = mod; 361 info.docGroups = null; 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.module.dt", req, info, DdoxDietTraits!(htmlOutputStyle)); 370 return; 371 } 372 } 373 } 374 375 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) 376 if (isOutputStream!OutputStream) 377 { 378 import std.algorithm : sort; 379 380 auto info = new DocPageInfo; 381 info.linkTo = link_to; 382 info.settings = settings; 383 info.rootPackage = root_package; 384 info.mod = mod; 385 info.node = mod; 386 info.docGroups = docgroups;//docGroups(mod.lookupAll!Declaration(nested_name)); 387 sort!((a, b) => cmpKind(a.members[0], b.members[0]))(info.docGroups); 388 info.nestedName = nested_name; 389 390 auto rng = streamOutputRange(dst); 391 final switch (settings.htmlOutputStyle) 392 { 393 foreach (htmlOutputStyle; EnumMembers!HTMLOutputStyle) 394 case htmlOutputStyle: 395 { 396 rng.compileHTMLDietFile!("ddox.docpage.dt", req, info, DdoxDietTraits!(htmlOutputStyle)); 397 return; 398 } 399 } 400 } 401 402 private bool cmpKind(in Entity a, in Entity b) 403 { 404 static immutable kinds = [ 405 DeclarationKind.Variable, 406 DeclarationKind.Function, 407 DeclarationKind.Struct, 408 DeclarationKind.Union, 409 DeclarationKind.Class, 410 DeclarationKind.Interface, 411 DeclarationKind.Enum, 412 DeclarationKind.EnumMember, 413 DeclarationKind.Template, 414 DeclarationKind.TemplateParameter, 415 DeclarationKind.Alias 416 ]; 417 418 auto ad = cast(const(Declaration))a; 419 auto bd = cast(const(Declaration))b; 420 421 if (!ad && !bd) return false; 422 if (!ad) return false; 423 if (!bd) return true; 424 425 auto ak = kinds.countUntil(ad.kind); 426 auto bk = kinds.countUntil(bd.kind); 427 428 if (ak < 0 && bk < 0) return false; 429 if (ak < 0) return false; 430 if (bk < 0) return true; 431 432 return ak < bk; 433 }