1/* 2 * Copyright (C) 2015 Jérémy Munsch <jeremy.munsch@gmail.com> 3 * 4 * This program is free software; you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation; either version 2 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program; if not, write to the Free Software 16 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 17 * 18 * Authored by Jérémy Munsch <jeremy.munsch@gmail.com> 19 * 20 */ 21 22namespace Synapse 23{ 24 /** 25 * This plugin looks for zeal installed documentations 26 * then it allows you to make a query on a specified doc 27 * It supports queries like : 28 * zeal php:php 29 * or 30 * zeal php constant 31 * zeal php:constant 32 * php constant 33 * php magic constant 34 * php magic constant zeal 35 * php : magic constant zeal 36 * 37 * and so it searches "php:magic constants" in zeal by opening it. 38 * It handles complicated doc names like Apache_HTTP_Server "apache license" 39 * "server license" "http license" these 3 result in "apache http server:license" 40 * 41 * An update would consist to aggregate the sqlite databases 42 * and show results directly or develop a CLI update to zeal 43 * directly. 44 */ 45 public class ZealPlugin : Object, Activatable, ItemProvider 46 { 47 public bool enabled { get; set; default = true; } 48 49 Gee.List<ZealDoc> doclist; 50 51 public void activate () 52 { 53 string docsets_path = "%s/Zeal/Zeal/docsets/".printf (Environment.get_user_data_dir ()); 54 doclist = new Gee.ArrayList<ZealDoc>(); 55 56 try 57 { 58 Dir dir = Dir.open (docsets_path, 0); 59 string? name = null; 60 61 while ((name = dir.read_name ()) != null) 62 { 63 string path = Path.build_filename (docsets_path, name); 64 string type = ""; 65 66 if (FileUtils.test (path, FileTest.IS_REGULAR)) 67 type += "| REGULAR "; 68 if (FileUtils.test (path, FileTest.IS_SYMLINK)) 69 type += "| SYMLINK "; 70 if (FileUtils.test (path, FileTest.IS_DIR)) 71 type += "| DIR "; 72 if (FileUtils.test (path, FileTest.IS_EXECUTABLE)) 73 type += "| EXECUTABLE "; 74 75 if (path == "") 76 continue; 77 78 var zdoc = new ZealDoc (path); 79 doclist.add (zdoc); 80 } 81 } 82 catch (FileError e) 83 { 84 warning ("%s", e.message); 85 } 86 } 87 88 public void deactivate () 89 { 90 doclist = null; 91 } 92 93 static construct 94 { 95 register_plugin (); 96 } 97 98 static void register_plugin () 99 { 100 PluginRegistry.get_default ().register_plugin ( 101 typeof (ZealPlugin), 102 _("Zeal"), 103 _("Zeal offline documentation (zealdocs.org)"), 104 "zeal", 105 register_plugin, 106 Environment.find_program_in_path ("zeal") != null, 107 _("zeal is not installed, please see zealdocs.org") 108 ); 109 } 110 111 public bool handles_query (Query query) 112 { 113 return (QueryFlags.ACTIONS in query.query_type && doclist.size > 0); 114 } 115 116 public async ResultSet? search (Query q) throws SearchError 117 { 118 Idle.add (search.callback); 119 yield; 120 q.check_cancellable (); 121 122 q.query_string = q.query_string.down ().replace ("zeal", "").strip (); 123 var results = new ResultSet (); 124 var matchers = Query.get_matchers_for_query (q.query_string, 0, RegexCompileFlags.OPTIMIZE | RegexCompileFlags.CASELESS); 125 126 foreach (var doc in this.doclist) 127 { 128 foreach (var matcher in matchers) 129 { 130 if (matcher.key.match (doc.scf_bundle_name)) 131 { 132 doc.update_title (q.query_string); 133 results.add (doc, matcher.value); 134 } 135 } 136 137 if (doc.regex.match (q.query_string)) 138 { 139 foreach (var part in doc.scf_bundle_name.split_set ("_ ")) 140 q.query_string = ZealDoc.replace_first_occurence (q.query_string, part, ""); 141 doc.update_title (q.query_string.replace (":", "").strip ()); 142 results.add (doc, MatchScore.AVERAGE); 143 break; 144 } 145 } 146 147 q.check_cancellable (); 148 return results; 149 } 150 } 151 152 public class ZealDoc : ActionMatch 153 { 154 public string doc_path { get; construct; } 155 156 string query = ""; 157 string doc_name = ""; 158 public string scf_bundle_identifier = ""; 159 public string scf_bundle_name = ""; 160 public Regex regex; 161 string version; 162 163 public ZealDoc (string doc_path) 164 { 165 Object ( 166 title: "Zeal Doc", 167 description: _("Zeal documentation research"), 168 icon_name: "zeal", 169 has_thumbnail: false, 170 doc_path: doc_path 171 ); 172 } 173 174 construct 175 { 176 parse_doc_name (); 177 parse_doc_bundle (); 178 179 try 180 { 181 regex = new Regex ("^(%s)[ :]+([a-z0-9 -_.:;,]*)".printf (string.joinv ("|", scf_bundle_name.split_set ("_ "))), RegexCompileFlags.OPTIMIZE); 182 } 183 catch (GLib.RegexError e) 184 { 185 regex = null; 186 warning ("regex error %s", e.message); 187 } 188 } 189 190 public override void do_action () 191 { 192 try 193 { 194 AppInfo ai = AppInfo.create_from_commandline ("zeal \"%s:%s\"".printf (scf_bundle_name, query), "zeal", 0); 195 ai.launch (null, null); 196 } 197 catch (Error err) 198 { 199 warning ("Could not launch zeal %s", err.message); 200 } 201 } 202 203 public void update_title (string query) 204 { 205 this.query = query; 206 string version = this.version != null ? " (v%s)".printf (this.version) : ""; 207 title = "Search for %s in %s%s".printf (query, doc_name, version); 208 } 209 210 public static string replace_first_occurence (string str, string search, string replace) 211 { 212 int pos = str.index_of (search); 213 if (pos < 0) 214 return str; 215 return str.substring (0, pos) + replace + str.substring (pos + search.length); 216 } 217 218 private void parse_doc_name () 219 { 220 string data; 221 222 try 223 { 224 FileUtils.get_contents (doc_path + "/meta.json", out data); 225 Json.Parser parser = new Json.Parser (); 226 227 if (parser.load_from_data (data, -1)) 228 { 229 Json.Node node = parser.get_root (); 230 231 if (node.get_node_type () != Json.NodeType.OBJECT) 232 throw new Json.ParserError.PARSE ("Unexpected element type %s", node.type_name ()); 233 234 unowned Json.Object obj = node.get_object (); 235 foreach (unowned string name in obj.get_members ()) 236 { 237 switch (name) 238 { 239 case "version": 240 unowned Json.Node item = obj.get_member (name); 241 if (item.get_node_type () != Json.NodeType.VALUE) 242 throw new Json.ParserError.PARSE ("Unexpected element type %s", item.type_name ()); 243 version = obj.get_string_member ("version"); 244 break; 245 246 case "name": 247 unowned Json.Node item = obj.get_member (name); 248 if (item.get_node_type () != Json.NodeType.VALUE) 249 throw new Json.ParserError.PARSE ("Unexpected element type %s", item.type_name ()); 250 doc_name = obj.get_string_member ("name"); 251 break; 252 } 253 } 254 } 255 else 256 { 257 throw new Json.ParserError.PARSE ("Unable to parse data form %s", doc_path + "/meta.json"); 258 } 259 } 260 catch (Error e) 261 { 262 warning ("%s", e.message); 263 } 264 } 265 266 private void parse_doc_bundle () 267 { 268 string contents; 269 Regex exp = /\<key\>([a-zA-Z0-9 _-]+)\<\/key\>[\n\t ]*\<string\>([a-zA-Z0-9\. _-]+)\<\/string\>/; 270 271 try 272 { 273 FileUtils.get_contents (doc_path + "/Contents/Info.plist", out contents, null); 274 } 275 catch (Error e) 276 { 277 warning ("Unable to read file: %s", e.message); 278 } 279 280 try 281 { 282 MatchInfo mi; 283 for (exp.match (contents, 0, out mi) ; mi.matches () ; mi.next ()) 284 { 285 switch (mi.fetch (1)) 286 { 287 case "CFBundleIdentifier": 288 scf_bundle_identifier = mi.fetch (2).down (); 289 break; 290 case "CFBundleName": 291 scf_bundle_name = mi.fetch (2).down (); 292 break; 293 } 294 } 295 } 296 catch (Error e) 297 { 298 warning ("Regex failed: %s", e.message); 299 } 300 } 301 } 302} 303