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 }