1 #!/usr/bin/env rdmd 2 // Written in the D programming language 3 4 /++ 5 This is a program for simplifying ddoc generation. 6 7 It ensures that the names of the generated .html files include the full 8 module path (with underscores instead of dots) rather than simply being 9 named after the modules (since just using the module names results in 10 connflicts if any packages have modules with the same name). 11 12 It also provides an easy way to exclude files from ddoc generation. Any 13 modules or packages with the name internal are excluded as well as any 14 files that are passed on the command line. And package.d files have their 15 corresponding .html files renamed to match the package name. 16 17 Also, the program generates a .ddoc file intended for use in a navigation 18 bar on the side of the documentation (similar to what dlang.org has) uses 19 it in the ddoc generation (it's deleted afterwards). The navigation bar 20 contains the full module hierarchy to allow for easy navigation among the 21 modules in the project. Of course, the other .ddoc files have to actually 22 use the MODULE_MENU macro in the generated .ddoc file, or the documentation 23 won't end up with a navigation bar. 24 25 The program assumes a layout similar to dub in that the source files are 26 expected to be in a directory called "source", and the generated 27 documentation goes in the "docs" directory (which is deleted before 28 documentation generation to ensure a clean build). 29 30 It's expected that any .ddoc files being used will be in the "ddoc" 31 directory, which isn't a dub thing, but they have to go somewhere. 32 33 In addition, the program expects there to be a "source_docs" directory. Any 34 .dd files that are there will have corresponding .html files generated for 35 them (e.g. for generating index.html), and any other files or directories 36 (e.g. a "css" or "js" folder) will be copied over to the "docs" folder. 37 38 Note that this program does assume that all module names match their file 39 names and that all package names match their folder names. 40 41 Copyright: Copyright 2017 - 2019 42 License: $(WEB www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 43 Author: Jonathan M Davis 44 +/ 45 module gendocs; 46 47 import std.algorithm : canFind; 48 import std.array : appender, array, replace; 49 import std.file : dirEntries, mkdir, SpanMode; 50 import std.format : format; 51 import std.path : baseName, buildPath, dirName, extension, setExtension, stripExtension; 52 import std.range.primitives; 53 54 enum sourceDir = "source"; 55 enum docsDir = "docs"; 56 enum ddocDir = "ddoc"; 57 enum sourceDocsDir = "source_docs"; 58 59 int main(string[] args) 60 { 61 import std.exception : enforce; 62 import std.file : exists, remove, rmdirRecurse; 63 64 try 65 { 66 auto excludes = args[1 .. $]; 67 68 enforce(ddocDir.exists, "ddoc directory is missing"); 69 enforce(sourceDocsDir.exists, "source_docs directory is missing"); 70 71 foreach(exclude; excludes) 72 { 73 auto file = buildPath(sourceDir, exclude); 74 enforce(file.exists, format("%s does not exist", file)); 75 } 76 77 if(docsDir.exists) 78 rmdirRecurse(docsDir); 79 mkdir(docsDir); 80 81 auto moduleListDdoc = genModuleListDdoc(excludes); 82 scope(exit) remove(moduleListDdoc); 83 84 auto ddocFiles = getDdocFiles(); 85 processSourceDocsDir(sourceDocsDir, docsDir, ddocFiles); 86 processSourceDir(sourceDir, docsDir, excludes, ddocFiles); 87 } 88 catch(Exception e) 89 { 90 import std.stdio : stderr, writeln; 91 stderr.writeln(e.msg); 92 return -1; 93 } 94 95 return 0; 96 } 97 98 void processSourceDocsDir(string sourceDir, string targetDir, string[] ddocFiles) 99 { 100 import std.file : copy; 101 102 foreach(de; dirEntries(sourceDir, SpanMode.shallow)) 103 { 104 auto target = buildPath(targetDir, de.baseName); 105 if(de.isDir) 106 { 107 mkdir(target); 108 processSourceDocsDir(de.name, target, ddocFiles); 109 } 110 else if(de.isFile) 111 { 112 if(de.name.extension == ".dd") 113 genDdoc(de.name, target.setExtension(".html"), ddocFiles); 114 else 115 copy(de.name, target); 116 } 117 } 118 } 119 120 void processSourceDir(string sourceDir, string target, string[] excludes, string[] ddocFiles, int depth = 0) 121 { 122 import std.algorithm : endsWith; 123 124 if(depth == 0 && !target.endsWith("/")) 125 target ~= "/"; 126 127 foreach(de; dirEntries(sourceDir, SpanMode.shallow)) 128 { 129 auto name = de.baseName; 130 if(name.stripExtension == "internal" || excludes.canFind(de.baseName)) 131 continue; 132 auto nextTarget = name == "package.d" ? target : format("%s%s%s", target, depth == 0 ? "" : "_", name); 133 if(de.isDir) 134 processSourceDir(de.name, nextTarget, excludes, ddocFiles, depth + 1); 135 else if(de.isFile) 136 genDdoc(de.name, nextTarget.setExtension(".html"), ddocFiles); 137 } 138 } 139 140 void genDdoc(string sourceFile, string htmlFile, string[] ddocFiles) 141 { 142 import std.process : execute; 143 auto result = execute(["dmd", "-o-", "-Isource/", "-Df" ~ htmlFile, sourceFile] ~ ddocFiles); 144 if(result.status != 0) 145 throw new Exception("dmd failed:\n" ~ result.output); 146 } 147 148 string[] getDdocFiles() 149 { 150 import std.algorithm : map; 151 return dirEntries("ddoc", SpanMode.shallow).map!(a => a.name)().array(); 152 } 153 154 string genModuleListDdoc(string[] excludes) 155 { 156 import std.array : join; 157 import std.file : write; 158 159 auto lines = appender!(string[])(); 160 put(lines, "MODULE_MENU="); 161 162 auto modules = getModules(sourceDir, excludes); 163 genModuleMenu(lines, modules); 164 put(lines, "_="); 165 166 put(lines, ""); 167 put(lines, `MENU_MODULE=$(A $2$(UNDERSCORE_PREFIXED_SKIP $+).html, $(SPAN, $1))`); 168 put(lines, "MENU_PKG=$(LIC expand-container open, $(AC expand-toggle, #, $(SPAN, $1))$(ITEMIZE $+))"); 169 put(lines, "_="); 170 171 put(lines, ""); 172 put(lines, "MODULE_INDEX="); 173 genModuleIndex(lines, modules); 174 put(lines, "_="); 175 176 put(lines, ""); 177 put(lines, `INDEX_MODULE=$(A $1$(UNDERSCORE_PREFIXED $+).html, ` ~ 178 `$(SPANC module_index, $1$(DOT_PREFIXED $+)))$(DDOC_BLANKLINE)`); 179 put(lines, "_="); 180 181 auto moduleListDDoc = buildPath(ddocDir, "module_list.ddoc"); 182 write(moduleListDDoc, lines.data.join("\n")); 183 return moduleListDDoc; 184 } 185 186 void genModuleMenu(OR)(ref OR lines, Package* pkg, int depth = 0) 187 { 188 import std.array : replicate; 189 190 auto outerIndent = " ".replicate(depth == 0 ? 0 : depth - 1); 191 auto innerIndent = " ".replicate(depth == 0 ? 0 : depth); 192 193 if(depth != 0) 194 put(lines, format("%s$(MENU_PKG %s,", outerIndent, pkg.path.baseName)); 195 196 foreach(modPath; pkg.modules) 197 { 198 auto modName = modPath.baseName.stripExtension(); 199 if(modName == "package") 200 modPath = modPath.dirName; 201 auto modPieces = modPath.replace("/", ", ").stripExtension(); 202 put(lines, format("%s$(MENU_MODULE %s, %s)", innerIndent, modName, modPieces)); 203 } 204 205 foreach(subPkg; pkg.packages) 206 genModuleMenu(lines, subPkg, depth + 1); 207 208 if(depth != 0) 209 put(lines, format("%s)", outerIndent)); 210 } 211 212 void genModuleIndex(OR)(ref OR lines, Package* pkg, int depth = 0) 213 { 214 import std.algorithm : filter; 215 import std.array : replicate; 216 217 static string genListModule(string modPath) 218 { 219 auto modName = modPath.baseName.stripExtension(); 220 auto modPieces = modPath.replace("/", ", ").stripExtension(); 221 return format("$(INDEX_MODULE %s)", modPieces); 222 } 223 224 if(depth != 0 && pkg.hasPackageD) 225 put(lines, genListModule(pkg.path)); 226 227 foreach(mod; pkg.modules.filter!(a => a.baseName != "package.d")()) 228 put(lines, genListModule(mod)); 229 230 foreach(subPkg; pkg.packages) 231 genModuleIndex(lines, subPkg, depth + 1); 232 } 233 234 struct Package 235 { 236 string path; 237 bool hasPackageD; 238 string[] modules; 239 Package*[] packages; 240 } 241 242 Package* getModules(string dir, string[] excludes, int depth = 0) 243 { 244 import std.algorithm : sort; 245 246 string path; 247 bool hasPackageD; 248 auto modules = appender!(string[])(); 249 auto packages = appender!(Package*[])(); 250 251 if(depth != 0) 252 path = stripSourceDir(dir); 253 254 foreach(de; dirEntries(dir, SpanMode.shallow)) 255 { 256 auto stripped = stripSourceDir(de.name); 257 if(excludes.canFind(stripped)) 258 continue; 259 if(de.isDir) 260 { 261 if(stripped.baseName == "internal") 262 continue; 263 if(auto subPackage = getModules(de.name, excludes, depth + 1)) 264 put(packages, subPackage); 265 } 266 else if(de.isFile) 267 { 268 if(stripped.baseName == "internal.d") 269 continue; 270 if(stripped.baseName == "package.d") 271 hasPackageD = true; 272 modules.put(stripped); 273 } 274 } 275 276 if(modules.data.empty && packages.data.empty) 277 return null; 278 279 auto pkg = new Package(path, hasPackageD, modules.data, packages.data); 280 sort(pkg.modules); 281 sort!((a, b) => a.path < b.path)(pkg.packages); 282 283 return pkg; 284 } 285 286 string stripSourceDir(string path) 287 { 288 import std.algorithm : startsWith; 289 assert(path.startsWith(sourceDir)); 290 version(Posix) 291 return path[sourceDir.length + 1 .. $]; 292 else version(Windows) 293 return path[sourceDir.length + 1 .. $].replace("\\", "/"); 294 else 295 static assert(0, "Unsupported platform"); 296 }