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.array;
15 import std.digest.md;
16 import std.format : formattedWrite;
17 import std.string : startsWith, toLower;
18 import std.variant;
19 import vibe.core.log;
20 import vibe.core.file;
21 import vibe.core.stream;
22 import vibe.data.json;
23 import vibe.inet.path;
24 import vibe.http.server;
25 import vibe.templ.diet;
26 
27 
28 /*
29 	structure:
30 	/index.html
31 	/pack1/pack2/module1.html
32 	/pack1/pack2/module1/member.html
33 	/pack1/pack2/module1/member.submember.html
34 */
35 
36 version (Windows) version = CaseInsensitiveFS;
37 else version (OSX) version = CaseInsensitiveFS;
38 
39 void generateHtmlDocs(Path dst_path, Package root, GeneratorSettings settings = null)
40 {
41 	import std.algorithm : splitter;
42 	import vibe.web.common : adjustMethodStyle;
43 
44 	if( !settings ) settings = new GeneratorSettings;
45 
46 	version (CaseInsensitiveFS) {
47 		final switch (settings.fileNameStyle) with (MethodStyle) {
48 			case unaltered, camelCase, pascalCase:
49 				logWarn("On Windows and OS X, file names that differ only in their case "
50 					~ "are treated as equal by default. Use one of the "
51 					~ "lower/upper case styles with the --file-name-style "
52 					~ "option to avoid missing files in the generated output.");
53 				break;
54 			case lowerCase, upperCase, lowerUnderscored, upperUnderscored:
55 				break;
56 		}
57 	}
58 
59 	string[string] file_hashes;
60 	string[string] new_file_hashes;
61 
62 	const hash_file_name = dst_path ~ "file_hashes.json";
63 	if (existsFile(hash_file_name)) {
64 		auto hfi = getFileInfo(hash_file_name);
65 		auto hf = readFileUTF8(hash_file_name);
66 		file_hashes = deserializeJson!(string[string])(hf);
67 	}
68 
69 	string linkTo(Entity ent, size_t level)
70 	{
71 		auto dst = appender!string();
72 
73 		if( level ) foreach( i; 0 .. level ) dst.put("../");
74 		else dst.put("./");
75 
76 		if( ent !is null ){
77 			if( !ent.parent ){
78 				dst.put("index.html");
79 				return dst.data();
80 			}
81 
82 			auto dp = cast(VariableDeclaration)ent;
83 			auto dfn = ent.parent ? cast(FunctionDeclaration)ent.parent : null;
84 			if( dp && dfn ) ent = ent.parent;
85 
86 			Entity[] nodes;
87 			size_t mod_idx = 0;
88 			while( ent ){
89 				if( cast(Module)ent ) mod_idx = nodes.length;
90 				nodes ~= ent;
91 				ent = ent.parent;
92 			}
93 			foreach_reverse(i, n; nodes[mod_idx .. $-1]){
94 				dst.put(n.name);
95 				if( i > 0 ) dst.put('/');
96 			}
97 			if( mod_idx == 0 ) dst.put(".html");
98 			else {
99 				dst.put('/');
100 				foreach_reverse(n; nodes[0 .. mod_idx]){
101 					dst.put(adjustMethodStyle(n.name, settings.fileNameStyle));
102 					dst.put('.');
103 				}
104 				dst.put("html");
105 			}
106 
107 			// FIXME: must also work for multiple function overloads in separate doc groups!
108 			if( dp && dfn ){
109 				dst.put('#');
110 				dst.put(dp.name);
111 			}
112 		}
113 
114 		return dst.data();
115 	}
116 
117 	void collectChildren(Entity parent, ref DocGroup[][string] pages)
118 	{
119 		Declaration[] members;
120 		if (auto mod = cast(Module)parent) members = mod.members;
121 		else if (auto ctd = cast(CompositeTypeDeclaration)parent) members = ctd.members;
122 		else if (auto td = cast(TemplateDeclaration)parent) members = td.members;
123 
124 		foreach (decl; members) {
125 			auto style = settings.fileNameStyle; // workaround for invalid value when directly used inside lamba
126 			auto name = decl.nestedName.splitter(".").map!(n => adjustMethodStyle(n, style)).join(".");
127 			auto pl = name in pages;
128 			if (pl && !canFind(*pl, decl.docGroup)) *pl ~= decl.docGroup;
129 			else if (!pl) pages[name] = [decl.docGroup];
130 
131 			collectChildren(decl, pages);
132 		}
133 	}
134 
135 	void writeHashedFile(Path filename, scope void delegate(OutputStream) del)
136 	{
137 		import vibe.stream.memory;
138 		assert(filename.startsWith(dst_path));
139 
140 		auto str = new MemoryOutputStream;
141 		del(str);
142 		auto h = md5Of(str.data).toHexString.idup;
143 		auto relfilename = filename[dst_path.length .. $].toString();
144 		auto ph = relfilename in file_hashes;
145 		if (!ph || *ph != h) {
146 			//logInfo("do write %s", filename);
147 			writeFile(filename, str.data);
148 		}
149 		new_file_hashes[relfilename] = h;
150 	}
151 
152 	void visitModule(Module mod, Path pack_path)
153 	{
154 		auto modpath = pack_path ~ PathEntry(mod.name);
155 		if (!existsFile(modpath)) createDirectory(modpath);
156 		logInfo("Generating module: %s", mod.qualifiedName);
157 		writeHashedFile(pack_path ~ PathEntry(mod.name~".html"), (stream) {
158 			generateModulePage(stream, root, mod, settings, ent => linkTo(ent, pack_path.length-dst_path.length));
159 		});
160 
161 		DocGroup[][string] pages;
162 		collectChildren(mod, pages);
163 		foreach (name, decls; pages)
164 			writeHashedFile(modpath ~ PathEntry(name~".html"), (stream) {
165 				generateDeclPage(stream, root, mod, name, decls, settings, ent => linkTo(ent, modpath.length-dst_path.length));
166 			});
167 	}
168 
169 	void visitPackage(Package p, Path path)
170 	{
171 		auto packpath = p.parent ? path ~ PathEntry(p.name) : path;
172 		if( !packpath.empty && !existsFile(packpath) ) createDirectory(packpath);
173 		foreach( sp; p.packages ) visitPackage(sp, packpath);
174 		foreach( m; p.modules ) visitModule(m, packpath);
175 	}
176 
177 	dst_path.normalize();
178 
179 	if( !dst_path.empty && !existsFile(dst_path) ) createDirectory(dst_path);
180 
181 	writeHashedFile(dst_path ~ PathEntry("index.html"), (stream) {
182 		generateApiIndex(stream, root, settings, ent => linkTo(ent, 0));
183 	});
184 
185 	writeHashedFile(dst_path ~ "symbols.js", (stream) {
186 		generateSymbolsJS(stream, root, settings, ent => linkTo(ent, 0));
187 	});
188 
189 	writeHashedFile(dst_path ~ PathEntry("sitemap.xml"), (stream) {
190 		generateSitemap(stream, root, settings, ent => linkTo(ent, 0));
191 	});
192 
193 	visitPackage(root, dst_path);
194 
195 	// delete obsolete files
196 	foreach (f; file_hashes.byKey)
197 		if (f !in new_file_hashes) {
198 			try removeFile(dst_path ~ Path(f));
199 			catch (Exception e) logWarn("Failed to remove obsolete file '%s': %s", f, e.msg);
200 		}
201 
202 	// write new file hash list
203 	writeFileUTF8(hash_file_name, new_file_hashes.serializeToJsonString());
204 }
205 
206 class DocPageInfo {
207 	string delegate(Entity ent) linkTo;
208 	GeneratorSettings settings;
209 	Package rootPackage;
210 	Entity node;
211 	Module mod;
212 	DocGroup[] docGroups; // for multiple doc groups with the same name
213 	string nestedName;
214 	
215 	@property NavigationType navigationType() const { return settings.navigationType; }
216 	string formatType(Type tp, bool include_code_tags = true) { return .formatType(tp, linkTo, include_code_tags); }
217 	string formatDoc(DocGroup group, int hlevel, bool delegate(string) display_section)
218 	{
219 		// TODO: memoize the DocGroupContext
220 		return group.comment.renderSections(new DocGroupContext(group, linkTo), display_section, hlevel);
221 	}
222 }
223 
224 void generateSitemap(OutputStream dst, Package root_package, GeneratorSettings settings, string delegate(Entity) link_to, HTTPServerRequest req = null)
225 {
226 	dst.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
227 	dst.write("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n");
228 	
229 	void writeEntry(string[] parts...){
230 		dst.write("<url><loc>");
231 		foreach( p; parts )
232 			dst.write(p);
233 		dst.write("</loc></url>\n");
234 	}
235 
236 	void writeEntityRec(Entity ent){
237 		import std.string;
238 		if( !cast(Package)ent || ent is root_package ){
239 			auto link = link_to(ent);
240 			if( indexOf(link, '#') < 0 ) // ignore URLs with anchors
241 				writeEntry((settings.siteUrl ~ Path(link)).toString());
242 		}
243 		ent.iterateChildren((ch){ writeEntityRec(ch); return true; });
244 	}
245 
246 	writeEntityRec(root_package);
247 	
248 	dst.write("</urlset>\n");
249 	dst.flush();
250 }
251 
252 void generateSymbolsJS(OutputStream dst, Package root_package, GeneratorSettings settings, string delegate(Entity) link_to)
253 {
254 	import vibe.stream.wrapper;
255 
256 	bool[string] visited;
257 
258 	auto rng = StreamOutputRange(dst);
259 
260 	void writeEntry(Entity ent) {
261 		if (cast(Package)ent || cast(TemplateParameterDeclaration)ent) return;
262 		if (ent.qualifiedName in visited) return;
263 		visited[ent.qualifiedName] = true;
264 
265 		string kind = ent.classinfo.name.split(".")[$-1].toLower;
266 		string[] attributes;
267 		if (auto fdecl = cast(FunctionDeclaration)ent) attributes = fdecl.attributes;
268 		else if (auto adecl = cast(AliasDeclaration)ent) attributes = adecl.attributes;
269 		else if (auto tdecl = cast(TypedDeclaration)ent) attributes = tdecl.type.attributes;
270 		attributes = attributes.map!(a => a.startsWith("@") ? a[1 .. $] : a).array;
271 		(&rng).formattedWrite(`{name: '%s', kind: "%s", path: '%s', attributes: %s},`, ent.qualifiedName, kind, link_to(ent), attributes);
272 		rng.put('\n');
273 	}
274 
275 	void writeEntryRec(Entity ent) {
276 		writeEntry(ent);
277 		if (cast(FunctionDeclaration)ent) return;
278 		ent.iterateChildren((ch) { writeEntryRec(ch); return true; });
279 	}
280 
281 	rng.put("// symbol index generated by DDOX - do not edit\n");
282 	rng.put("var symbols = [\n");
283 	writeEntryRec(root_package);
284 	rng.put("];\n");
285 }
286 
287 void generateApiIndex(OutputStream dst, Package root_package, GeneratorSettings settings, string delegate(Entity) link_to, HTTPServerRequest req = null)
288 {
289 	auto info = new DocPageInfo;
290 	info.linkTo = link_to;
291 	info.settings = settings;
292 	info.rootPackage = root_package;
293 	info.node = root_package;
294 
295 	dst.compileDietFile!("ddox.overview.dt", req, info);
296 }
297 
298 void generateModulePage(OutputStream dst, Package root_package, Module mod, GeneratorSettings settings, string delegate(Entity) link_to, HTTPServerRequest req = null)
299 {
300 	auto info = new DocPageInfo;
301 	info.linkTo = link_to;
302 	info.settings = settings;
303 	info.rootPackage = root_package;
304 	info.mod = mod;
305 	info.node = mod;
306 	info.docGroups = null;
307 
308 	dst.compileDietFile!("ddox.module.dt", req, info);
309 }
310 
311 void generateDeclPage(OutputStream dst, Package root_package, Module mod, string nested_name, DocGroup[] docgroups, GeneratorSettings settings, string delegate(Entity) link_to, HTTPServerRequest req = null)
312 {
313 	import std.algorithm : sort;
314 
315 	auto info = new DocPageInfo;
316 	info.linkTo = link_to;
317 	info.settings = settings;
318 	info.rootPackage = root_package;
319 	info.mod = mod;
320 	info.node = mod;
321 	info.docGroups = docgroups;//docGroups(mod.lookupAll!Declaration(nested_name));
322 	sort!((a, b) => cmpKind(a.members[0], b.members[0]))(info.docGroups);
323 	info.nestedName = nested_name;
324 
325 	dst.compileDietFile!("ddox.docpage.dt", req, info);
326 }
327 
328 private bool cmpKind(Entity a, Entity b)
329 {
330 	static immutable kinds = [
331 		DeclarationKind.Variable,
332 		DeclarationKind.Function,
333 		DeclarationKind.Struct,
334 		DeclarationKind.Union,
335 		DeclarationKind.Class,
336 		DeclarationKind.Interface,
337 		DeclarationKind.Enum,
338 		DeclarationKind.EnumMember,
339 		DeclarationKind.Template,
340 		DeclarationKind.TemplateParameter,
341 		DeclarationKind.Alias
342 	];
343 
344 	auto ad = cast(Declaration)a;
345 	auto bd = cast(Declaration)b;
346 
347 	if (!ad && !bd) return false;
348 	if (!ad) return false;
349 	if (!bd) return true;
350 
351 	auto ak = kinds.countUntil(ad.kind);
352 	auto bk = kinds.countUntil(bd.kind);
353 
354 	if (ak < 0 && bk < 0) return false;
355 	if (ak < 0) return false;
356 	if (bk < 0) return true;
357 
358 	return ak < bk;
359 }