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 }