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 }