1 /**
2 	Serves documentation on through HTTP server.
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.htmlserver;
9 
10 import ddox.api;
11 import ddox.ddoc; // just so that rdmd picks it up
12 import ddox.entities;
13 import ddox.htmlgenerator;
14 import ddox.settings;
15 
16 import std.array;
17 import std.string;
18 import vibe.core.log;
19 import vibe.http.fileserver;
20 import vibe.http.router;
21 
22 
23 void registerApiDocs(URLRouter router, Package pack, GeneratorSettings settings = null)
24 {
25 	if( !settings ) settings = new GeneratorSettings;
26 
27 	string linkTo(in Entity ent_, size_t level)
28 	{
29 		import std.typecons : Rebindable;
30 
31 		Rebindable!(const(Entity)) ent = ent_;
32 		auto dst = appender!string();
33 
34 		if( level ) foreach( i; 0 .. level ) dst.put("../");
35 		else dst.put("./");
36 
37 		if( ent !is null && ent.parent !is null ){
38 			Entity nested;
39 			if (
40 				// link parameters to their function
41 				(cast(FunctionDeclaration)ent.parent !is null &&
42 				 (nested = cast(VariableDeclaration)ent) !is null) ||
43 				// link enum members to their enum
44 				(!settings.enumMemberPages &&
45 				 cast(EnumDeclaration)ent.parent !is null &&
46 				 (nested = cast(EnumMemberDeclaration)ent) !is null))
47 				ent = ent.parent;
48 
49 			const(Entity)[] nodes;
50 			size_t mod_idx = 0;
51 			while( ent ){
52 				if( cast(Module)ent ) mod_idx = nodes.length;
53 				nodes ~= ent;
54 				ent = ent.parent;
55 			}
56 			foreach_reverse(i, n; nodes[mod_idx .. $-1]){
57 				dst.put(n.name[]);
58 				if( i > 0 ) dst.put('.');
59 			}
60 			dst.put("/");
61 			foreach_reverse(i, n; nodes[0 .. mod_idx]){
62 				dst.put(n.name[]);
63 				if( i > 0 ) dst.put('.');
64 			}
65 
66 			// link nested elements to anchor in parent, e.g. params, enum members
67 			if( nested ){
68 				dst.put('#');
69 				dst.put(nested.name[]);
70 			}
71 		}
72 
73 		return dst.data();
74 	}
75 
76 	void showApi(HTTPServerRequest req, HTTPServerResponse res)
77 	{
78 		res.contentType = "text/html; charset=UTF-8";
79 		generateApiIndex(res.bodyWriter, pack, settings, ent => linkTo(ent, 0), req);
80 	}
81 
82 	void showApiModule(HTTPServerRequest req, HTTPServerResponse res)
83 	{
84 		auto mod = pack.lookup!Module(req.params["modulename"]);
85 		if( !mod ) return;
86 
87 		res.contentType = "text/html; charset=UTF-8";
88 		generateModulePage(res.bodyWriter, pack, mod, settings, ent => linkTo(ent, 1), req);
89 	}
90 
91 	void showApiItem(HTTPServerRequest req, HTTPServerResponse res)
92 	{
93 		import std.algorithm;
94 
95 		auto mod = pack.lookup!Module(req.params["modulename"]);
96 		logDebug("mod: %s", mod !is null);
97 		if( !mod ) return;
98 		auto items = mod.lookupAll!Declaration(req.params["itemname"]);
99 		logDebug("items: %s", items.length);
100 		if( !items.length ) return;
101 
102 		auto docgroups = items.map!(i => i.docGroup).uniq.array;
103 
104 		res.contentType = "text/html; charset=UTF-8";
105 		generateDeclPage(res.bodyWriter, pack, mod, items[0].nestedName, docgroups, settings, ent => linkTo(ent, 1), req);
106 	}
107 
108 	void showSitemap(HTTPServerRequest req, HTTPServerResponse res)
109 	{
110 		res.contentType = "application/xml";
111 		generateSitemap(res.bodyWriter, pack, settings, ent => linkTo(ent, 0), req);
112 	}
113 
114 	void showSearchResults(HTTPServerRequest req, HTTPServerResponse res)
115 	{
116 		import std.algorithm.iteration : map, splitter;
117 		import std.algorithm.sorting : sort;
118 		import std.algorithm.searching : canFind;
119 		import std.conv : to;
120 
121 		auto terms = req.query.get("q", null).splitter(' ').map!(t => t.toLower()).array;
122 
123 		size_t getPrefixIndex(string[] parts)
124 		{
125 			foreach_reverse (i, p; parts)
126 				foreach (t; terms)
127 					if (p.startsWith(t))
128 						return parts.length - 1 - i;
129 			return parts.length;
130 		}
131 
132 		immutable(CachedString)[] getAttributes(Entity ent)
133 		{
134 			if (auto fdecl = cast(FunctionDeclaration)ent) return fdecl.attributes;
135 			else if (auto adecl = cast(AliasDeclaration)ent) return adecl.attributes;
136 			else if (auto tdecl = cast(TypedDeclaration)ent) return tdecl.type.attributes;
137 			else return null;
138 		}
139 
140 		bool sort_pred(Entity a, Entity b)
141 		{
142 			// prefer non-deprecated matches
143 			auto adep = getAttributes(a).canFind("deprecated");
144 			auto bdep = getAttributes(b).canFind("deprecated");
145 			if (adep != bdep) return bdep;
146 
147 			// normalize the names
148 			auto aname = a.qualifiedName.to!string.toLower(); // FIXME: avoid GC allocations
149 			auto bname = b.qualifiedName.to!string.toLower();
150 
151 			auto anameparts = aname.split("."); // FIXME: avoid GC allocations
152 			auto bnameparts = bname.split(".");
153 
154 			auto asname = anameparts[$-1];
155 			auto bsname = bnameparts[$-1];
156 
157 			// prefer exact matches
158 			auto aexact = terms.canFind(asname);
159 			auto bexact = terms.canFind(bsname);
160 			if (aexact != bexact) return aexact;
161 
162 			// prefer prefix matches
163 			auto apidx = getPrefixIndex(anameparts);
164 			auto bpidx = getPrefixIndex(bnameparts);
165 			if (apidx != bpidx) return apidx < bpidx;
166 
167 			// prefer elements with less nesting
168 			if (anameparts.length != bnameparts.length)
169 				return anameparts.length < bnameparts.length;
170 
171 			// prefer matches with a shorter name
172 			if (asname.length != bsname.length)
173 				return asname.length < bsname.length;
174 
175 			// sort the rest alphabetically
176 			return aname < bname;
177 		}
178 
179 		auto dst = appender!(Entity[]);
180 		if (terms.length)
181 			searchEntries(dst, pack, terms);
182 		dst.data.sort!sort_pred();
183 
184 		static class Info : DocPageInfo {
185 			Entity[] results;
186 		}
187 		scope info = new Info;
188 		info.linkTo = (e) => linkTo(e, 0);
189 		info.settings = settings;
190 		info.rootPackage = pack;
191 		info.node = pack;
192 		info.results = dst.data;
193 
194 		res.render!("ddox.search-results.dt", req, info);
195 	}
196 
197 	string symbols_js;
198 	string symbols_js_md5;
199 
200 	void showSymbolJS(HTTPServerRequest req, HTTPServerResponse res)
201 	{
202 		if (!symbols_js.length) {
203 			import std.digest.md;
204 			import vibe.stream.memory;
205 			auto os = createMemoryOutputStream;
206 			generateSymbolsJS(os, pack, settings, ent => linkTo(ent, 0));
207 			symbols_js = cast(string)os.data;
208 			symbols_js_md5 = '"' ~ md5Of(symbols_js).toHexString().idup ~ '"';
209 		}
210 
211 		if (req.headers.get("If-None-Match", "") == symbols_js_md5) {
212 			res.statusCode = HTTPStatus.NotModified;
213 			res.writeVoidBody();
214 			return;
215 		}
216 
217 		res.headers["ETag"] = symbols_js_md5;
218 		res.writeBody(symbols_js, "application/javascript");
219 	}
220 
221 	auto path_prefix = settings.siteUrl.path.toString();
222 	if( path_prefix.endsWith("/") ) path_prefix = path_prefix[0 .. $-1];
223 
224 	router.get(path_prefix~"/", &showApi);
225 	router.get(path_prefix~"/:modulename/", &showApiModule);
226 	router.get(path_prefix~"/:modulename/:itemname", &showApiItem);
227 	router.get(path_prefix~"/sitemap.xml", &showSitemap);
228 	router.get(path_prefix~"/symbols.js", &showSymbolJS);
229 	router.get(path_prefix~"/search", &showSearchResults);
230 	router.get("*", serveStaticFiles("public"));
231 
232 	// convenience redirects (when leaving off the trailing slash)
233 	if( path_prefix.length ) router.get(path_prefix, staticRedirect(path_prefix~"/"));
234 	router.get(path_prefix~"/:modulename", (HTTPServerRequest req, HTTPServerResponse res){ res.redirect(path_prefix~"/"~req.params["modulename"]~"/"); });
235 }
236 
237 private void searchEntries(R)(ref R dst, Entity root_ent, string[] search_terms) {
238 	bool[DocGroup] known_groups;
239 	void searchRec(Entity ent) {
240 		import std.conv : to;
241 		if ((!ent.docGroup || ent.docGroup !in known_groups) && matchesSearch(ent.qualifiedName.to!string, search_terms)) // FIXME: avoid GC allocations
242 			dst.put(ent);
243 		known_groups[ent.docGroup] = true;
244 		if (cast(FunctionDeclaration)ent) return;
245 		ent.iterateChildren((ch) { searchRec(ch); return true; });
246 	}
247 	searchRec(root_ent);
248 }
249 
250 private bool matchesSearch(string name, in string[] terms)
251 {
252 	import std.algorithm.searching : canFind;
253 
254 	foreach (t; terms)
255 		if (!name.toLower().canFind(t)) // FIXME: avoid GC allocations
256 			return false;
257 	return true;
258 }