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.format : formattedWrite;
16 import std.string : startsWith, toLower;
17 import std.variant;
18 import vibe.core.log;
19 import vibe.core.file;
20 import vibe.core.stream;
21 import vibe.inet.path;
22 import vibe.http.server;
23 import vibe.templ.diet;
24 
25 
26 /*
27 	structure:
28 	/index.html
29 	/pack1/pack2/module1.html
30 	/pack1/pack2/module1/member.html
31 	/pack1/pack2/module1/member.submember.html
32 */
33 
34 void generateHtmlDocs(Path dst_path, Package root, GeneratorSettings settings = null)
35 {
36 	import std.algorithm : splitter;
37 	import vibe.web.common : adjustMethodStyle;
38 
39 	if( !settings ) settings = new GeneratorSettings;
40 
41 	string linkTo(Entity ent, size_t level)
42 	{
43 		auto dst = appender!string();
44 
45 		if( level ) foreach( i; 0 .. level ) dst.put("../");
46 		else dst.put("./");
47 
48 		if( ent !is null ){
49 			if( !ent.parent ){
50 				dst.put("index.html");
51 				return dst.data();
52 			}
53 
54 			auto dp = cast(VariableDeclaration)ent;
55 			auto dfn = ent.parent ? cast(FunctionDeclaration)ent.parent : null;
56 			if( dp && dfn ) ent = ent.parent;
57 
58 			Entity[] nodes;
59 			size_t mod_idx = 0;
60 			while( ent ){
61 				if( cast(Module)ent ) mod_idx = nodes.length;
62 				nodes ~= ent;
63 				ent = ent.parent;
64 			}
65 			foreach_reverse(i, n; nodes[mod_idx .. $-1]){
66 				dst.put(n.name);
67 				if( i > 0 ) dst.put('/');
68 			}
69 			if( mod_idx == 0 ) dst.put(".html");
70 			else {
71 				dst.put('/');
72 				foreach_reverse(n; nodes[0 .. mod_idx]){
73 					dst.put(adjustMethodStyle(n.name, settings.fileNameStyle));
74 					dst.put('.');
75 				}
76 				dst.put("html");
77 			}
78 
79 			// FIXME: must also work for multiple function overloads in separate doc groups!
80 			if( dp && dfn ){
81 				dst.put('#');
82 				dst.put(dp.name);
83 			}
84 		}
85 
86 		return dst.data();
87 	}
88 
89 	void collectChildren(Entity parent, ref DocGroup[][string] pages)
90 	{
91 		Declaration[] members;
92 		if (auto mod = cast(Module)parent) members = mod.members;
93 		else if (auto ctd = cast(CompositeTypeDeclaration)parent) members = ctd.members;
94 		else if (auto td = cast(TemplateDeclaration)parent) members = td.members;
95 
96 		foreach (decl; members) {
97 			auto style = settings.fileNameStyle; // workaround for invalid value when directly used inside lamba
98 			auto name = decl.nestedName.splitter(".").map!(n => adjustMethodStyle(n, style)).join(".");
99 			auto pl = name in pages;
100 			if (pl && !canFind(*pl, decl.docGroup)) *pl ~= decl.docGroup;
101 			else if (!pl) pages[name] = [decl.docGroup];
102 
103 			collectChildren(decl, pages);
104 		}
105 	}
106 
107 	void visitModule(Module mod, Path pack_path)
108 	{
109 		auto modpath = pack_path ~ PathEntry(mod.name);
110 		if (!existsFile(modpath)) createDirectory(modpath);
111 		logInfo("Generating module: %s", mod.qualifiedName);
112 		auto file = openFile(pack_path ~ PathEntry(mod.name~".html"), FileMode.createTrunc);
113 		scope(exit) file.close();
114 		generateModulePage(file, root, mod, settings, ent => linkTo(ent, pack_path.length-dst_path.length));
115 
116 		DocGroup[][string] pages;
117 		collectChildren(mod, pages);
118 		foreach (name, decls; pages) {
119 			auto file = openFile(modpath ~ PathEntry(name~".html"), FileMode.createTrunc);
120 			scope(exit) file.close();
121 			generateDeclPage(file, root, mod, name, decls, settings, ent => linkTo(ent, modpath.length-dst_path.length));
122 		}
123 	}
124 
125 	void visitPackage(Package p, Path path)
126 	{
127 		auto packpath = p.parent ? path ~ PathEntry(p.name) : path;
128 		if( !packpath.empty && !existsFile(packpath) ) createDirectory(packpath);
129 		foreach( sp; p.packages ) visitPackage(sp, packpath);
130 		foreach( m; p.modules ) visitModule(m, packpath);
131 	}
132 
133 	dst_path.normalize();
134 
135 	if( !dst_path.empty && !existsFile(dst_path) ) createDirectory(dst_path);
136 
137 	{
138 		auto idxfile = openFile(dst_path ~ PathEntry("index.html"), FileMode.createTrunc);
139 		scope(exit) idxfile.close();
140 		generateApiIndex(idxfile, root, settings, ent => linkTo(ent, 0));
141 	}
142 
143 	{
144 		auto symfile = openFile(dst_path ~ "symbols.js", FileMode.createTrunc);
145 		scope(exit) symfile.close();
146 		generateSymbolsJS(symfile, root, settings, ent => linkTo(ent, 0));
147 	}
148 
149 	{
150 		auto smfile = openFile(dst_path ~ PathEntry("sitemap.xml"), FileMode.createTrunc);
151 		scope(exit) smfile.close();
152 		generateSitemap(smfile, root, settings, ent => linkTo(ent, 0));
153 	}
154 
155 	visitPackage(root, dst_path);
156 }
157 
158 class DocPageInfo {
159 	string delegate(Entity ent) linkTo;
160 	GeneratorSettings settings;
161 	Package rootPackage;
162 	Entity node;
163 	Module mod;
164 	DocGroup[] docGroups; // for multiple doc groups with the same name
165 	string nestedName;
166 	
167 	@property NavigationType navigationType() const { return settings.navigationType; }
168 	string formatType(Type tp, bool include_code_tags = true) { return .formatType(tp, linkTo, include_code_tags); }
169 	string formatDoc(DocGroup group, int hlevel, bool delegate(string) display_section)
170 	{
171 		return group.comment.renderSections(new DocGroupContext(group, linkTo), display_section, hlevel);
172 	}
173 }
174 
175 void generateSitemap(OutputStream dst, Package root_package, GeneratorSettings settings, string delegate(Entity) link_to, HTTPServerRequest req = null)
176 {
177 	dst.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
178 	dst.write("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n");
179 	
180 	void writeEntry(string[] parts...){
181 		dst.write("<url><loc>");
182 		foreach( p; parts )
183 			dst.write(p);
184 		dst.write("</loc></url>\n");
185 	}
186 
187 	void writeEntityRec(Entity ent){
188 		import std.string;
189 		if( !cast(Package)ent || ent is root_package ){
190 			auto link = link_to(ent);
191 			if( indexOf(link, '#') < 0 ) // ignore URLs with anchors
192 				writeEntry((settings.siteUrl ~ Path(link)).toString());
193 		}
194 		ent.iterateChildren((ch){ writeEntityRec(ch); return true; });
195 	}
196 
197 	writeEntityRec(root_package);
198 	
199 	dst.write("</urlset>\n");
200 	dst.flush();
201 }
202 
203 void generateSymbolsJS(OutputStream dst, Package root_package, GeneratorSettings settings, string delegate(Entity) link_to)
204 {
205 	bool[string] visited;
206 
207 	void writeEntry(Entity ent) {
208 		if (cast(Package)ent || cast(TemplateParameterDeclaration)ent) return;
209 		if (ent.qualifiedName in visited) return;
210 		visited[ent.qualifiedName] = true;
211 
212 		string kind = ent.classinfo.name.split(".")[$-1].toLower;
213 		string[] attributes;
214 		if (auto fdecl = cast(FunctionDeclaration)ent) attributes = fdecl.attributes;
215 		else if (auto adecl = cast(AliasDeclaration)ent) attributes = adecl.attributes;
216 		else if (auto tdecl = cast(TypedDeclaration)ent) attributes = tdecl.type.attributes;
217 		attributes = attributes.map!(a => a.startsWith("@") ? a[1 .. $] : a).array;
218 		dst.formattedWrite(`{name: "%s", kind: "%s", path: "%s", attributes: %s},`, ent.qualifiedName, kind, link_to(ent), attributes);
219 		dst.put('\n');
220 	}
221 
222 	void writeEntryRec(Entity ent) {
223 		writeEntry(ent);
224 		if (cast(FunctionDeclaration)ent) return;
225 		ent.iterateChildren((ch) { writeEntryRec(ch); return true; });
226 	}
227 
228 	dst.write("// symbol index generated by DDOX - do not edit\n");
229 	dst.write("var symbols = [\n");
230 	writeEntryRec(root_package);
231 	dst.write("];\n");
232 }
233 
234 void generateApiIndex(OutputStream dst, Package root_package, GeneratorSettings settings, string delegate(Entity) link_to, HTTPServerRequest req = null)
235 {
236 	auto info = new DocPageInfo;
237 	info.linkTo = link_to;
238 	info.settings = settings;
239 	info.rootPackage = root_package;
240 	info.node = root_package;
241 
242 	dst.parseDietFileCompat!("ddox.overview.dt",
243 		HTTPServerRequest, "req",
244 		DocPageInfo, "info")
245 		(Variant(req), Variant(info));
246 }
247 
248 void generateModulePage(OutputStream dst, Package root_package, Module mod, GeneratorSettings settings, string delegate(Entity) link_to, HTTPServerRequest req = null)
249 {
250 	auto info = new DocPageInfo;
251 	info.linkTo = link_to;
252 	info.settings = settings;
253 	info.rootPackage = root_package;
254 	info.mod = mod;
255 	info.node = mod;
256 	info.docGroups = null;
257 
258 	dst.parseDietFile!("ddox.module.dt", req, info);
259 }
260 
261 void generateDeclPage(OutputStream dst, Package root_package, Module mod, string nested_name, DocGroup[] docgroups, GeneratorSettings settings, string delegate(Entity) link_to, HTTPServerRequest req = null)
262 {
263 	import std.algorithm : sort;
264 
265 	auto info = new DocPageInfo;
266 	info.linkTo = link_to;
267 	info.settings = settings;
268 	info.rootPackage = root_package;
269 	info.mod = mod;
270 	info.node = mod;
271 	info.docGroups = docgroups;//docGroups(mod.lookupAll!Declaration(nested_name));
272 	sort!((a, b) => cmpKind(a.members[0], b.members[0]))(info.docGroups);
273 	info.nestedName = nested_name;
274 
275 	dst.parseDietFile!("ddox.docpage.dt", req, info);
276 }
277 
278 private bool cmpKind(Entity a, Entity b)
279 {
280 	static immutable kinds = [
281 		DeclarationKind.Variable,
282 		DeclarationKind.Function,
283 		DeclarationKind.Struct,
284 		DeclarationKind.Union,
285 		DeclarationKind.Class,
286 		DeclarationKind.Interface,
287 		DeclarationKind.Enum,
288 		DeclarationKind.EnumMember,
289 		DeclarationKind.Template,
290 		DeclarationKind.TemplateParameter,
291 		DeclarationKind.Alias
292 	];
293 
294 	auto ad = cast(Declaration)a;
295 	auto bd = cast(Declaration)b;
296 
297 	if (!ad && !bd) return false;
298 	if (!ad) return false;
299 	if (!bd) return true;
300 
301 	auto ak = kinds.countUntil(ad.kind);
302 	auto bk = kinds.countUntil(bd.kind);
303 
304 	if (ak < 0 && bk < 0) return false;
305 	if (ak < 0) return false;
306 	if (bk < 0) return true;
307 
308 	return ak < bk;
309 }