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 }