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