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