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