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