From 4623a802a5d44f652d414d0c48017d55c1bdfae9 Mon Sep 17 00:00:00 2001 From: Is Isilon Date: Sun, 7 Feb 2016 22:48:22 +0800 Subject: [PATCH 1/9] added services, and stems empty input --- package.json | 11 +++-- public/index.html | 30 +++++++++----- public/js/main.js | 101 ++++++++++++++++++++++++++++++++-------------- 3 files changed, 99 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index e14a73c..d646ac0 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,17 @@ "name": "keywordshitter2", "version": "0.0.5", "description": "The second generation of keyword shitter. Bigger, better, and more pungent than other products.", - "main": "public/index.html", + "main": "server.js", "scripts": { "test": "mocha", "build-css": "lessc ./src/less/main.less > public/css/style.css", - "build": "npm run build-css" + "build": "npm run build-css", + "start": "node server.js" }, "author": "", - "license": "MIT" + "license": "MIT", + "devDependencies": { + "cors": "^2.7.1", + "express": "^4.13.4" + } } diff --git a/public/index.html b/public/index.html index 5c174c0..836dd96 100644 --- a/public/index.html +++ b/public/index.html @@ -55,8 +55,8 @@ lying internet
- - @@ -64,19 +64,29 @@ lying internet + +
+
+ + +
+
+ + +
- +
- - + +
@@ -125,11 +135,11 @@ lying internet id - Keyword + Keyword Length Volume CPC - Search + Search @@ -143,11 +153,13 @@ lying internet
- + - + + + diff --git a/public/js/main.js b/public/js/main.js index b5e7ab0..f166a23 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -29,19 +29,46 @@ $.getJSON('https://api.ipify.org?format=json', function (data) { // TODO Implement alternative services // Ref: https://github.com/estivo/Instantfox/blob/master/firefox/c1hrome/content/defaultPluginList.js // Ref: https://github.com/bnoordhuis/mozilla-central/tree/master/browser/locales/en-US/searchplugins -services={ - "google": - "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&q=${query}&ds=${site}&gl=${country}", - "yahoo": - "https://search.yahoo.com/sugg/ff?output=fxjson&appid=ffd&command=${query}", - "bing": "http://api.bing.com/osjson.aspx?query=${query}", - "ebay": - "http://autosug.ebay.com/autosug?kwd=${query}&_jgr=1&sId=0&_ch=0&callback=nil", - "amazon": - "http://completion.amazon.co.uk/search/complete?method=completion&q=${query}&search-alias=aps&mkt=4", - "twitter": - "https://twitter.com/i/search/typeahead.json?count=${max_results}&q=${query}&result_type=topics&src=SEARCH_BOX" + +/** + * Get the service url based on options set in the dom. + * @return {String} A jsonp url for search suggestions with query missing from the end. + */ +function getUrl(){ + services={ + "google": + "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&gl=${country}&callback=?&q=", + "google news": + "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&ds=n&gl=${country}&callback=?&q=", + "google shopping": + "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&ds=sh&gl=${country}&callback=?&q=", + "google books": + "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&ds=bo&gl=${country}&callback=?&q=", + "youtube": + "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&ds=yt&gl=${country}&callback=?&q=", + "google videos": + "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&ds=v&gl=${country}&callback=?&q=", + "google images": + "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&ds=i&gl=${country}&callback=?&q=", + "yahoo": + "https://search.yahoo.com/sugg/ff?output=jsonp&appid=ffd&callback=?&command=", + "bing": "http://api.bing.com/osjson.aspx?JsonType=callback&JsonCallback=?&query=", + "ebay": + "http://autosug.ebay.com/autosug?_jgr=1&sId=0&_ch=0&callback=?&kwd=", + "amazon": + "http://completion.amazon.co.uk/search/complete?method=completion&search-alias=aps&mkt=4&callback=?&q=", + "twitter": + "https://twitter.com/i/search/typeahead.json?count=30&result_type=topics&src=SEARCH_BOX&callback=?&q=" + }; + options={ + country: $('#country').val(), + service: $('#service').val(), + lang: $('#lang').val(), }; + return _.template(services[options.service])(options); +} + + function ebayParser(){} // s = req.lstrip('/**/nil/(').rstrip(')') // sugg_texts = json.loads(s)['res']['sug'] @@ -56,15 +83,22 @@ function twitterParser(){} // 'meta': j, // 'relevances': [t['rounded_score'] for t in j['topics']], // } + +/** Parse response per service **/ +function parseServiceResponse(res){ + var service = $('#service').val(); + var parser = RESPONSE_TEMPLATES[service] || RESPONSE_TEMPLATES["default"]; + return parser(res); +} var RESPONSE_TEMPLATES = { - "google": ['query', 'sugg_texts', 'sugg_titles', '_', 'meta'], - "google_maps": ['query', 'sugg_texts', 'sugg_titles', '_', 'meta'], - "yahoo": ['query', 'sugg_texts'], - "bing": ['query', 'sugg_texts'], - "bing_search": ['query', 'sugg_texts'], - "ebay": ebayParser, - "amazon": ['query', 'sugg_texts'], - "twitter": twitterParser + "default": function(res){return res[1];}, + "google": function(res){return res[1];}, + "youtube": function(res){return res[1];}, + "yahoo": function(res){return _.map(res.gossip.results,'key');}, + "bing": function(res){return res[1];}, + "ebay": function(res){return res.res.sug;}, + "amazon": function(res){return res[1];}, + "twitter": function(res){return Array.concat(res.users,_.map(res.topics,'topic'),res.hashtags,res.oneclick);} }; // setup a db. Ref: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB @@ -130,12 +164,12 @@ function StartJob() { suffixes = $('#suffixes').val().split(','); var ks = $('#input').val().split("\n"); - var i = 0; - for (i = 0; i < ks.length; i++) { - keywordsToQuery[keywordsToQuery.length] = ks[i]; - + for (var i = 0; i < ks.length; i++) { + if (ks[i].trim().length) + keywordsToQuery[keywordsToQuery.length] = ks[i]; } numOfInitialKeywords = keywordsToQuery.length; + if (!numOfInitialKeywords) permuteResultsToQueue([' ']); FilterAndDisplay(); doWork = true; @@ -342,17 +376,18 @@ function QueryKeyword(search) { } else { // search not done, lets do the query + url = getUrl()+search; $.ajax({ - url: "http://suggestqueries.google.com/complete/search", + url: url, jsonp: "jsonp", dataType: "jsonp", - data: { - q: search, - client: "chrome" - }, + // data: { + // q: search, + // client: "chrome" + // }, success: function (res, statusText, jqXHR) { - // var search = res[0]; - var retList = res[1]; + + var retList = parseServiceResponse(res); var char, currentx; storeResults(retList, search, this.url); @@ -477,6 +512,8 @@ function FilterAndDisplay() { function loadSettings(){ // TODO do table settings as well, e.g. column visibilitity if (localStorage.service) $("#service").val( localStorage.service ); + if (localStorage.country) $('#country').val(localStorage.country); + if (localStorage.lang) $('#lang').val(localStorage.lang); if (localStorage.filterNegative) $("#filter-negative").val( localStorage.filterNegative ); if (localStorage.filterPositive) $("#filter-positive").val( localStorage.filterPositive ); if (localStorage.rateLimit) $("#rate-limit").val( localStorage.rateLimit ); @@ -488,6 +525,8 @@ function loadSettings(){ /** save settings to localStorage. **/ function saveSettings(){ localStorage.service = $('#service').val(); + localStorage.country = $('#country').val(); + localStorage.lang = $('#lang').val(); localStorage.filterNegative = $('#filter-negative').val(); localStorage.filterPositive = $('#filter-positive').val(); localStorage.rateLimit = $('#rate-limit').val(); From b2f1b5a24751cb4ed0bb7ddcd35306a3e8c1b946 Mon Sep 17 00:00:00 2001 From: Is Isilon Date: Wed, 10 Feb 2016 10:07:45 +0800 Subject: [PATCH 2/9] Bugfix, faster db transactions, show source --- public/favicon.ico | Bin 0 -> 1150 bytes public/fonts/glyphicons-halflings-regular.eot | Bin 0 -> 20127 bytes public/fonts/glyphicons-halflings-regular.svg | 288 ++++++++++++++++++ public/fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 45404 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 0 -> 23424 bytes .../fonts/glyphicons-halflings-regular.woff2 | Bin 0 -> 18028 bytes public/js/main.js | 273 ++++++++++------- 7 files changed, 449 insertions(+), 112 deletions(-) create mode 100644 public/favicon.ico create mode 100644 public/fonts/glyphicons-halflings-regular.eot create mode 100644 public/fonts/glyphicons-halflings-regular.svg create mode 100644 public/fonts/glyphicons-halflings-regular.ttf create mode 100644 public/fonts/glyphicons-halflings-regular.woff create mode 100644 public/fonts/glyphicons-halflings-regular.woff2 diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b4210211497e3972296e186d2147925a619347b7 GIT binary patch literal 1150 zcmbu7zfW3G6vr>WB{G;w8v~)KA&r8qLK79CgLE)WWHJ>h;9$CRQ&*sqorn&sn&@mT z5$OK_VIhRchJ_ysGXn|X$G7($c`xNf5}MwZb58C#_jAs<=Uv8B{F|F&T)$#pwT!)B zj4gBFF7A2sbHf;qJ)3kWYO%q}@9yZFUaOT^ zDTma@^3oFO^*Y+^HaeZoSo!e5fy|4VN)bJ&_)N_6^RO>2!m_Y{U%4FR5chJetr0J3 zDOuW&W@$2+P^;CDOeW#+cx3+5-X1pB*YV!xgU{=IQba5i4ASg=+}^_NazXqPQUCP# z7@tES97dyvMj~Sp35W6J=m=h3Z*yY<-_OqEyS}#D(Q38EW})1O{9C&UQafc%~E`l`n$g16Y)54MuUNDDwUdq zl3&S7^=FMn;!jUb{)vx|?yn#99|#1d<1-mJ9S&TapX0gN3UGurY literal 0 HcmV?d00001 diff --git a/public/fonts/glyphicons-halflings-regular.eot b/public/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..b93a4953fff68df523aa7656497ee339d6026d64 GIT binary patch literal 20127 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/fonts/glyphicons-halflings-regular.ttf b/public/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1413fc609ab6f21774de0cb7e01360095584f65b GIT binary patch literal 45404 zcmd?Sd0-pWwLh*qi$?oCk~i6sWlOeWJC3|4juU5JNSu9hSVACzERcmjLV&P^utNzg zIE4Kr1=5g!SxTX#Ern9_%4&01rlrW`Z!56xXTGQR4C z3vR~wXq>NDx$c~e?;ia3YjJ*$!C>69a?2$lLyhpI!CFfJsP=|`8@K0|bbMpWwVUEygg0=0x_)HeHpGSJagJNLA3c!$EuOV>j$wi! zbo{vZ(s8tl>@!?}dmNHXo)ABy7ohD7_1G-P@SdJWT8*oeyBVYVW9*vn}&VI4q++W;Z+uz=QTK}^C75!`aFYCX# zf7fC2;o`%!huaTNJAB&VWrx=szU=VLhwnbT`vc<#<`4WI6n_x@AofA~2d90o?1L3w z9!I|#P*NQ)$#9aASijuw>JRld^-t)Zhmy|i-`Iam|IWkguaMR%lhi4p~cX-9& zjfbx}yz}s`4-6>D^+6FzihR)Y!GsUy=_MWi_v7y#KmYi-{iZ+s@ekkq!@Wxz!~BQwiI&ti z>hC&iBe2m(dpNVvSbZe3DVgl(dxHt-k@{xv;&`^c8GJY%&^LpM;}7)B;5Qg5J^E${ z7z~k8eWOucjX6)7q1a%EVtmnND8cclz8R1=X4W@D8IDeUGXxEWe&p>Z*voO0u_2!! zj3dT(Ki+4E;uykKi*yr?w6!BW2FD55PD6SMj`OfBLwXL5EA-9KjpMo4*5Eqs^>4&> z8PezAcn!9jk-h-Oo!E9EjX8W6@EkTHeI<@AY{f|5fMW<-Ez-z)xCvW3()Z#x0oydB zzm4MzY^NdpIF9qMp-jU;99LjlgY@@s+=z`}_%V*xV7nRV*Kwrx-i`FzI0BZ#yOI8# z!SDeNA5b6u9!Imj89v0(g$;dT_y|Yz!3V`i{{_dez8U@##|X9A};s^7vEd!3AcdyVlhVk$v?$O442KIM1-wX^R{U7`JW&lPr3N(%kXfXT_`7w^? z=#ntx`tTF|N$UT?pELvw7T*2;=Q-x@KmDUIbLyXZ>f5=y7z1DT<7>Bp0k;eItHF?1 zErzhlD2B$Tm|^7DrxnTYm-tgg`Mt4Eivp5{r$o9e)8(fXBO4g|G^6Xy?y$SM*&V52 z6SR*%`%DZC^w(gOWQL?6DRoI*hBNT)xW9sxvmi@!vI^!mI$3kvAMmR_q#SGn3zRb_ zGe$=;Tv3dXN~9XuIHow*NEU4y&u}FcZEZoSlXb9IBOA}!@J3uovp}yerhPMaiI8|SDhvWVr z^BE&yx6e3&RYqIg;mYVZ*3#A-cDJ;#ms4txEmwm@g^s`BB}KmSr7K+ruIoKs=s|gOXP|2 zb1!)87h9?(+1^QRWb(Vo8+@G=o24gyuzF3ytfsKjTHZJ}o{YznGcTDm!s)DRnmOX} z3pPL4wExoN$kyc2>#J`k+<67sy-VsfbQ-1u+HkyFR?9G`9r6g4*8!(!c65Be-5hUg zZHY$M0k(Yd+DT1*8)G(q)1&tDl=g9H7!bZTOvEEFnBOk_K=DXF(d4JOaH zI}*A3jGmy{gR>s}EQzyJa_q_?TYPNXRU1O;fcV_&TQZhd{@*8Tgpraf~nT0BYktu*n{a~ub^UUqQPyr~yBY{k2O zgV)honv{B_CqY|*S~3up%Wn%7i*_>Lu|%5~j)}rQLT1ZN?5%QN`LTJ}vA!EE=1`So z!$$Mv?6T)xk)H8JTrZ~m)oNXxS}pwPd#);<*>zWsYoL6iK!gRSBB{JCgB28C#E{T? z5VOCMW^;h~eMke(w6vLlKvm!!TyIf;k*RtK)|Q>_@nY#J%=h%aVb)?Ni_By)XNxY)E3`|}_u}fn+Kp^3p4RbhFUBRtGsDyx9Eolg77iWN z2iH-}CiM!pfYDIn7;i#Ui1KG01{3D<{e}uWTdlX4Vr*nsb^>l0%{O?0L9tP|KGw8w z+T5F}md>3qDZQ_IVkQ|BzuN08uN?SsVt$~wcHO4pB9~ykFTJO3g<4X({-Tm1w{Ufo zI03<6KK`ZjqVyQ(>{_aMxu7Zm^ck&~)Q84MOsQ-XS~{6j>0lTl@lMtfWjj;PT{nlZ zIn0YL?kK7CYJa)(8?unZ)j8L(O}%$5S#lTcq{rr5_gqqtZ@*0Yw4}OdjL*kBv+>+@ z&*24U=y{Nl58qJyW1vTwqsvs=VRAzojm&V zEn6=WzdL1y+^}%Vg!ap>x%%nFi=V#wn# zUuheBR@*KS)5Mn0`f=3fMwR|#-rPMQJg(fW*5e`7xO&^UUH{L(U8D$JtI!ac!g(Ze89<`UiO@L+)^D zjPk2_Ie0p~4|LiI?-+pHXuRaZKG$%zVT0jn!yTvvM^jlcp`|VSHRt-G@_&~<4&qW@ z?b#zIN)G(}L|60jer*P7#KCu*Af;{mpWWvYK$@Squ|n-Vtfgr@ZOmR5Xpl;0q~VILmjk$$mgp+`<2jP z@+nW5Oap%fF4nFwnVwR7rpFaOdmnfB$-rkO6T3#w^|*rft~acgCP|ZkgA6PHD#Of| zY%E!3tXtsWS`udLsE7cSE8g@p$ceu*tI71V31uA7jwmXUCT7+Cu3uv|W>ZwD{&O4Nfjjvl43N#A$|FWxId! z%=X!HSiQ-#4nS&smww~iXRn<-`&zc)nR~js?|Ei-cei$^$KsqtxNDZvl1oavXK#Pz zT&%Wln^Y5M95w=vJxj0a-ko_iQt(LTX_5x#*QfQLtPil;kkR|kz}`*xHiLWr35ajx zHRL-QQv$|PK-$ges|NHw8k6v?&d;{A$*q15hz9{}-`e6ys1EQ1oNNKDFGQ0xA!x^( zkG*-ueZT(GukSnK&Bs=4+w|(kuWs5V_2#3`!;f}q?>xU5IgoMl^DNf+Xd<=sl2XvkqviJ>d?+G@Z5nxxd5Sqd$*ENUB_mb8Z+7CyyU zA6mDQ&e+S~w49csl*UePzY;^K)Fbs^%?7;+hFc(xz#mWoek4_&QvmT7Fe)*{h-9R4 zqyXuN5{)HdQ6yVi#tRUO#M%;pL>rQxN~6yoZ)*{{!?jU)RD*oOxDoTjVh6iNmhWNC zB5_{R=o{qvxEvi(khbRS`FOXmOO|&Dj$&~>*oo)bZz%lPhEA@ zQ;;w5eu5^%i;)w?T&*=UaK?*|U3~{0tC`rvfEsRPgR~16;~{_S2&=E{fE2=c>{+y} zx1*NTv-*zO^px5TA|B```#NetKg`19O!BK*-#~wDM@KEllk^nfQ2quy25G%)l72<> zzL$^{DDM#jKt?<>m;!?E2p0l12`j+QJjr{Lx*47Nq(v6i3M&*P{jkZB{xR?NOSPN% zU>I+~d_ny=pX??qjF*E78>}Mgts@_yn`)C`wN-He_!OyE+gRI?-a>Om>Vh~3OX5+& z6MX*d1`SkdXwvb7KH&=31RCC|&H!aA1g_=ZY0hP)-Wm6?A7SG0*|$mC7N^SSBh@MG z9?V0tv_sE>X==yV{)^LsygK2=$Mo_0N!JCOU?r}rmWdHD%$h~~G3;bt`lH& zAuOOZ=G1Mih**0>lB5x+r)X^8mz!0K{SScj4|a=s^VhUEp#2M=^#WRqe?T&H9GnWa zYOq{+gBn9Q0e0*Zu>C(BAX=I-Af9wIFhCW6_>TsIH$d>|{fIrs&BX?2G>GvFc=<8` zVJ`#^knMU~65dWGgXcht`Kb>{V2oo%<{NK|iH+R^|Gx%q+env#Js*(EBT3V0=w4F@W+oLFsA)l7Qy8mx_;6Vrk;F2RjKFvmeq} zro&>@b^(?f))OoQ#^#s)tRL>b0gzhRYRG}EU%wr9GjQ#~Rpo|RSkeik^p9x2+=rUr}vfnQoeFAlv=oX%YqbLpvyvcZ3l$B z5bo;hDd(fjT;9o7g9xUg3|#?wU2#BJ0G&W1#wn?mfNR{O7bq747tc~mM%m%t+7YN}^tMa24O4@w<|$lk@pGx!;%pKiq&mZB z?3h<&w>un8r?Xua6(@Txu~Za9tI@|C4#!dmHMzDF_-_~Jolztm=e)@vG11bZQAs!tFvd9{C;oxC7VfWq377Y(LR^X_TyX9bn$)I765l=rJ%9uXcjggX*r?u zk|0!db_*1$&i8>d&G3C}A`{Fun_1J;Vx0gk7P_}8KBZDowr*8$@X?W6v^LYmNWI)lN92yQ;tDpN zOUdS-W4JZUjwF-X#w0r;97;i(l}ZZT$DRd4u#?pf^e2yaFo zbm>I@5}#8FjsmigM8w_f#m4fEP~r~_?OWB%SGWcn$ThnJ@Y`ZI-O&Qs#Y14To( zWAl>9Gw7#}eT(!c%D0m>5D8**a@h;sLW=6_AsT5v1Sd_T-C4pgu_kvc?7+X&n_fct znkHy(_LExh=N%o3I-q#f$F4QJpy>jZBW zRF7?EhqTGk)w&Koi}QQY3sVh?@e-Z3C9)P!(hMhxmXLC zF_+ZSTQU`Gqx@o(~B$dbr zHlEUKoK&`2gl>zKXlEi8w6}`X3kh3as1~sX5@^`X_nYl}hlbpeeVlj#2sv)CIMe%b zBs7f|37f8qq}gA~Is9gj&=te^wN8ma?;vF)7gce;&sZ64!7LqpR!fy)?4cEZposQ8 zf;rZF7Q>YMF1~eQ|Z*!5j0DuA=`~VG$Gg6B?Om1 z6fM@`Ck-K*k(eJ)Kvysb8sccsFf@7~3vfnC=<$q+VNv)FyVh6ZsWw}*vs>%k3$)9| zR9ek-@pA23qswe1io)(Vz!vS1o*XEN*LhVYOq#T`;rDkgt86T@O`23xW~;W_#ZS|x zvwx-XMb7_!hIte-#JNpFxskMMpo2OYhHRr0Yn8d^(jh3-+!CNs0K2B!1dL$9UuAD= zQ%7Ae(Y@}%Cd~!`h|wAdm$2WoZ(iA1(a_-1?znZ%8h72o&Mm*4x8Ta<4++;Yr6|}u zW8$p&izhdqF=m8$)HyS2J6cKyo;Yvb>DTfx4`4R{ zPSODe9E|uflE<`xTO=r>u~u=NuyB&H!(2a8vwh!jP!yfE3N>IiO1jI>7e&3rR#RO3_}G23W?gwDHgSgekzQ^PU&G5z&}V5GO? zfg#*72*$DP1T8i`S7=P;bQ8lYF9_@8^C(|;9v8ZaK2GnWz4$Th2a0$)XTiaxNWfdq z;yNi9veH!j)ba$9pke8`y2^63BP zIyYKj^7;2don3se!P&%I2jzFf|LA&tQ=NDs{r9fIi-F{-yiG-}@2`VR^-LIFN8BC4 z&?*IvLiGHH5>NY(Z^CL_A;yISNdq58}=u~9!Ia7 zm7MkDiK~lsfLpvmPMo!0$keA$`%Tm`>Fx9JpG^EfEb(;}%5}B4Dw!O3BCkf$$W-dF z$BupUPgLpHvr<<+QcNX*w@+Rz&VQz)Uh!j4|DYeKm5IC05T$KqVV3Y|MSXom+Jn8c zgUEaFW1McGi^44xoG*b0JWE4T`vka7qTo#dcS4RauUpE{O!ZQ?r=-MlY#;VBzhHGU zS@kCaZ*H73XX6~HtHd*4qr2h}Pf0Re@!WOyvres_9l2!AhPiV$@O2sX>$21)-3i+_ z*sHO4Ika^!&2utZ@5%VbpH(m2wE3qOPn-I5Tbnt&yn9{k*eMr3^u6zG-~PSr(w$p> zw)x^a*8Ru$PE+{&)%VQUvAKKiWiwvc{`|GqK2K|ZMy^Tv3g|zENL86z7i<c zW`W>zV1u}X%P;Ajn+>A)2iXZbJ5YB_r>K-h5g^N=LkN^h0Y6dPFfSBh(L`G$D%7c` z&0RXDv$}c7#w*7!x^LUes_|V*=bd&aP+KFi((tG*gakSR+FA26%{QJdB5G1F=UuU&koU*^zQA=cEN9}Vd?OEh| zgzbFf1?@LlPkcXH$;YZe`WEJ3si6&R2MRb}LYK&zK9WRD=kY-JMPUurX-t4(Wy{%` zZ@0WM2+IqPa9D(^*+MXw2NWwSX-_WdF0nMWpEhAyotIgqu5Y$wA=zfuXJ0Y2lL3#ji26-P3Z?-&0^KBc*`T$+8+cqp`%g0WB zTH9L)FZ&t073H4?t=(U6{8B+uRW_J_n*vW|p`DugT^3xe8Tomh^d}0k^G7$3wLgP& zn)vTWiMA&=bR8lX9H=uh4G04R6>C&Zjnx_f@MMY!6HK5v$T%vaFm;E8q=`w2Y}ucJ zkz~dKGqv9$E80NTtnx|Rf_)|3wxpnY6nh3U9<)fv2-vhQ6v=WhKO@~@X57N-`7Ppc zF;I7)eL?RN23FmGh0s;Z#+p)}-TgTJE%&>{W+}C`^-sy{gTm<$>rR z-X7F%MB9Sf%6o7A%ZHReD4R;imU6<9h81{%avv}hqugeaf=~^3A=x(Om6Lku-Pn9i zC;LP%Q7Xw*0`Kg1)X~nAsUfdV%HWrpr8dZRpd-#%)c#Fu^mqo|^b{9Mam`^Zw_@j@ zR&ZdBr3?@<@%4Z-%LT&RLgDUFs4a(CTah_5x4X`xDRugi#vI-cw*^{ncwMtA4NKjByYBza)Y$hozZCpuxL{IP&=tw6ZO52WY3|iwGf&IJCn+u(>icK zZB1~bWXCmwAUz|^<&ysd#*!DSp8}DLNbl5lRFat4NkvItxy;9tpp9~|@ z;JctShv^Iq4(z+y7^j&I?GCdKMVg&jCwtCkc4*@O7HY*veGDBtAIn*JgD$QftP}8= zxFAdF=(S>Ra6(4slk#h%b?EOU-96TIX$Jbfl*_7IY-|R%H zF8u|~hYS-YwWt5+^!uGcnKL~jM;)ObZ#q68ZkA?}CzV-%6_vPIdzh_wHT_$mM%vws9lxUj;E@#1UX?WO2R^41(X!nk$+2oJGr!sgcbn1f^yl1 z#pbPB&Bf;1&2+?};Jg5qgD1{4_|%X#s48rOLE!vx3@ktstyBsDQWwDz4GYlcgu$UJ zp|z_32yN72T*oT$SF8<}>e;FN^X&vWNCz>b2W0rwK#<1#kbV)Cf`vN-F$&knLo5T& z8!sO-*^x4=kJ$L&*h%rQ@49l?7_9IG99~xJDDil00<${~D&;kiqRQqeW5*22A`8I2 z(^@`qZoF7_`CO_e;8#qF!&g>UY;wD5MxWU>azoo=E{kW(GU#pbOi%XAn%?W{b>-bTt&2?G=E&BnK9m0zs{qr$*&g8afR_x`B~o zd#dxPpaap;I=>1j8=9Oj)i}s@V}oXhP*{R|@DAQXzQJekJnmuQ;vL90_)H_nD1g6e zS1H#dzg)U&6$fz0g%|jxDdz|FQN{KJ&Yx0vfuzAFewJjv`pdMRpY-wU`-Y6WQnJ(@ zGVb!-8DRJZvHnRFiR3PG3Tu^nCn(CcZHh7hQvyd7i6Q3&ot86XI{jo%WZqCPcTR0< zMRg$ZE=PQx66ovJDvI_JChN~k@L^Pyxv#?X^<)-TS5gk`M~d<~j%!UOWG;ZMi1af< z+86U0=sm!qAVJAIqqU`Qs1uJhQJA&n@9F1PUrYuW!-~IT>l$I!#5dBaiAK}RUufjg{$#GdQBkxF1=KU2E@N=i^;xgG2Y4|{H>s` z$t`k8c-8`fS7Yfb1FM#)vPKVE4Uf(Pk&%HLe z%^4L>@Z^9Z{ZOX<^e)~adVRkKJDanJ6VBC_m@6qUq_WF@Epw>AYqf%r6qDzQ~AEJ!jtUvLp^CcqZ^G-;Kz3T;O4WG45Z zFhrluCxlY`M+OKr2SeI697btH7Kj`O>A!+2DTEQ=48cR>Gg2^5uqp(+y5Sl09MRl* zp|28!v*wvMd_~e2DdKDMMQ|({HMn3D%%ATEecGG8V9>`JeL)T0KG}=}6K8NiSN5W< z79-ZdYWRUb`T}(b{RjN8>?M~opnSRl$$^gT`B27kMym5LNHu-k;A;VF8R(HtDYJHS zU7;L{a@`>jd0svOYKbwzq+pWSC(C~SPgG~nWR3pBA8@OICK$Cy#U`kS$I;?|^-SBC zBFkoO8Z^%8Fc-@X!KebF2Ob3%`8zlVHj6H;^(m7J35(_bS;cZPd}TY~qixY{MhykQ zV&7u7s%E=?i`}Ax-7dB0ih47w*7!@GBt<*7ImM|_mYS|9_K7CH+i}?*#o~a&tF-?C zlynEu1DmiAbGurEX2Flfy$wEVk7AU;`k#=IQE*6DMWafTL|9-vT0qs{A3mmZGzOyN zcM9#Rgo7WgB_ujU+?Q@Ql?V-!E=jbypS+*chI&zA+C_3_@aJal}!Q54?qsL0In({Ly zjH;e+_SK8yi0NQB%TO+Dl77jp#2pMGtwsgaC>K!)NimXG3;m7y`W+&<(ZaV>N*K$j zLL~I+6ouPk6_(iO>61cIsinx`5}DcKSaHjYkkMuDoVl>mKO<4$F<>YJ5J9A2Vl}#BP7+u~L8C6~D zsk`pZ$9Bz3teQS1Wb|8&c2SZ;qo<#F&gS;j`!~!ADr(jJXMtcDJ9cVi>&p3~{bqaP zgo%s8i+8V{UrYTc9)HiUR_c?cfx{Yan2#%PqJ{%?Wux4J;T$#cumM0{Es3@$>}DJg zqe*c8##t;X(4$?A`ve)e@YU3d2Balcivot{1(ahlE5qg@S-h(mPNH&`pBX$_~HdG48~)$x5p z{>ghzqqn_t8~pY<5?-To>cy^6o~mifr;KWvx_oMtXOw$$d6jddXG)V@a#lL4o%N@A zNJlQAz6R8{7jax-kQsH6JU_u*En%k^NHlvBB!$JAK!cYmS)HkLAkm0*9G3!vwMIWv zo#)+EamIJHEUV|$d|<)2iJ`lqBQLx;HgD}c3mRu{iK23C>G{0Mp1K)bt6OU?xC4!_ zZLqpFzeu&+>O1F>%g-%U^~yRg(-wSp@vmD-PT#bCWy!%&H;qT7rfuRCEgw67V!Qob z&tvPU@*4*$YF#2_>M0(75QxqrJr3Tvh~iDeFhxl=MzV@(psx%G8|I{~9;tv#BBE`l z3)_98eZqFNwEF1h)uqhBmT~mSmT8k$7vSHdR97K~kM)P9PuZdS;|Op4A?O<*%!?h` zn`}r_j%xvffs46x2hCWuo0BfIQWCw9aKkH==#B(TJ%p}p-RuIVzsRlaPL_Co{&R0h zQrqn=g1PGjQg3&sc2IlKG0Io#v%@p>tFwF)RG0ahYs@Zng6}M*d}Xua)+h&?$`%rb z;>M=iMh5eIHuJ5c$aC`y@CYjbFsJnSPH&}LQz4}za9YjDuao>Z^EdL@%saRm&LGQWXs*;FzwN#pH&j~SLhDZ+QzhplV_ij(NyMl z;v|}amvxRddO81LJFa~2QFUs z+Lk zZck)}9uK^buJNMo4G(rSdX{57(7&n=Q6$QZ@lIO9#<3pA2ceDpO_340B*pHlh_y{>i&c1?vdpN1j>3UN-;;Yq?P+V5oY`4Z(|P8SwWq<)n`W@AwcQ?E9 zd5j8>FT^m=MHEWfN9jS}UHHsU`&SScib$qd0i=ky0>4dz5ADy70AeIuSzw#gHhQ_c zOp1!v6qU)@8MY+ zMNIID?(CysRc2uZQ$l*QZVY)$X?@4$VT^>djbugLQJdm^P>?51#lXBkdXglYm|4{L zL%Sr?2f`J+xrcN@=0tiJt(<-=+v>tHy{XaGj7^cA6felUn_KPa?V4ebfq7~4i~GKE zpm)e@1=E;PP%?`vK6KVPKXjUXyLS1^NbnQ&?z>epHCd+J$ktT1G&L~T)nQeExe;0Z zlei}<_ni ztFo}j7nBl$)s_3odmdafVieFxc)m!wM+U`2u%yhJ90giFcU1`dR6BBTKc2cQ*d zm-{?M&%(={xYHy?VCx!ogr|4g5;V{2q(L?QzJGsirn~kWHU`l`rHiIrc-Nan!hR7zaLsPr4uR zG{En&gaRK&B@lyWV@yfFpD_^&z>84~_0Rd!v(Nr%PJhFF_ci3D#ixf|(r@$igZiWw za*qbXIJ_Hm4)TaQ=zW^g)FC6uvyO~Hg-#Z5Vsrybz6uOTF>Rq1($JS`imyNB7myWWpxYL(t7`H8*voI3Qz6mvm z$JxtArLJ(1wlCO_te?L{>8YPzQ})xJlvc5wv8p7Z=HviPYB#^#_vGO#*`<0r%MR#u zN_mV4vaBb2RwtoOYCw)X^>r{2a0kK|WyEYoBjGxcObFl&P*??)WEWKU*V~zG5o=s@ z;rc~uuQQf9wf)MYWsWgPR!wKGt6q;^8!cD_vxrG8GMoFGOVV=(J3w6Xk;}i)9(7*U zwR4VkP_5Zx7wqn8%M8uDj4f1aP+vh1Wue&ry@h|wuN(D2W;v6b1^ z`)7XBZ385zg;}&Pt@?dunQ=RduGRJn^9HLU&HaeUE_cA1{+oSIjmj3z+1YiOGiu-H zf8u-oVnG%KfhB8H?cg%@#V5n+L$MO2F4>XoBjBeX>css^h}Omu#)ExTfUE^07KOQS znMfQY2wz?!7!{*C^)aZ^UhMZf=TJNDv8VrrW;JJ9`=|L0`w9DE8MS>+o{f#{7}B4P z{I34>342vLsP}o=ny1eZkEabr@niT5J2AhByUz&i3Ck0H*H`LRHz;>3C_ru!X+EhJ z6(+(lI#4c`2{`q0o9aZhI|jRjBZOV~IA_km7ItNtUa(Wsr*Hmb;b4=;R(gF@GmsRI`pF+0tmq0zy~wnoJD(LSEwHjTOt4xb0XB-+ z&4RO{Snw4G%gS9w#uSUK$Zbb#=jxEl;}6&!b-rSY$0M4pftat-$Q)*y!bpx)R%P>8 zrB&`YEX2%+s#lFCIV;cUFUTIR$Gn2%F(3yLeiG8eG8&)+cpBlzx4)sK?>uIlH+$?2 z9q9wk5zY-xr_fzFSGxYp^KSY0s%1BhsI>ai2VAc8&JiwQ>3RRk?ITx!t~r45qsMnj zkX4bl06ojFCMq<9l*4NHMAtIxDJOX)H=K*$NkkNG<^nl46 zHWH1GXb?Og1f0S+8-((5yaeegCT62&4N*pNQY;%asz9r9Lfr;@Bl${1@a4QAvMLbV6JDp>8SO^q1)#(o%k!QiRSd0eTmzC< zNIFWY5?)+JTl1Roi=nS4%@5iF+%XztpR^BSuM~DX9q`;Mv=+$M+GgE$_>o+~$#?*y zAcD4nd~L~EsAjXV-+li6Lua4;(EFdi|M2qV53`^4|7gR8AJI;0Xb6QGLaYl1zr&eu zH_vFUt+Ouf4SXA~ z&Hh8K@ms^`(hJfdicecj>J^Aqd00^ccqN!-f-!=N7C1?`4J+`_f^nV!B3Q^|fuU)7 z1NDNT04hd4QqE+qBP+>ZE7{v;n3OGN`->|lHjNL5w40pePJ?^Y6bFk@^k%^5CXZ<+4qbOplxpe)l7c6m%o-l1oWmCx%c6@rx85hi(F=v(2 zJ$jN>?yPgU#DnbDXPkHLeQwED5)W5sH#-eS z%#^4dxiVs{+q(Yd^ShMN3GH)!h!@W&N`$L!SbElXCuvnqh{U7lcCvHI#{ZjwnKvu~ zAeo7Pqot+Ohm{8|RJsTr3J4GjCy5UTo_u_~p)MS&Z5UrUc|+;Mc(YS+ju|m3Y_Dvt zonVtpBWlM718YwaN3a3wUNqX;7TqvAFnVUoD5v5WTh~}r)KoLUDw%8Rrqso~bJqd> z_T!&Rmr6ebpV^4|knJZ%qmzL;OvG3~A*loGY7?YS%hS{2R0%NQ@fRoEK52Aiu%gj( z_7~a}eQUh8PnyI^J!>pxB(x7FeINHHC4zLDT`&C*XUpp@s0_B^!k5Uu)^j_uuu^T> z8WW!QK0SgwFHTA%M!L`bl3hHjPp)|wL5Var_*A1-H8LV?uY5&ou{hRjj>#X@rxV>5%-9hbP+v?$4}3EfoRH;l_wSiz{&1<+`Y5%o%q~4rdpRF0jOsCoLnWY5x?V)0ga>CDo`NpqS) z@x`mh1QGkx;f)p-n^*g5M^zRTHz%b2IkLBY{F+HsjrFC9_H(=9Z5W&Eymh~A_FUJ} znhTc9KG((OnjFO=+q>JQZJbeOoUM77M{)$)qQMcxK9f;=L;IOv_J>*~w^YOW744QZ zoG;!b9VD3ww}OX<8sZ0F##8hvfDP{hpa3HjaLsKbLJ8 z0WpY2E!w?&cWi7&N%bOMZD~o7QT*$xCRJ@{t31~qx~+0yYrLXubXh2{_L699Nl_pn z6)9eu+uUTUdjHXYs#pX^L)AIb!FjjNsTp7C399w&B{Q4q%yKfmy}T2uQdU|1EpNcY zDk~(h#AdxybjfzB+mg6rdU9mDZ^V>|U13Dl$Gj+pAL}lR2a1u!SJXU_YqP9N{ose4 zk+$v}BIHX60WSGVWv;S%zvHOWdDP(-ceo(<8`y@Goy%4wDu>57QZNJc)f>Ls+}9h7 z^N=#3q3|l?aG8K#HwiW2^PJu{v|x5;awYfahC?>_af3$LmMc4%N~JwVlRZa4c+eW2 zE!zosAjOv&UeCeu;Bn5OQUC=jtZjF;NDk9$fGbxf3d29SUBekX1!a$Vmq_VK*MHQ4)eB!dQrHH)LVYNF%-t8!d`@!cb z2CsKs3|!}T^7fSZm?0dJ^JE`ZGxA&a!jC<>6_y67On0M)hd$m*RAzo_qM?aeqkm`* zXpDYcc_>TFZYaC3JV>{>mp(5H^efu!Waa7hGTAts29jjuVd1vI*fEeB?A&uG<8dLZ z(j6;-%vJ7R0U9}XkH)1g>&uptXPHBEA*7PSO2TZ+dbhVxspNW~ZQT3fApz}2 z_@0-lZODcd>dLrYp!mHn4k>>7kibI!Em+Vh*;z}l?0qro=aJt68joCr5Jo(Vk<@i) z5BCKb4p6Gdr9=JSf(2Mgr=_6}%4?SwhV+JZj3Ox^_^OrQk$B^v?eNz}d^xRaz&~ zKVnlLnK#8^y=If2f1zmb~^5lPLe?%l}>?~wN4IN((2~U{e9fKhLMtYFj)I$(y zgnKv?R+ZpxA$f)Q2l=aqE6EPTK=i0sY&MDFJp!vQayyvzh4wee<}kybNthRlX>SHh z7S}9he^EBOqzBCww^duHu!u+dnf9veG{HjW!}aT7aJqzze9K6-Z~8pZAgdm1n~aDs z8_s7?WXMPJ3EPJHi}NL&d;lZP8hDhAXf5Hd!x|^kEHu`6QukXrVdLnq5zbI~oPo?7 z2Cbu8U?$K!Z4_yNM1a(bL!GRe!@{Qom+DxjrJ!B99qu5b*Ma%^&-=6UEbC+S2zX&= zQ!%bgJTvmv^2}hhvNQg!l=kbapAgM^hruE3k@jTxsG(B6d=4thBC*4tzVpCYXFc$a zeqgVB^zua)y-YjpiibCCdU%txXYeNFnXcbNj*D?~)5AGjL+!!ij_4{5EWKGav0^={~M^q}baAFOPzxfUM>`KPf|G z&hsaR*7(M6KzTj8Z?;45zX@L#xU{4n$9Q_<-ac(y4g~S|Hyp^-<*d8+P4NHe?~vfm z@y309=`lGdvN8*jw-CL<;o#DKc-%lb0i9a3%{v&2X($|Qxv(_*()&=xD=5oBg=$B0 zU?41h9)JKvP0yR{KsHoC>&`(Uz>?_`tlLjw1&5tPH3FoB%}j;yffm$$s$C=RHi`I3*m@%CPqWnP@B~%DEe;7ZT{9!IMTo1hT3Q347HJ&!)BM2 z3~aClf>aFh0_9||4G}(Npu`9xYY1*SD|M~9!CCFn{-J$u2&Dg*=5$_nozpoD2nxqq zB!--eA8UWZlcEDp4r#vhZ6|vq^9sFvRnA9HpHch5Mq4*T)oGbruj!U8Lx_G%Lby}o zTQ-_4A7b)5A42vA0U}hUJq6&wQ0J%$`w#ph!EGmW96)@{AUx>q6E>-r^Emk!iCR+X zdIaNH`$}7%57D1FyTccs3}Aq0<0Ei{`=S7*>pyg=Kv3nrqblqZcpsCWSQl^uMSsdj zYzh73?6th$c~CI0>%5@!Ej`o)Xm38u0fp9=HE@Sa6l2oX9^^4|Aq%GA z3(AbFR9gA_2T2i%Ck5V2Q2WW-(a&(j#@l6wE4Z`xg#S za#-UWUpU2U!TmIo`CN0JwG^>{+V#9;zvx;ztc$}@NlcyJr?q(Y`UdW6qhq!aWyB5xV1#Jb{I-ghFNO0 zFU~+QgPs{FY1AbiU&S$QSix>*rqYVma<-~s%ALhFyVhAYepId1 zs!gOB&weC18yhE-v6ltKZMV|>JwTX+X)Y_EI(Ff^3$WTD|Ea-1HlP;6L~&40Q&5{0 z$e$2KhUgH8ucMJxJV#M%cs!d~#hR^nRwk|uuCSf6irJCkSyI<%CR==tftx6d%;?ef zYIcjZrP@APzbtOeUe>m-TW}c-ugh+U*RbL1eIY{?>@8aW9bb1NGRy@MTse@>= za%;5=U}X%K2tKTYe9gjMcBvX%qrC&uZ`d(t)g)X8snf?vBe3H%dG=bl^rv8Z@YN$gd9yveHY0@Wt0$s zh^7jCp(q+6XDoekb;=%y=Wr8%6;z0ANH5dDR_VudDG|&_lYykJaiR+(y{zpR=qL3|2e${8 z2V;?jgHj7}Kl(d8C9xWRjhpf_)KOXl+@c4wrHy zL3#9U(`=N59og2KqVh>nK~g9>fX*PI0`>i;;b6KF|8zg+k2hViCt}4dfMdvb1NJ-Rfa7vL2;lPK{Lq*u`JT>S zoM_bZ_?UY6oV6Ja14X^;LqJPl+w?vf*C!nGK;uU^0GRN|UeFF@;H(Hgp8x^|;ygh? zIZx3DuO(lD01ksanR@Mn#lti=p28RTNYY6yK={RMFiVd~k8!@a&^jicZ&rxD3CCI! zVb=fI?;c#f{K4Pp2lnb8iF2mig)|6JEmU86Y%l}m>(VnI*Bj`a6qk8QL&~PFDxI8b z2mcsQBe9$q`Q$LfG2wdvK`M1}7?SwLAV&)nO;kAk`SAz%x9CDVHVbUd$O(*aI@D|s zLxJW7W(QeGpQY<$dSD6U$ja(;Hb3{Zx@)*fIQaW{8<$KJ&fS0caI2Py^clOq9@Irt z7th7F?7W`j{&UmM==Lo~T&^R7A?G=K_e-zfTX|)i`pLitlNE(~tq*}sS1x2}Jlul6 z5+r#4SpQu8h{ntIv#qCVH`uG~+I8l+7ZG&d`Dm!+(rZQDV*1LS^WfH%-!5aTAxry~ z4xl&rot5ct{xQ$w$MtVTUi6tBFSJWq2Rj@?HAX1H$eL*fk{Hq;E`x|hghRkipYNyt zKCO=*KSziiVk|+)qQCGrTYH9X!Z0$k{Nde~0Wl`P{}ca%nv<6fnYw^~9dYxTnTZB&&962jX0DM&wy&8fdxX8xeHSe=UU&Mq zRTaUKnQO|A>E#|PUo+F=Q@dMdt`P*6e92za(TH{5C*2I2S~p?~O@hYiT>1(n^Lqqn zqewq3ctAA%0E)r53*P-a8Ak32mGtUG`L^WVcm`QovX`ecB4E9X60wrA(6NZ7z~*_DV_e z8$I*eZ8m=WtChE{#QzeyHpZ%7GwFHlwo2*tAuloI-j2exx3#x7EL^&D;Re|Kj-XT- zt908^soV2`7s+Hha!d^#J+B)0-`{qIF_x=B811SZlbUe%kvPce^xu7?LY|C z@f1gRPha1jq|=f}Se)}v-7MWH9)YAs*FJ&v3ZT9TSi?e#jarin0tjPNmxZNU_JFJG z+tZi!q)JP|4pQ)?l8$hRaPeoKf!3>MM-bp06RodLa*wD=g3)@pYJ^*YrwSIO!SaZo zDTb!G9d!hb%Y0QdYxqNSCT5o0I!GDD$Z@N!8J3eI@@0AiJmD7brkvF!pJGg_AiJ1I zO^^cKe`w$DsO|1#^_|`6XTfw6E3SJ(agG*G9qj?JiqFSL|6tSD6vUwK?Cwr~gg)Do zp@$D~7~66-=p4`!!UzJDKAymb!!R(}%O?Uel|rMH>OpRGINALtg%gpg`=}M^Q#V5( zMgJY&gF)+;`e38QHI*c%B}m94o&tOfae;og&!J2;6ENW}QeL73jatbI1*9X~y=$Dm%6FwDcnCyMRL}zo`0=y7=}*Uw zo3!qZncAL{HCgY!+}eKr{P8o27ye+;qJP;kOB%RpSesGoHLT6tcYp*6v~Z9NCyb6m zP#qds0jyqXX46qMNhXDn3pyIxw2f_z;L_X9EIB}AhyC`FYI}G3$WnW>#NMy{0aw}nB%1=Z4&*(FaCn5QG(zvdG^pQRU25;{wwG4h z@kuLO0F->{@g2!;NNd!PfqM-;@F0;&wK}0fT9UrH}(8A5I zt33(+&U;CLN|8+71@g z(s!f-kZZZILUG$QXm9iYiE*>2w;gpM>lgM{R9vT3q>qI{ELO2hJHVi`)*jzOk$r)9 zq}$VrE0$GUCm6A3H5J-=Z9i*biw8ng zi<1nM0lo^KqRY@Asucc#DMmWsnCS;5uPR)GL3pL=-IqSd>4&D&NKSGHH?pG;=Xo`w zw~VV9ddkwbp~m>9G0*b?j7-0fOwR?*U#BE#n7A=_fDS>`fwatxQ+`FzhBGQUAyIRZ??eJt46vHBlR>9m!vfb6I)8!v6TmtZ%G6&E|1e zOtx5xy%yOSu+<9Ul5w5N=&~4Oph?I=ZKLX5DXO(*&Po>5KjbY7s@tp$8(fO|`Xy}Y z;NmMypLoG7r#Xz4aHz7n)MYZ7Z1v;DFHLNV{)to;(;TJ=bbMgud96xRMME#0d$z-S z-r1ROBbW^&YdQWA>U|Y>{whex#~K!ZgEEk=LYG8Wqo28NFv)!t!~}quaAt}I^y-m| z8~E{9H2VnyVxb_wCZ7v%y(B@VrM6lzk~|ywCi3HeiSV`TF>j+Ijd|p*kyn;=mqtf8&DK^|*f+y$38+9!sis9N=S)nINm9=CJ<;Y z!t&C>MIeyou4XLM*ywT_JuOXR>VkpFwuT9j5>667A=CU*{TBrMTgb4HuW&!%Yt`;#md7-`R`ouOi$rEd!ErI zo#>qggAcx?C7`rQ2;)~PYCw%CkS(@EJHZ|!!lhi@Dp$*n^mgrrImsS~(ioGak>3)w zvop0lq@IISuA0Ou*#1JkG{U>xSQV1e}c)!d$L1plFX5XDXX5N7Ns{kT{y5|6MfhBD+esT)e7&CgSW8FxsXTAY=}?0A!j_V9 zJ;IJ~d%av<@=fNPJ9)T3qE78kaz64E>dJaYab5uaU`n~Zdp2h{8DV%SKE5G^$LfuOTRRjB;TnT(Jk$r{Pfe4CO!SM_7d)I zquW~FVCpSycJ~c*B*V8?Qqo=GwU8CkmmLFugfHQ7;A{yCy1OL-+X=twLYg9|H=~8H znnN@|tCs^ZLlCBl5wHvYF}2vo>a6%mUWpTds_mt*@wMN4-r`%NTA%+$(`m6{MNpi@ zMx)8f>U4hd!row@gM&PVo&Hx+lV@$j9yWTjTue zG9n0DP<*HUmJ7ZZWwI2x+{t3QEfr6?T}2iXl=6e0b~)J>X3`!fXd9+2wc1%cj&F@Z zgYR|r5Xd5jy9;YW&=4{-0rJ*L5CgDPj9^3%bp-`HkyBs`j1iTUGD4?WilZ6RO8mIE z+~Joc?GID6K96dyuv(dWREK9Os~%?$$FxswxQsoOi8M?RnL%B~Lyk&(-09D0M?^Jy zWjP)n(b)TF<-|CG%!Vz?8Fu&6iU<>oG#kGcrcrrBlfZMVl0wOJvsq%RL9To%iCW@)#& zZAJWhgzYAq)#NTNb~3GBcD%ZZOc43!YWSyA7TD6xkk)n^FaRAz73b}%9d&YisBic(?mv=Iq^r%Ug zzHq-rRrhfOOF+yR=AN!a9*Rd#sM9ONt5h~w)yMP7Dl9lfpi$H0%GPW^lS4~~?vI8Z z%^ToK#NOe0ExmUsb`lLO$W*}yXNOxPe@zD*90uTDULnH6C?InP3J=jYEO2d)&e|mP z1DSd0QOZeuLWo*NqZzopA+LXy9)fJC00NSX=_4Mi1Z)YyZVC>C!g}cY(Amaj%QN+bev|Xxd2OPD zk!dfkY6k!(sDBvsFC2r^?}hb81(WG5Lt9|riT`2?P;B%jaf5UX<~OJ;uAL$=Ien+V zC!V8u0v?CUa)4*Q+Q_u zkx{q;NjLcvyMuU*{+uDsCQ4U{JLowYby-tn@hatL zy}X>9y08#}oytdn^qfFesF)Tt(2!XGw#r%?7&zzFFh2U;#U9XBO8W--#gOpfbJ`Ey z|M8FCKlWQrOJwE;@Sm02l9OBr7N}go4V8ur)}M@m2uWjggb)DC4s`I4d7_8O&E(j; z?3$9~R$QDxNM^rNh9Y;6P7w+bo2q}NEd6f&_raor-v`UCaTM3TT8HK2-$|n{N@U>_ zL-`P7EXoEU5JRMa)?tNUEe8XFis+w8g9k(QQ)%?&Oac}S`2V$b?%`DwXBgja&&fR@ zH_XidF$p1wA)J|Wk1;?lCl?fgc)=TB3>Y8;BoMqHwJqhL)Tgydv9(?(TBX)fq%=~C zmLj!iX-kn7QA(9snzk0LRf<%SzO&~IhLor6A3f*U^UcoAygRe!H#@UCv$JUP&vPxs zeDj$1%#<2T1!e|!7xI+~_VXLl5|jHqvOhU7ZDUGee;HnkcPP=_k_FFxPjXg*9KyI+ zIh0@+s)1JDSuKMeaDZ3|<_*J8{TUFDLl|mXmY8B>Wj_?4mC#=XjsCKPEO=p0c&t&Z zd1%kHxR#o9S*C?du*}tEHfAC7WetnvS}`<%j=o7YVna)6pw(xzkUi7f#$|^y4WQ{7 zu@@lu=j6xr*11VEIY+`B{tgd(c3zO8%nGk0U^%ec6h)G_`ki|XQXr!?NsQkxzV6Bn1ea9L+@ z(Zr7CU_oXaW>VOdfzENm+FlFQ7Se0ROrNdw(QLvb6{f}HRQ{$Je>(c&rws#{dFI^r zZ4^(`J*G0~Pu_+p5AAh>RRpkcbaS2a?Fe&JqxDTp`dIW9;DL%0wxX5;`KxyA4F{(~_`93>NF@bj4LF!NC&D6Zm+Di$Q-tb2*Q z&csGmXyqA%Z9s(AxNO3@Ij=WGt=UG6J7F;r*uqdQa z?7j!nV{8eQE-cwY7L(3AEXF3&V*9{DpSYdyCjRhv#&2johwf{r+k`QB81%!aRVN<& z@b*N^xiw_lU>H~@4MWzgHxSOGVfnD|iC7=hf0%CPm_@@4^t-nj#GHMug&S|FJtr?i z^JVrobltd(-?Ll>)6>jwgX=dUy+^n_ifzM>3)an3iOzpG9Tu;+96TP<0Jm_PIqof3 zMn=~M!#Ky{CTN_2f7Y-i#|gW~32RCWKA4-J9sS&>kYpTOx#xVNLCo)A$LUme^fVNH z@^S7VU^UJ0YR8?Oy$^IYuG*bm|g;@aX~i60%`7XLy*AYpYvZ^F^U(!|RW z*C!rJ@+7TGdL=nNd1gv^%B+;Fcr$y)i0!GRsZXRHPs>QVGVR{9r_#&Qd(wL|5;H;> zD>HUw=4CF++&{7$<8G@j*nGjhEO%BQYfjeItp4mPvY*JYb1HKd!{HJ9*)(3%BR%{Pp?AM&*yHAJsW({ivOzj*qS!-7|XEn6@zo z3L*tBT%<4RxoAh>q{0n_JBmgW6&8hx?kL(_^k%VL>?xjAyrKBmSl`$=V|SK}ELl}@ zd|d0eo#RfG`bw9SK3%r4Y+rdvc}w}~ixV%tqawbdqvE-WcgE+BUpxMT%F@btm76MG zn=oQRWWuTm+a{dy)Oc2V4yX(@M{QAkx>(QB59*`dLT`Pz3Lsj9iB=HSHAiCq()ns|Cr)1*c605Cx}3V&x}Lg?b+6Q?)z7Kl zQh&1Hx`y6JY-Cwvd*ozeps}a1xAA0CR+Da;+O(i)P1C;SjOI}Dtmf6tPqo-Bl`U78 zv$kYgPntPp@G)n1an9tEoL*Vumu9`>_@I(;+5+fBa-*?fEx=mTEjZ7wq}#@Gd5_cW z!mP{N=yqEntDo)|>oy6{9cu+-3*GTnmb^`O0^FzRPO^&aG`f@F_R*aQ_e{F+_9%NW z4KG_B`@X3EVV9L>?_RNDMddA>w=e0KfAiw5?#i1NFT%Zz#nuv(&!yIU>lVxmzYKQ` zzJ*0w9<&L4aJ6A;0j|_~i>+y(q-=;2Xxhx2v%CYY^{} z^J@LO()eLo|7!{ghQ+(u$wxO*xY#)cL(|miH2_ck2yN{mu4O9=hBW*pM_()-_YdH#Ru{JtwJ^R2}3?!>>m1pohh zrn(!xCjE0Q&EH1QK?zA%sxVh&H99cObJUY$veZhQ)MLu-h%`!*G)s$2k;~+A z)Kk->Ri?`oGDEJEtI*wijm(s5f$W78FH{+qBxiU{~kq((J3uK{m z$|C8K#j-?hm8H@x%VfFqpnvu@xn1s%J7uNZC9C99a<_b1J|mx%)$%!6gPU|~<@2&m zz99GDp`|a%m*iggvfL;4%X;~WY>)@!tMWB@P`)k?$;0x9JSrRI8?s3rlgH(o@`OAo zn{f*gZ#t2u6K??hx|aElOM`Xd0t+SAIUEHvFw%?Wsm$s zUXq{6UU?a>Nc@@Xlb_2k9M1Ctr<#+O?yd}rv z_wu&=_t$!Yngd@N_AUj}T; z#*Ce|%XZr_sQcsWcsl{pCnnj+c8ZNIMmx<;w=-g$Q>BU;9k;w|zQ;4!W32Xg2Cd?{ zvmO3kuKQ^Hv;o>6ZHP8ZJ2`4~Bx?N;cf<0fi=!*G^^WzbTF3e$b&d^qqB{>nqLG81 zs94bBh%|Vj+hLu=!8(b9brJ>ZBns9^6s(gdSVyP9qnu2_I{Sg8j-rloG6{d`De5We zDe5WeY3ga}Y3ga}Y3ga}Y3ga}Y3ga}d8y~6o|k%F>UpW>rJk31Ug~+N=cS&HdOqs; zsOO`ek9t1p`Kafko{xGy>iMbXr=FjBxZMYc8a#gL`Kjlpo}YSt>iMY`pk9DF0qO*( z6QE9jIsxhgs1u-0kUBx8D@eT{^@7w3QZGooAoYUO3sNscy%6<6)C*BBM7L`dk$Xk%6}eZQXgo#!75P`>Uy*-B{uTLGUy*-B{uTLGUy*-B{uTLG))v8{5gt_uj9!t5)^yb-JtjRGrhi zYInOUNJxNyf_yKX01)K=WP|Si>HqEj|B{eUl?MR<)%<1&{(~)D+NPwKxWqT-@~snp zg9KCz1VTZDiS?UH`PRk1VPM{29cgT9=D?!Wc_@}qzggFv;gb@2cJQAYWWtpEZ7?y@jSVqjx${B5UV@SO|wH<<0; z{><1KdVI%Ki}>~<`46C0AggwUwx-|QcU;iiZ{NZu`ur>hd*|Hb(|6veERqxu=b@5Bab=rqptGxd{QJg!4*-i_$sES~)AB46}Fjg|ea#e@?J}z%CUJ zOsLWRQR1#ng^sD)A4FDuY!iUhzlgfJh(J@BRqd&P#v2B`+saBx>m+M&q7vk-75$NH%T5pi%m z5FX?`2-5l53=a&GkC9^NZCLpN5(DMKMwwab$FDIs?q>4!!xBS}75gX_5;(luk;3Vl zLCLd5a_8`Iyz}K}+#RMwu6DVk3O_-}n>aE!4NaD*sQn`GxY?cHe!Bl9n?u&g6?aKm z-P8z&;Q3gr;h`YIxX%z^o&GZZg1=>_+hP2$$-DnL_?7?3^!WAsY4I7|@K;aL<>OTK zByfjl2PA$T83*LM9(;espx-qB%wv7H2i6CFsfAg<9V>Pj*OpwX)l?^mQfr$*OPPS$ z=`mzTYs{*(UW^ij1U8UfXjNoY7GK*+YHht(2oKE&tfZuvAyoN(;_OF>-J6AMmS5fB z^sY6wea&&${+!}@R1f$5oC-2J>J-A${@r(dRzc`wnK>a7~8{Y-scc|ETOI8 zjtNY%Y2!PI;8-@a=O}+{ap1Ewk0@T`C`q!|=KceX9gK8wtOtIC96}-^7)v23Mu;MH zhKyLGOQMujfRG$p(s`(2*nP4EH7*J57^=|%t(#PwCcW7U%e=8Jb>p6~>RAlY4a*ts=pl}_J{->@kKzxH|8XQ5{t=E zV&o`$D#ZHdv&iZWFa)(~oBh-Osl{~CS0hfM7?PyWUWsr5oYlsyC1cwULoQ4|Y5RHA2*rN+EnFPnu z`Y_&Yz*#550YJwDy@brZU>0pWV^RxRjL221@2ABq)AtA%Cz?+FG(}Yh?^v)1Lnh%D zeM{{3&-4#F9rZhS@DT0E(WRkrG!jC#5?OFjZv*xQjUP~XsaxL2rqRKvPW$zHqHr8Urp2Z)L z+)EvQeoeJ8c6A#Iy9>3lxiH3=@86uiTbnnJJJoypZ7gco_*HvKOH97B? zWiwp>+r}*Zf9b3ImxwvjL~h~j<<3shN8$k-$V1p|96I!=N6VBqmb==Bec|*;HUg?) z4!5#R*(#Fe)w%+RH#y{8&%%!|fQ5JcFzUE;-yVYR^&Ek55AXb{^w|@j|&G z|6C-+*On%j;W|f8mj?;679?!qY86c{(s1-PI2Wahoclf%1*8%JAvRh1(0)5Vu37Iz z`JY?RW@qKr+FMmBC{TC7k@}fv-k8t6iO}4K-i3WkF!Lc=D`nuD)v#Na zA|R*no51fkUN3^rmI;tty#IK284*2Zu!kG13!$OlxJAt@zLU`kvsazO25TpJLbK&;M8kw*0)*14kpf*)3;GiDh;C(F}$- z1;!=OBkW#ctacN=je*Pr)lnGzX=OwgNZjTpVbFxqb;8kTc@X&L2XR0A7oc!Mf2?u9 zcctQLCCr+tYipa_k=;1ETIpHt!Jeo;iy^xqBES^Ct6-+wHi%2g&)?7N^Yy zUrMIu){Jk)luDa@7We5U!$$3XFNbyRT!YPIbMKj5$IEpTX1IOtVP~(UPO2-+9ZFi6 z-$3<|{Xb#@tABt0M0s1TVCWKwveDy^S!!@4$s|DAqhsEv--Z}Dl)t%0G>U#ycJ7cy z^8%;|pg32=7~MJmqlC-x07Sd!2YX^|2D`?y;-$a!rZ3R5ia{v1QI_^>gi(HSS_e%2 zUbdg^zjMBBiLr8eSI^BqXM6HKKg#@-w`a**w(}RMe%XWl3MipvBODo*hi?+ykYq)z ziqy4goZw0@VIUY65+L7DaM5q=KWFd$;W3S!Zi>sOzpEF#(*3V-27N;^pDRoMh~(ZD zJLZXIam0lM7U#)119Hm947W)p3$%V`0Tv+*n=&ybF&}h~FA}7hEpA&1Y!BiYIb~~D z$TSo9#3ee02e^%*@4|*+=Nq6&JG5>zX4k5f?)z*#pI-G(+j|jye%13CUdcSP;rNlY z#Q!X%zHf|V)GWIcEz-=fW6AahfxI~y7w7i|PK6H@@twdgH>D_R@>&OtKl}%MuAQ7I zcpFmV^~w~8$4@zzh~P~+?B~%L@EM3x(^KXJSgc6I=;)B6 zpRco2LKIlURPE*XUmZ^|1vb?w*ZfF}EXvY13I4af+()bAI5V?BRbFp`Sb{8GRJHd* z4S2s%4A)6Uc=PK%4@PbJ<{1R6+2THMk0c+kif**#ZGE)w6WsqH z`r^DL&r8|OEAumm^qyrryd(HQ9olv$ltnVGB{aY?_76Uk%6p;e)2DTvF(;t=Q+|8b zqfT(u5@BP);6;jmRAEV057E*2d^wx@*aL1GqWU|$6h5%O@cQtVtC^isd%gD7PZ_Io z_BDP5w(2*)Mu&JxS@X%%ByH_@+l>y07jIc~!@;Raw)q_;9oy@*U#mCnc7%t85qa4? z%_Vr5tkN^}(^>`EFhag;!MpRh!&bKnveQZAJ4)gEJo1@wHtT$Gs6IpznN$Lk-$NcM z3ReVC&qcXvfGX$I0nfkS$a|Pm%x+lq{WweNc;K>a1M@EAVWs2IBcQPiEJNt}+Ea8~WiapASoMvo(&PdUO}AfC~>ZGzqWjd)4no( ziLi#e3lOU~sI*XPH&n&J0cWfoh*}eWEEZW%vX?YK!$?w}htY|GALx3;YZoo=JCF4@ zdiaA-uq!*L5;Yg)z-_`MciiIwDAAR3-snC4V+KA>&V%Ak;p{1u>{Lw$NFj)Yn0Ms2*kxUZ)OTddbiJM}PK!DM}Ot zczn?EZXhx3wyu6i{QMz_Ht%b?K&-@5r;8b076YDir`KXF0&2i9NQ~#JYaq*}Ylb}^ z<{{6xy&;dQ;|@k_(31PDr!}}W$zF7Jv@f%um0M$#=8ygpu%j(VU-d5JtQwT714#f0z+Cm$F9JjGr_G!~NS@L9P;C1? z;Ij2YVYuv}tzU+HugU=f9b1Wbx3418+xj$RKD;$gf$0j_A&c;-OhoF*z@DhEW@d9o zbQBjqEQnn2aG?N9{bmD^A#Um6SDKsm0g{g_<4^dJjg_l_HXdDMk!p`oFv8+@_v_9> zq;#WkQ!GNGfLT7f8m60H@$tu?p;o_It#TApmE`xnZr|_|cb3XXE)N^buLE`9R=Qbg zXJu}6r07me2HU<)S7m?@GzrQDTE3UH?FXM7V+-lT#l}P(U>Fvnyw8T7RTeP`R579m zj=Y>qDw1h-;|mX-)cSXCc$?hr;43LQt)7z$1QG^pyclQ1Bd!jbzsVEgIg~u9b38;> zfsRa%U`l%did6HzPRd;TK{_EW;n^Ivp-%pu0%9G-z@Au{Ry+EqEcqW=z-#6;-!{WA z;l+xC6Zke>dl+(R1q7B^Hu~HmrG~Kt575mzve>x*cL-shl+zqp6yuGX)DDGm`cid! znlnZY=+a5*xQ=$qM}5$N+o!^(TqTFHDdyCcL8NM4VY@2gnNXF|D?5a558Lb*Yfm4) z_;0%2EF7k{)i(tTvS`l5he^KvW%l&-suPwpIlWB_Za1Hfa$@J!emrcyPpTKKM@NqL z?X_SqHt#DucWm<3Lp}W|&YyQE27zbGP55=HtZmB(k*WZA79f##?TweCt{%5yuc+Kx zgfSrIZI*Y57FOD9l@H0nzqOu|Bhrm&^m_RK6^Z<^N($=DDxyyPLA z+J)E(gs9AfaO`5qk$IGGY+_*tEk0n_wrM}n4G#So>8Dw6#K7tx@g;U`8hN_R;^Uw9JLRUgOQ?PTMr4YD5H7=ryv)bPtl=<&4&% z*w6k|D-%Tg*F~sh0Ns(h&mOQ_Qf{`#_XU44(VDY8b})RFpLykg10uxUztD>gswTH} z&&xgt>zc(+=GdM2gIQ%3V4AGxPFW0*l0YsbA|nFZpN~ih4u-P!{39d@_MN)DC%d1w z7>SaUs-g@Hp7xqZ3Tn)e z7x^sC`xJ{V<3YrmbB{h9i5rdancCEyL=9ZOJXoVHo@$$-%ZaNm-75Z-Ry9Z%!^+STWyv~To>{^T&MW0-;$3yc9L2mhq z;ZbQ5LGNM+aN628)Cs16>p55^T^*8$Dw&ss_~4G5Go63gW^CY+0+Z07f2WB4Dh0^q z-|6QgV8__5>~&z1gq0FxDWr`OzmR}3aJmCA^d_eufde7;d|OCrKdnaM>4(M%4V`PxpCJc~UhEuddx9)@)9qe_|i z)0EA%&P@_&9&o#9eqZCUCbh?`j!zgih5sJ%c4(7_#|Xt#r7MVL&Q+^PQEg3MBW;4T zG^4-*8L%s|A}R%*eGdx&i}B1He(mLygTmIAc^G(9Si zK7e{Ngoq>r-r-zhyygK)*9cj8_%g z)`>ANlipCdzw(raeqP-+ldhyUv_VOht+!w*>Sh+Z7(7(l=9~_Vk ztsM|g1xW`?)?|@m2jyAgC_IB`Mtz(O`mwgP15`lPb2V+VihV#29>y=H6ujE#rdnK` zH`EaHzABs~teIrh`ScxMz}FC**_Ii?^EbL(n90b(F0r0PMQ70UkL}tv;*4~bKCiYm zqngRuGy`^c_*M6{*_~%7FmOMquOEZXAg1^kM`)0ZrFqgC>C%RJvQSo_OAA(WF3{euE}GaeA?tu5kF@#62mM$a051I zNhE>u>!gFE8g#Jj95BqHQS%|>DOj71MZ?EYfM+MiJcX?>*}vKfGaBfQFZ3f^Q-R1# znhyK1*RvO@nHb|^i4Ep_0s{lZwCNa;Ix<{E5cUReguJf+72QRZIc%`9-Vy)D zWKhb?FbluyDTgT^naN%l2|rm}oO6D0=3kfXO2L{tqj(kDqjbl(pYz9DykeZlk4iW5 zER`)vqJxx(NOa;so@buE!389-YLbEi@6rZG0#GBsC+Z0fzT6+d7deYVU;dy!rPXiE zmu73@Jr&~K{-9MVQD}&`)e>yLNWr>Yh8CXae9XqfvVQ&eC_;#zpoaMxZ0GpZz7xjx z`t_Q-F?u=vrRPaj3r<9&t6K=+egimiJ8D4gh-rUYvaVy zG($v+3zk5sMuOhjxkH7bQ}(5{PD3Mg?!@8PkK&w>n7tO8FmAmoF30_#^B~c(Q_`4L zYWOoDVSnK|1=p{+@`Fk^Qb81Xf89_S`RSTzv(a4ID%71nll%{Wad$!CKfeTKkyC?n zCkMKHU#*nz_(tO$M)UP&ZfJ#*q(0Gr!E(l5(ce<3xut+_i8XrK8?Xr7_oeHz(bZ?~8q5q~$Rah{5@@7SMN zx9PnJ-5?^xeW2m?yC_7A#WK*B@oIy*Y@iC1n7lYKj&m7vV;KP4TVll=II)$39dOJ^czLRU>L> z68P*PFMN+WXxdAu=Hyt3g$l(GTeTVOZYw3KY|W0Fk-$S_`@9`K=60)bEy?Z%tT+Iq z7f>%M9P)FGg3EY$ood+v$pdsXvG? zd2q3abeu-}LfAQWY@=*+#`CX8RChoA`=1!hS1x5dOF)rGjX4KFg!iPHZE2E=rv|A} zro(8h38LLFljl^>?nJkc+wdY&MOOlVa@6>vBki#gKhNVv+%Add{g6#-@Z$k*ps}0Y zQ=8$)+Nm||)mVz^aa4b-Vpg=1daRaOU)8@BY4jS>=5n#6abG@(F2`=k-eQ9@u# zxfNFHv=z2w@{p1dzSOgHokX1AUGT0DY4jQI@YMw)EWQ~q5wmR$KQ}Y;(HPMSQCwzu zdli|G?bj(>++CP)yQ4s6YfpDc3KqPmquQSxg%*EnTWumWugbDW5ef%8j-rT#3rJu? z)5n;4b2c*;2LIW%LmvUu6t1~di~}0&Svy}QX#ER|hDFZwl!~zUP&}B1oKAxIzt~so zb!GaJYOb#&qRUjEI1xe_`@7qv_-LggQ$JE8+{ryT4%ldwC5ete+{G3C#g@^oxfY3#F zcLlj(l2G8>tC<5XWV|6_DZQZ7ow?MD8EZ9mM2oV~WoV-uoExmbwpzc6eMV}%J_{3l zW(4t2a-o}XRlU|NSiYn!*nR(Sc>*@TuU*(S77gfCi7+WR%2b;4#RiyxWR3(u5BIdf zo@#g4wQjtG3T$PqdX$2z8Zi|QP~I^*9iC+(!;?qkyk&Q7v>DLJGjS44q|%yBz}}>i z&Ve%^6>xY<=Pi9WlwpWB%K10Iz`*#gS^YqMeV9$4qFchMFO}(%y}xs2Hn_E}s4=*3 z+lAeCKtS}9E{l(P=PBI;rsYVG-gw}-_x;KwUefIB@V%RLA&}WU2XCL_?hZHoR<7ED zY}4#P_MmX(_G_lqfp=+iX|!*)RdLCr-1w`4rB_@bI&Uz# z!>9C3&LdoB$r+O#n);WTPi;V52OhNeKfW6_NLnw zpFTuLC^@aPy~ZGUPZr;)=-p|b$-R8htO)JXy{ecE5a|b{{&0O%H2rN&9(VHxmvNly zbY?sVk}@^{aw)%#J}|UW=ucLWs%%j)^n7S%8D1Woi$UT}VuU6@Sd6zc2+t_2IMBxd zb4R#ykMr8s5gKy=v+opw6;4R&&46$V+OOpDZwp3iR0Osqpjx))joB*iX+diVl?E~Q zc|$qmb#T#7Kcal042LUNAoPTPUxF-iGFw>ZFnUqU@y$&s8%h-HGD`EoNBbe#S>Y-4 zlkeAP>62k~-N zHQqXXyN67hGD6CxQIq_zoepU&j0 zYO&}<4cS^2sp!;5))(aAD!KmUED#QGr48DVlwbyft31WlS2yU<1>#VMp?>D1BCFfB z_JJ-kxTB{OLI}5XcPHXUo}x~->VP%of!G_N-(3Snvq`*gX3u0GR&}*fFwHo3-vIw0 zeiWskq3ZT9hTg^je{sC^@+z3FAd}KNhbpE5RO+lsLgv$;1igG7pRwI|;BO7o($2>mS(E z$CO@qYf5i=Zh6-xB=U8@mR7Yjk%OUp;_MMBfe_v1A(Hqk6!D})x%JNl838^ZA13Xu zz}LyD@X2;5o1P61Rc$%jcUnJ>`;6r{h5yrEbnbM$$ntA@P2IS1PyW^RyG0$S2tUlh z8?E(McS?7}X3nAAJs2u_n{^05)*D7 zW{Y>o99!I9&KQdzgtG(k@BT|J*;{Pt*b|?A_})e98pXCbMWbhBZ$t&YbNQOwN^=F) z_yIb_az2Pyya2530n@Y@s>s>n?L79;U-O9oPY$==~f1gXro5Y z*3~JaenSl_I}1*&dpYD?i8s<7w%~sEojqq~iFnaYyLgM#so%_ZZ^WTV0`R*H@{m2+ zja4MX^|#>xS9YQo{@F1I)!%RhM{4ZUapHTKgLZLcn$ehRq(emb8 z9<&Nx*RLcS#)SdTxcURrJhxPM2IBP%I zf1bWu&uRf{60-?Gclb5(IFI*!%tU*7d`i!l@>TaHzYQqH4_Y*6!Wy0d-B#Lz7Rg3l zqKsvXUk9@6iKV6#!bDy5n&j9MYpcKm!vG7z*2&4G*Yl}iccl*@WqKZWQSJCgQSj+d ze&}E1mAs^hP}>`{BJ6lv*>0-ft<;P@`u&VFI~P3qRtufE11+|#Y6|RJccqo27Wzr}Tp|DH z`G4^v)_8}R24X3}=6X&@Uqu;hKEQV^-)VKnBzI*|Iskecw~l?+R|WKO*~(1LrpdJ? z0!JKnCe<|m*WR>m+Qm+NKNH<_yefIml z+x32qzkNRrhR^IhT#yCiYU{3oq196nC3ePkB)f%7X1G^Ibog$ZnYu4(HyHUiFB`6x zo$ty-8pknmO|B9|(5TzoHG|%>s#7)CM(i=M7Nl=@GyDi-*ng6ahK(&-_4h(lyUN-oOa$` zo+P;C4d@m^p9J4c~rbi$rq9nhGxayFjhg+Rqa{l#`Y z!(P6K7fK3T;y!VZhGiC#)|pl$QX?a)a9$(4l(usVSH>2&5pIu5ALn*CqBt)9$yAl; z-{fOmgu><7YJ5k>*0Q~>lq72!XFX6P5Z{vW&zLsraKq5H%Z26}$OKDMv=sim;K?vsoVs(JNbgTU8-M%+ zN(+7Xl}`BDl=KDkUHM9fLlV)gN&PqbyX)$86!Wv!y+r*~kAyjFUKPDWL3A)m$@ir9 zjJ;uQV9#3$*`Dqo1Cy5*;^8DQcid^Td=CivAP+D;gl4b7*xa9IQ-R|lY5tIpiM~9- z%Hm9*vDV@_1FfiR|Kqh_5Ml0sm?abD>@peo(cnhiSWs$uy&$RYcd+m`6%X9FN%?w}s~Q=3!pJzbN~iJ}bbM*PPi@!E0eN zhKcuT=kAsz8TQo76CMO+FW#hr6da({mqpGK2K4T|xv9SNIXZ}a=4_K5pbz1HE6T}9 zbApW~m0C`q)S^F}B9Kw5!eT)Bj_h9vlCX8%VRvMOg8PJ*>PU>%yt-hyGOhjg!2pZR4{ z=VR_*?Hw|aai##~+^H>3p$W@6Zi`o4^iO2Iy=FPdEAI58Ebc~*%1#sh8KzUKOVHs( z<3$LMSCFP|!>fmF^oESZR|c|2JI3|gucuLq4R(||_!8L@gHU8hUQZKn2S#z@EVf3? zTroZd&}JK(mJLe>#x8xL)jfx$6`okcHP?8i%dW?F%nZh=VJ)32CmY;^y5C1^?V0;M z<3!e8GZcPej-h&-Osc>6PU2f4x=XhA*<_K*D6U6R)4xbEx~{3*ldB#N+7QEXD^v=I z+i^L+V7_2ld}O2b-(#bmv*PyZI4|U#Q5|22a(-VLOTZc3!9ns1RI-? zA<~h|tPH0y*bO1#EMrsWN>4yJM7vqFZr?uw$H8*PhiHRQg1U9YoscX-G|gck+SSRX!(e7@~eeUEw+POsT;=W9J&=EV`cUc{PIg_#TQVGnZsQbCs7#Q-)v#BicxLw#Fb?#)8TYbu zN)5R=MI1i7FHhF|X}xEl=sW~`-kf;fOR^h1yjthSw?%#F{HqrY2$q>7!nbw~nZ8q9 zh{vY! z%i=H!!P&wh z7_E%pB7l5)*VU>_O-S~d5Z!+;f{pQ4e86*&);?G<9*Q$JEJ!ZxY;Oj5&@^eg0Zs!iLCAR`2K?MSFzjX;kHD6)^`&=EZOIdW>L#O`J zf~$M4}JiV}v6B-e{NUBGFgj-*H%NG zfY0X(@|S8?V)drF;2OQcpDl2LV=~=%gGx?_$fbSsi@%J~taHcMTLLpjNF8FkjnjyM zW;4sSf6RHaa~LijL#EJ0W2m!BmQP(f=%Km_N@hsBFw%q#7{Er?y1V~UEPEih87B`~ zv$jE%>Ug9&=o+sZVZL7^+sp)PSrS;ZIJac4S-M>#V;T--4FXZ*>CI7w%583<{>tb6 zOZ8gZ#B0jplyTbzto2VOs)s9U%trre`m=RlKf{I_Nwdxn(xNG%zaVNurEYiMV3*g| z``3;{j7`UyfFrjlEbIJN{0db|r>|LA@=vX9CHFZYiexnkn$b%8Rvw0TZOQIXa;oTI zv@j;ZP+#~|!J(aBz9S{wL7W%Dr1H)G-XUNt9-lP?ijJ-XEj1e*CI~-Xz@4(Xg;UoG z{uzBf-U+(SHe}6oG%;A*93Zb=oE>uTb^%qsL>|bQf?7_6=KIiPU`I|r;YcZ!YG7y~ zQu@UldAwz$^|uoz3mz1;An-WVBtefSh-pv<`n&TU3oM!hrEI?l@v8A4#^$4t&~T32 zl*J=1q~h+60sNc43>0aVvhzyfjshgPYZoQ(OOh>LbUIoblb@1z~zp?))n?^)q6WGuDh}gMUaA9|X z3qq-XlcNldy5==T4rq*~g@XVY!9sYZjo#R7 zr{n)r5^S{9+$+8l7IVB*3_k5%-TBY@C%`P@&tZf>82sm#nfw7L%92>nN$663yW!yt zhS>EfLcE_Z)gv-Y^h1;xj(<4nD4GY{C-nWUgQc9cMmH{qpa!uEznrGF^?bbJHApScQ$j>$JZHAX80DdXu z--AMgrA0$Otdd#N9#!cg2Z~N8&lj1d+wDh+^ZObWJ$J)_h(&2#msu>q0B$DEERy{1 zCJN{7M@%#E@8pda`@u!v@{gcT3bA*>g*xYLXlbb&o@1vX*x+l}Voys6o~^_7>#GB| z*r!R%kA9k%J`?m>1tMHB9x$ZRe0$r~ui}X}jOC)9LH=Po*2SLdtf3^4?VKnu2ox&mV~0oDgi` z;9d}P$g~9%ThTK8s}5ow2V4?(-lU*ed8ro|}mU}pk% z;bqB0bx3AOk<0Joeh}Vl@_7Po&C`Cg>>gff>e7fu41U3Ic{JQu1W%+!Gvz3GDO2ixKd;KF6UEw8F_cDAh08gB>@ zaRH2Q96sBJ>`4aXvrF0xPtIWoA1pPsRQtU~xDtnEfTJnl{A9u5pR^K8=UdNq%T8F$)FbN> zgK+_(BF#D>R>kK!M#OT~=@@}3yAYqm33?{Bv?2iBr|-aRK0@uapzuXI)wE0=R@m^7 zQ`wLBn(M*wg!mgmQT1d!@3<2z>~rmDW)KG0*B4>_R6LjiI0^9QT8gtDDT|Lclxppm z+OeL6H3QpearJAB%1ellZ6d*)wBQ(hPbE=%?y6i^uf%`RXm*JW*WQ%>&J+=V(=qf{ zri~yItvTZbII+7S0>4Q0U9@>HnMP$X>8TqAfD(vAh};2P{QK)ik`a6$W$nG<{bR2Ufd!^iE z#1K58$gW!xpeYHeehuhQCXZ9p%N8m zB+l~T_u-Ycr!U>!?xu!!*6rNxq37{`DhMMfY6NpD3Jw zkYQDstvt30Hc_SaZuuMP2YrdW@HsPMbf^Y9lI<9$bnMil2X7`Ba-DGLbzgqP>mxwe zf1&JkDH54D3nLar2KjJ3z`*R+rUABq4;>>4Kjc2iQEj7pVLcZYZ~pteAG4rm1{>PQy=!QiV5G|tVk)53 zP?Azw+N)Yq3zZ`dW7Q9Bq@Y*jSK0<1f`HM;_>GH57pf_S%Ounz_yhTY8lplQSM`xx zU{r-Deqs+*I~sLI$Oq`>i`J1kJ(+yNOYy$_>R3Jfi680<|^u#J@aY%Q>O zqfI~sCbk#3--^zMkV&Yj0D(R^rK}+_npgPr_4^kYuG=pO%$C_7v{s@-{M-P@RL3^<`kO@b=YdKMuccfO1ZW# zeRYE%D~CMAgPlo?T!O6?b|pOZv{iMWb;sN=jF%=?$Iz_5zH?K;aFGU^8l7u%zHgiy z%)~y|k;Es-7YX69AMj^epGX#&^c@pp+lc}kKc`5CjPN4Z$$e58$Yn*J?81%`0~A)D zPg-db*pj-t4-G9>ImW4IMi*v#9z^9VD9h@9t;3jMAUVxt=oor+16yHf{lT|G4 zya6{4#BxFw!!~UTRwXXawKU4iz$$GMY6=Z8VM{2@0{=5A0+A#p6$aT3ubRyWMWPq9 zCEH5(Il0v4e4=Yxg(tDglfYAy!UpC>&^4=x7#6_S&Ktds)a8^`^tp6RnRd{KImB^o z2n=t#>iKx<*evmvoE{+fH#@WXGWs$)Uxrtf?r>AaxV0?kf0o@oDboJ6z0cgP@A$;k>SK1UqC?Q_ zk_I?j74;}uNXhOf_5ZxQSgB4otDEb9JJrX1kq`-o%T>g%M5~xXf!2_4P~K64tKgXq z&KHZ0@!cPvUJG4kw-0;tPo$zJrU-Nop>Uo65Pm|yaNvKjhi7V1g98;^N1~V3% zTR>yWa+X2FJ_wpPwz3i^6AGwOa_VMS-&`*KoKgF2&oR10Jn6{!pvVG@n=Jk@vjNuY zL~P7aDGhg~O9G^!bHi$8?G9v9Gp0cmekYkK;(q=47;~gI>h-kx-ceM{ml$#8KI$4ltyjaqP zki^cyDERloAb)dcDBU4na9C(pfD{P@eBGA}0|Rb)p{ISqi60=^FUEdF!ok{Gs;vb) zfj9(#1QA64w*ud^YsN5&PeiI>c`VioE8h)e}W%S9NMA55Gs zrWL6l+@3CKd@8(UQLTwe12SGWMqRn+j)QZRj*g)Xua)%ayzpqs{pD(WWESJYL3{M$ z%qkpM`jFoqLYVv6{IbCkL?fEiJj$VG=$taup&RL9e{s(Sgse2xVJlw0h74EXJKt2eX|dxz{->0)3W`JN7Bv!rLvRZc z0tAOZ2yVe4g9iq826qXAg`f!*+}(o1;1FDb>kKexumFS40KvK0yH1_@Z=LgWZ+}(Y zwYsa;OLz6tTA%gS=>8$=Z7pLh>|K2QElL)E=Q*(n*H`8R`8={-@4mTD-SWBOYRxV? zmF(-rJB8^Wlp?319rTrh^?QEP?|Msxrv?WbJ-+id+V#F2Y4(JPJ6U9bv+U1cIIH^W z)lg$_=g^Ma>2~Pyd_YOAv29Cb-U6DJO?NxnW7~QP*SmYi*vdUVuW#LWQ_u0`hymZi zaQS3Nb^4`ro$>0G%zbXmr5|D|iq0R<;S@?kr0j5Ruq87-Z1>crx%EzVZ9#U;{?}ti zW2W%*9MQg3Nbh%Ti6LhDd|-aFSgXoPG`mHlUU1iCHr>ru>DX?W_#13(`u*!Plu2OP z6jk=2>BC0l)aw;HCmxoYD1i4b%m$1`DYC_^L~ zIEAnFcHvad=-aO3(_MI=9#`z6-9*_!&$?<%meb5;jGd5Qp=MGf z6BD{%`L#TAOq%z%@*ib95Ey7NbUF=BlszVk3Iu3imD&*91N-ij%hW?W@~2TtdHTfP z#n0@Xd7X8Dyu36n{k#PwQ~T~X7mAO^cNV+z<HO@3X-# z_@rAn$k~(l@kciCC;&Qd*fWRI>=;fL{UPlciNDWyj$bX<#r^(r;EE8wwUVQm&7~QY zCXRj!**r^xybAEPq>h3W$uvI1j=yNIyzkE_D7fpGw)OV{U*Uwm{xB;mEg2(|y|ICd zMdQVqzMb-=XM6|E-a9kNh)^9lY`-DjhhHD1w5lufRcy+QLgJ47!fFne86#F; zX{ufroVBEZJOY?rDo!;Te6aOZ^1SO!dYRxQ*2njyA~dCWawn)>!*k7~>8Ikt&e*0>>V5ZbO|*1+2LFOqVe zXHb!aMk03^h%&9L8GMy7UDI2Kev>V@(R}*Iu6x+!Hn4~D@wj`P%#Hdbf(lK{+DD7f zJ&(v*mhn_e(R$^5L#bM^^Q@-!*b!l|+Xrb(q*MRFJYnrE7*xko!SJOy9LngR2|q5k zY`Ioiu+YBfzF{Labszk-E#*BYQk>$()=xWEGZRKwY)*UxP}0dGuPLZOkNJDI9Hy zFjfwiK6RjhH#rHW#B0(MW}i%V`943<6@Z*Nd^JEP5uZonXm=u%AM>{H^U@&Jy*i0s za_Da^xI6pMtXzHc{e~_ZcnKP*;=YL2Z^RmzDl{dJTk7*}E_h*NvgnhnxVKB59Duh~ zqouS_WoOR*{UvUw_K#OWz;gMracr%8>QQ&V*jv!8)ho;U8}9~8EU{N<=Z_gR%IpMT zbkePUG_afm=#|iIfFmdqkpLMGxY5D$`?I}&T7>TexU@v zkBx09kG)O;09ckj#(_Uov6vv{{HOcr-%H#DUQ@*GzF8Zh{iSM13%fuB%>wjdU@3Nf zlnYE!GTyNrqes|;nLFXfWU*Wg-9wmr=NBd$nCk+H?iwNvcd0Wab^3CT9a`>3V~oWI z9=_H+N-Q=MQ(io4u4mpdQ;k&5FXnKV5M7R`@WJ9h(GrAirO#XXOU{qQpk^B^Vd=Dt{wiqT zg-#j9J~@o%H2;W9mg)o6@*Vo;BSs2*4HAHpDk02mndAsov08R_48zJZ@J)s7+hyCo zy*0L#y)?AqZt-wX%+_Vx`8*A95OLHvs1$k~{h-_N_vov_gHJE=`X>L?5K+ zD?u59=mjtImMvd1GsDytuYp{IyUkW&?h zF>$#`n$~bZ)KN0B$XGeMYh&`;g8 zo_2-koaO6+8O!+L>SpIQbG(i;QW9UJi{Ecewlo?s&D!^>i$|#jaW}#HJuxt|W48=? zb^Y&O$a1s5ddr8DIt!sD!t=y1g(d4GR(s;s-HfV$GXl&m;+sAAxB^rk(3_NjE$p#L z*t4em?tA0d+XwRxN^OQwzbDZMuSE0J1)Ky{mq)^t4bnSl*)s>zNM@mMdtd78&ebHN z`!(|lE5q-p+TsRaNnMXwALaN5QIZ2IUi^Z22tsN5>nvIO+YU}Q*xh6}ee6@rR~<&1 z(PB4z>9ZBUMXZwSMmd9-aKKsmJeJq^G|#JclOh*xf0?^e0(`40nsg1z)(48;4}B_( zGwPI)yo|{oX{dVDL-5-aMGr;~vU1cPtJP5JM(sswz&Q`e<@0?y{YhsO9YK8EYJA;L z>7oG_Mts+(wCBC*Md82#XdKw&J*IizR?9k^rf1r{Ot-&>V^ke{9nI9zavlcNkIJtN z7T>?o|4rENk-?|lewZ(EfdR;%BUrzKJ^UkCpsM)EA9QHBVV8trT&*O(9?FO{MLTFL z=5P0H+T6C^jAuX0k4U;~GM!x`!X2N~3_n?qXY$HI>x@(DHEy&Q3ucT1R6fj28wX!I zC=&d$@bJ_v^%?W2Ngl}e8ww`b%BrN-PzGH;$@B2Ky1?%GMkm#~Okj(-Admyy;qya| zOi73kr_pwt?5Nj3p=&H>81!w#>Agj z(QXx{j0r=pTl>micAI_5vUw<3`Sht?Z}-j2Wx~F8DKCUQrsXl2?W8hur42(F_ zsSJ)_36&x6A|YkY6c<2a94SXbv~d>4CC4nkDPvf9Z5Fys^6^5r0j5=E>Cgy_Dk@tS z%?c}9!qB?t6t8(XMH%le8UeNWp@Nsma~Ql+^3Bo%_npMryeQJz4V=BAqE~T?dejng z3ge{fjCHoNAfYBvsfq;G%VL|j7t z`X0sy1EEgpyD;)tS1x+fnv-?C@glP0{RCW}Ma?3qpoq_&IJAYOy3G#s`rsh5=3>`K zkj``=;|*x5HSjZC zXNvPLh372q;=+6ja|SC!R-`JcL}}wwskajjTUGTpL(1zkN-p?BA2lmf+J3WsB7!k`0Brx8^cLTF9h)r+LZ$vsZo}`OpOs)?c6$hclR!R#MAeh|_DY|9r zy+_3c%IO9h9X?ksp?an&>Lw;QeQ`T-Ku6HaK~H?E9-Z5$cZu{YU;1+-6B$|JD;%!^ zt(4l>F8}a-UkC4YtOxFHckhl4VKr6P$P_O*U!)IDory%}Wz`YeFx6TO{y2Y${SBm?H9cTWV=WWJ z`_*CGso!ZN>l@~_jkeXtV}fczfA{TUkyeD>)i3|NFGcCsBmK3HXp&ol_@GVs7PIpfULy!hi zs+%KYgS%(n7_z_}6)hblk~W#LZ@&2)fwm6xkFP%&Ju|MFWbNiTwy{{g-pV1RK`L&=RE2D z4|g;~vd8xd|teYS%w!IlT4W$&FTrk-hcTADX!P?*f1YWEIRwq$Ys%^(Z9w&HT$>} zsMD#6Df=uJrX!JHP7<>Or;e_Cf=}`!`qR=i8fBj)$6Lxx{HRzd8Tnzd0p>kSps{OG zKJkml>bUj8$u|F=``l(-aMxWBC@CGZ#FXClQZ<4|&%jN}Tkg#q8z)=>Ly{$i0`rjU zvt|QddO&i=91e?h3>s~i;+6{ z8X4i6a1wDLrSuE#W(zhan+U*Zq+8p3a))JFVF4ffaV51K^YgTso~3;Y*NmM; zx8T?y-N0uyWY(8=me-HUC9xtABvX5~%yg+Cp&XF$Bq=OcK6T*D7eZ2EmIoCFWm{$S z1PNw8HDpe5hHeCusN8kdeb&f2#=3M^A~7YwJ7FRrhq*)PG9x?JIAaC{MV}5}g#7R$-Ly%)4=IUkRCGOR|XTMjn&okRmFjaO^YF5^* z@)#MCBOBezD)*xQNxydlUyN?dW{fS(s-T`gv*0BEnk}`BdmrbmPO8q8y(X$AA}*RH%I7Av!~84pudHb&%Q5-j zt?=6x(iR?<^_7X0v6Ys#VAL}dKk^hcjI=|EY;kPcZ_w<*H`_*|N7SacaM1ERD@6ab zg`!iTm7$URV+lpW_{V$ruR&A>jrX68k4x2wo$45}&wf7o<|o(@B!u-L@bKyQBAGwy z4#}UrRAu>^>Vb6k2-th^>WjvP;Nl|i3WrjWv3ISkj{m{eAcQIW^_ndxSX@|8T(ASJ z?_$fcP2u*6uOBk-{d>^ z0vWlfGQMvysI%R=iE|A+!!Nw?C917EU*_$`;;)px?s83CRd3i_jBN)k#nR5t$dJ(+ z_sP;wG@Ad)^(3LRj7q}0b2O(b`|i0~5SYb%Sjk^*5ISZ-Ab+}DGu$-X1n^TF1Ndw_ zF|e*1)cI2%`TR&AW~XpqpFb!=3cHbS>np9hYD_Mr5}y5Y`SY^r7isA2Q4(z zazRQEqWDKT2zIEbjSYdCPi1ZOGz80Nsl}gxO^DWMY0AV<2K&OL{&^6#@L1?lXu#6xSMh%3^5c*}oM6DQGY#(a^@z<&D zF(43I9e&5`h|A$5!+UFuOH0>F3$shBV4`0#M4RSB8=6F0ZgIbq<2LQ$Hh^(kAJu=! zt8ZGXTacD{(3W{V1$j_{Jc)Ka7t6u}ho`4kF+4@t_0!mCBn z)}o%eA}L)_L?=jw6BIfll7tb3n}?*yLt&XADa=rW>qz=_6s9ziOd5sXjil>FVFx3r zf>Feewk0v#W9>Gp4GacTRr>Sd2T6dWi-{YX`v!D)kCWzG5xQB=?es5ON(%nkwUhNl zV>@xkWWWv*N+{e$(SrExvN6BXzU(Hxlx27{VYHf+LpIbTO+Yu(ltMk<;)3A(LU@ytVYFkYvTa79idMtUFhfxx?P!)2F`prNWW#Fub#l>N2s@nh&n_ zA4{#}|AIs9|A4P0ZF%fy=hDN!t#ifH<)4u2kirK~JUpjQ-J+~cXOZI&dIts;P}UeXslP6zKvpEKSN-$y>kJ^nw2tC9bv zo(|lT@?vZ!{_l|d^8Yh)eEBh*5ABh+Lzjw+?V)o z#P-W7361>E(Y4;@`sv;VKn G`u_lkUM?>H literal 0 HcmV?d00001 diff --git a/public/fonts/glyphicons-halflings-regular.woff2 b/public/fonts/glyphicons-halflings-regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..64539b54c3751a6d9adb44c8e3a45ba5a73b77f0 GIT binary patch literal 18028 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 literal 0 HcmV?d00001 diff --git a/public/js/main.js b/public/js/main.js index f166a23..27110e7 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -16,12 +16,7 @@ var objectStore; var myIp; -$.getJSON('https://api.ipify.org?format=json', function (data) { - myIp = data.ip; -}); -// $.getJSON("http://jsonip.com?callback=?", function (data) { -// myIp = data.host; -// }); + @@ -68,38 +63,25 @@ function getUrl(){ return _.template(services[options.service])(options); } - -function ebayParser(){} - // s = req.lstrip('/**/nil/(').rstrip(')') - // sugg_texts = json.loads(s)['res']['sug'] - // print('j', sugg_texts) - // return {'sugg_texts': sugg_texts} - - -function twitterParser(){} - // j = json.loads(req) - // return { - // 'sugg_texts': [t['topic'] for t in j['topics']], - // 'meta': j, - // 'relevances': [t['rounded_score'] for t in j['topics']], - // } - -/** Parse response per service **/ -function parseServiceResponse(res){ - var service = $('#service').val(); - var parser = RESPONSE_TEMPLATES[service] || RESPONSE_TEMPLATES["default"]; - return parser(res); -} var RESPONSE_TEMPLATES = { "default": function(res){return res[1];}, "google": function(res){return res[1];}, "youtube": function(res){return res[1];}, "yahoo": function(res){return _.map(res.gossip.results,'key');}, "bing": function(res){return res[1];}, - "ebay": function(res){return res.res.sug;}, + "ebay": function(res){ + return res.res? res.res.sug: undefined; + }, "amazon": function(res){return res[1];}, - "twitter": function(res){return Array.concat(res.users,_.map(res.topics,'topic'),res.hashtags,res.oneclick);} + "twitter": function(res){return _.concat(res.users,_.map(res.topics,'topic'),res.hashtags,res.oneclick);} }; +/** Parse response per service **/ +function parseServiceResponse(res){ + var service = $('#service').val(); + var parser = RESPONSE_TEMPLATES[service] || RESPONSE_TEMPLATES["default"]; + return parser(res); +} + // setup a db. Ref: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB @@ -109,18 +91,16 @@ function errorHandler(){ return this; // for chaining } var db; -var dbReq = window.indexedDB.open("KeywordShitter2", 2); +var dbReq = window.indexedDB.open("KeywordShitter2", 3); +/** Error handler for all child transactions as events bubbe up **/ dbReq.onerror = function (event) { console.error('dbReq', event); }; dbReq.onsuccess = function (event) { - // Do something with request.result! - console.log('dbReq', event); db = event.target.result; db.onerror = function (event) { - // Generic error handler for all errors targeted at this database's - // requests! - console.error("Database error: " + event.target.errorCode); + // Generic error handler requests + console.error("Database error: " + event.currentTarget.error.errorCode, event.currentTarget.error.message, event); }; }; dbReq.onupgradeneeded = function (event) { @@ -128,22 +108,18 @@ dbReq.onupgradeneeded = function (event) { db = event.target.result; if (!db.objectStoreNames.contains("suggestions")) { - objectStore = db.createObjectStore("suggestions", { - autoIncrement: true + var objectStore = db.createObjectStore("suggestions", { + autoIncrement: true, keyPath: 'id' }); - // Create an index to search suggestions by // he query that prompted the suggestion - objectStore.createIndex("search", "search", { - unique: false - }); - // and by suggestion - objectStore.createIndex("keyword", "keyword", { - unique: false - }); + if (!objectStore.indexNames.contains('search')) + objectStore.createIndex("search", "search", {unique: false}); + + // and by suggestion/keyword + if (!objectStore.indexNames.contains('keyword')) + objectStore.createIndex("keyword", "keyword", {unique: false}); - } else { - // objectStore = db.objectStore("customers"); } }; @@ -157,7 +133,7 @@ function StartJob() { hashMapResults[""] = 1; // hashMapResults[" "] = 1; - // hashMapResults[" "] = 1; + hashMapResults[" "] = 1; // update config prefixes = $('#prefixes').val().split(','); @@ -229,7 +205,7 @@ function permuteResultsToQueue(retList, search){ hashMapResults[cleanKw] = 1; // add base suggestion to queue - if (cleanKw!==search) + if (cleanKw && cleanKw.trim().length && cleanKw!==search) keywordsToQuery[keywordsToQuery.length] = cleanKw; // add prefix permutations @@ -257,80 +233,120 @@ function loadFromDB(){ .getAll() .onsuccess = function(e) { if (e.target.result.length){ - table.data(e.target.result); + /// grab the fields we want var data =[]; for (var i = 0; i < e.target.result.length; i++) { var d = e.target.result[i]; - data.push([i,d.keyword,d.Length,null,null,d.search]); + var da = [ + d.id, + d.keyword, + d.length, + d.volume, + d.cpc, + d.search, + d.domain + ]; + // also remove undefined so datatables doesn't bring up alerts + da = da.map(function(v){return v===undefined ? null: v;}); + // parse nums + // da = da.map(v => /^[\d+\.]+$/.test(v) ? Number(v): v); + data.push(da); } table.rows.add(data); } - table.draw(false); + return table.draw(false); }; reqObj.onerror=errorHandler; + return; } /** Display results **/ -function displayResults(retList, search, dontDisplay){ - - - - - +function displayResults(retList, search, dontDisplay, url,data){ + var rows=[]; for (var i = 0; i < retList.length; i++) { var cleanKw = CleanVal(retList[i]); - // we get an annoying popup alert if we add undefined values, catch them - if (cleanKw===undefined||table.rows()[0].length===undefined||cleanKw.length===undefined,search===undefined){ - console.error('Undefined values',{ - id: table.rows()[0].length, - keyword: cleanKw, - length: cleanKw? cleanKw.length: undefined, - search:search - }); - continue; + // url might be in retlist + if (url===undefined) url=data[i].url; - } else if (cleanKw!==undefined && cleanKw.length){ - // Check if suggestion is already displayed before adding + var da = [ + table.rows()[0].length+i, + cleanKw, + cleanKw.length, + null, + null, + search, + extractDomain(url) + ]; + + // remove undefined values to avoid datatable alerts + da = da.map(function(v){return v===undefined ? null: v;}); + + // TODO Check if suggestion is already displayed before adding // var matches = table.data().filter(function(v){return v[1]===cleanKw && v[5]==search;}).count(); // if (!matches) - table.row.add([ - table.rows()[0].length, - cleanKw, - cleanKw.length, - null, - null, - search]); - } + rows.push(da); } + table.rows.add(rows); if (!dontDisplay) table.draw(false); } +function extractDomain(url) { + if (url===undefined) return null; + var domain; + //find & remove protocol (http, ftp, etc.) and get domain + if (url.indexOf("://") > -1) { + domain = url.split('/')[2]; + } + else { + domain = url.split('/')[0]; + } + + //find & remove port number + domain = domain.split(':')[0]; + + return domain; +} + /** Store new results in db and hashmap **/ function storeResults(retList, search, url){ - for (var i = 0; i < retList.length; i++) { - var cleanKw = CleanVal(retList[i]); - if (cleanKw.length){ - // TODO check if I should add in bulk? + // We will add the items async in order + // Ref: http://stackoverflow.com/a/13666741/221742 + var transaction = db.transaction(["suggestions"], "readwrite"); + var store = transaction.objectStore("suggestions"); + var i=0; + addNext(); - // add to db - var transaction = db.transaction(["suggestions"], "readwrite"); - transaction.onerror = errorHandler; - var objectStore = transaction.objectStore("suggestions"); - addReq = objectStore.add({ - keyword: cleanKw, - Length: cleanKw.length, - search: search, - ip: myIp, - url: this.url, - time: (new Date()).toUTCString() - }); - addReq.onerror=errorHandler; + /** Like an async for loop to add each to db **/ + function addNext() { + if (i<'col-sm-7'<'pull-right'p>>>" + "<'row'<'col-sm-8'i><'col-sm-4'<'pull-right'f>>>" + "<'row'<'col-sm-12'tr>>", - buttons: ['copyHtml5', 'csvHtml5','colvis','pageLength'], + buttons: [ + 'copyHtml5', + 'csvHtml5', + 'colvis', + 'pageLength', + { + extend: 'csvHtml5', + fieldBoundary: "", + text: 'Copy keywords', + // 'customize': function(data,options){return data.split('\n').join(',');}, + header: false, + exportOptions: { + stripNewlines: true, + stripHtml: true, + decodeEntities: true, + columns: 1, + // format:{ + // body: function(html,i){console.log(html);return html} + // } + } + }, + ], "columnDefs": [ { "name": "id", @@ -574,9 +615,17 @@ $(document).ready(function () { "name": "search", "targets": 5, "visible": false, + }, { + "name": "domain", + "targets": 6, + "visible": false, }], order: [[ 0, 'desc' ]], colReorder: {}, stateSave: true }); }); + +$.getJSON('https://api.ipify.org?format=json', function (data) { + myIp = data.ip; +}); From 8e80e77cc8ad450d127c1a0f26af393cda58d381 Mon Sep 17 00:00:00 2001 From: Is Isilon Date: Wed, 10 Feb 2016 13:26:52 +0800 Subject: [PATCH 3/9] Added lang and country codes And improved datatable --- package.json | 5 +- public/css/style.css | 4 + public/index.html | 330 ++++++++++++++++++++++++++++++++++++++++--- public/js/main.js | 223 +++++++++++++++++------------ src/less/main.less | 4 + 5 files changed, 459 insertions(+), 107 deletions(-) diff --git a/package.json b/package.json index d646ac0..ceb9b1c 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,11 @@ "build": "npm run build-css", "start": "node server.js" }, - "author": "", + "author": "blackmia,wassname", "license": "MIT", "devDependencies": { "cors": "^2.7.1", - "express": "^4.13.4" + "express": "^4.13.4", + "lessc": "latest" } } diff --git a/public/css/style.css b/public/css/style.css index de240d1..e9ea7b4 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -6888,3 +6888,7 @@ label { .pagination { margin: 0px; } +.dataTables_wrapper .dataTables_paginate .paginate_button { + margin: 0px; + padding: 0px; +} diff --git a/public/index.html b/public/index.html index 836dd96..31cba47 100644 --- a/public/index.html +++ b/public/index.html @@ -4,15 +4,23 @@ + + + + + + + + + - - +
@@ -46,13 +54,13 @@ lying internet - Load cache + Reset - -
- +
+ Load cache + Export cache
@@ -73,11 +81,303 @@ lying internet
- + +
- +
@@ -86,7 +386,7 @@ lying internet
- +
@@ -131,7 +431,7 @@ lying internet

Results

- +
@@ -140,6 +440,7 @@ lying internet + @@ -153,19 +454,16 @@ lying internet - + - - - - + Fork me on GitHub - + diff --git a/public/js/main.js b/public/js/main.js index 27110e7..ae077c2 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -1,35 +1,31 @@ -var keywordsToDisplay = []; +// quick way to check if we already have keyword var hashMapResults = {}; + +// keeping track of queue var numOfInitialKeywords = 0; -var doWork = false; var keywordsToQuery = []; var keywordsToQueryIndex = 0; + +// flags +var doWork = false; var queryLock = false; - - +var db var table; var prefixes; var suffixes; -var objectStore; - var myIp; - - - - - -// TODO Implement alternative services -// Ref: https://github.com/estivo/Instantfox/blob/master/firefox/c1hrome/content/defaultPluginList.js -// Ref: https://github.com/bnoordhuis/mozilla-central/tree/master/browser/locales/en-US/searchplugins - /** * Get the service url based on options set in the dom. * @return {String} A jsonp url for search suggestions with query missing from the end. */ function getUrl(){ + // Ref: https://github.com/estivo/Instantfox/blob/master/firefox/c1hrome/content/defaultPluginList.js + // Ref: https://github.com/bnoordhuis/mozilla-central/tree/master/browser/locales/en-US/searchplugins + // Ref: https://developers.google.com/custom-search/json-api/v1/reference/cse/list + // https://developers.google.com/custom-search/docs/ref_languages services={ "google": "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&gl=${country}&callback=?&q=", @@ -63,67 +59,71 @@ function getUrl(){ return _.template(services[options.service])(options); } -var RESPONSE_TEMPLATES = { - "default": function(res){return res[1];}, - "google": function(res){return res[1];}, - "youtube": function(res){return res[1];}, - "yahoo": function(res){return _.map(res.gossip.results,'key');}, - "bing": function(res){return res[1];}, - "ebay": function(res){ - return res.res? res.res.sug: undefined; - }, - "amazon": function(res){return res[1];}, - "twitter": function(res){return _.concat(res.users,_.map(res.topics,'topic'),res.hashtags,res.oneclick);} -}; + /** Parse response per service **/ function parseServiceResponse(res){ + // Each take a json response tand return a keyword array + RESPONSE_TEMPLATES = { + "default": function(res){return res[1];}, // opensearch default + "google": function(res){return res[1];}, + "youtube": function(res){return res[1];}, + "yahoo": function(res){return _.map(res.gossip.results,'key');}, + "bing": function(res){return res[1];}, + "ebay": function(res){ + return res.res? res.res.sug: undefined; + }, + "amazon": function(res){return res[1];}, + "twitter": function(res){return _.concat(res.users,_.map(res.topics,'topic'),res.hashtags,res.oneclick);} + }; var service = $('#service').val(); var parser = RESPONSE_TEMPLATES[service] || RESPONSE_TEMPLATES["default"]; return parser(res); } - -// setup a db. Ref: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB - -/** Basic error handler **/ -function errorHandler(){ - console.error(this,arguments); - return this; // for chaining -} -var db; -var dbReq = window.indexedDB.open("KeywordShitter2", 3); -/** Error handler for all child transactions as events bubbe up **/ -dbReq.onerror = function (event) { - console.error('dbReq', event); -}; -dbReq.onsuccess = function (event) { - db = event.target.result; - db.onerror = function (event) { - // Generic error handler requests - console.error("Database error: " + event.currentTarget.error.errorCode, event.currentTarget.error.message, event); +function setUpDb(){ + // setup a db. Ref: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB + var dbReq = window.indexedDB.open("KeywordShitter2", 3); + /** Error handler for all child transactions as events bubbe up **/ + dbReq.onerror = function (event) { + return console.error('Error opening indexedDB database KeywordShitter2', event); }; -}; -dbReq.onupgradeneeded = function (event) { - console.log("running onupgradeneeded"); - db = event.target.result; + dbReq.onsuccess = function (event) { + db = event.target.result; + db.onerror = function (event) { + // Generic error handler requests + if (event && event.target && event.target.error) + console.error("Database error",event.target.error.errorCode, event.target.error.message, event); + else + console.error("Database error",arguments); + return this; + }; + return db; + }; + dbReq.onupgradeneeded = function (event) { + console.log("running onupgradeneeded"); + db = event.target.result; - if (!db.objectStoreNames.contains("suggestions")) { - var objectStore = db.createObjectStore("suggestions", { - autoIncrement: true, keyPath: 'id' - }); - // Create an index to search suggestions by - // he query that prompted the suggestion - if (!objectStore.indexNames.contains('search')) - objectStore.createIndex("search", "search", {unique: false}); + if (!db.objectStoreNames.contains("suggestions")) { + var objectStore = db.createObjectStore("suggestions", { + autoIncrement: true, keyPath: 'id' + }); + // Create an index to search suggestions by + // he query that prompted the suggestion + if (!objectStore.indexNames.contains('search')) + objectStore.createIndex("search", "search", {unique: false}); - // and by suggestion/keyword - if (!objectStore.indexNames.contains('keyword')) - objectStore.createIndex("keyword", "keyword", {unique: false}); + // and by suggestion/keyword + if (!objectStore.indexNames.contains('keyword')) + objectStore.createIndex("keyword", "keyword", {unique: false}); + + } + + return db; + }; + return db; +} - } -}; -window.setInterval(DoJob, 750); function StartJob() { if (doWork === false) { @@ -226,10 +226,26 @@ function permuteResultsToQueue(retList, search){ } } +/** xport db as json **/ +function exportDB(){ + var reqObj = db.transaction(["suggestions"],"readonly") + .objectStore("suggestions") + .getAll(); + reqObj.onsuccess = function(e) { + if (e.target.result.length){ + var jsonData = JSON.stringify(e.target.result); + var blob = new Blob([jsonData], {type: "octet/stream"}); + saveAs(blob,'keywordshitter_'+(new Date())+'.json'); + } + return; + }; + return reqObj; +} + /** Display data from db upon pressing load button **/ function loadFromDB(){ - var reqObj = db.transaction(["suggestions"],"readonly"). - objectStore("suggestions") + var reqObj = db.transaction(["suggestions"],"readonly") + .objectStore("suggestions") .getAll() .onsuccess = function(e) { if (e.target.result.length){ @@ -246,17 +262,19 @@ function loadFromDB(){ d.search, d.domain ]; + // also remove undefined so datatables doesn't bring up alerts da = da.map(function(v){return v===undefined ? null: v;}); + // parse nums // da = da.map(v => /^[\d+\.]+$/.test(v) ? Number(v): v); + // data.push(da); } table.rows.add(data); + return table.draw(false); } - return table.draw(false); }; - reqObj.onerror=errorHandler; return; } @@ -292,6 +310,9 @@ function displayResults(retList, search, dontDisplay, url,data){ if (!dontDisplay) table.draw(false); } +/** Takes url string and returns domain e.g. www.google.com or google.com + * and some extra params to identify is + **/ function extractDomain(url) { if (url===undefined) return null; var domain; @@ -306,6 +327,18 @@ function extractDomain(url) { //find & remove port number domain = domain.split(':')[0]; + // custom, add ds= param to distinguish googles diff searchs + var ds = url.match('ds=(..?)&')[1]; + if (ds && ds.length) domain+='&ds='+ds; + + // also country + var gl = url.match('gl=(..?)&')[1]; + if (gl && gl.length) domain+='&gl='+gl; + + // also country + var hl = url.match('hl=(..?)&')[1]; + if (hl && hl.length) domain+='&hl='+gl; + return domain; } @@ -405,9 +438,8 @@ function QueryKeyword(search) { displayResults(retList, search, undefined, this.url); permuteResultsToQueue(retList); } else { - console.warn('No reslts for query "',search,'" ', this.url); + console.warn('No results for query "',search,'" '); } - markAsDone(search); queryLock = false; @@ -418,7 +450,6 @@ function QueryKeyword(search) { }); } }; - reqObj.onerror=errorHandler; } /** Clean input, may not all be needed **/ @@ -500,10 +531,11 @@ function Filter(listToFilter) { return retList; } +/** display the queue, and update description of it **/ function FilterAndDisplay() { var i = 0; var sb = ''; - var outputKeywords = Filter(keywordsToQuery); + var outputKeywords = keywordsToQuery; for (i = 0; i < outputKeywords.length; i++) { sb += outputKeywords[i]; sb += '\n'; @@ -522,6 +554,7 @@ function FilterAndDisplay() { // prefixes = $('#prefixes').val(); // suffixes = $('#suffixes').val(); // } + /** load settings from localStorage **/ function loadSettings(){ // TODO do table settings as well, e.g. column visibilitity @@ -548,6 +581,8 @@ function saveSettings(){ localStorage.prefixes = $('#prefixes').val(); localStorage.suffixes = $('#suffixes').val(); } + +/** reset inputs and results, but not settings **/ function reset(){ table.clear(); table.draw(); @@ -555,7 +590,9 @@ function reset(){ saveSettings(); } +setUpDb(); $(document).ready(function () { + window.setInterval(DoJob, 750); // // $('#startjob').on('click',StartJob); // $('#reset').on('click',reset); @@ -574,22 +611,22 @@ $(document).ready(function () { 'csvHtml5', 'colvis', 'pageLength', - { - extend: 'csvHtml5', - fieldBoundary: "", - text: 'Copy keywords', - // 'customize': function(data,options){return data.split('\n').join(',');}, - header: false, - exportOptions: { - stripNewlines: true, - stripHtml: true, - decodeEntities: true, - columns: 1, - // format:{ - // body: function(html,i){console.log(html);return html} - // } - } - }, + // { + // extend: 'csvHtml5', + // fieldBoundary: "", + // text: 'Copy keywords', + // // 'customize': function(data,options){return data.split('\n').join(',');}, + // header: false, + // exportOptions: { + // stripNewlines: true, + // stripHtml: true, + // decodeEntities: true, + // columns: 1, + // // format:{ + // // body: function(html,i){console.log(html);return html} + // // } + // } + // }, ], "columnDefs": [ { @@ -597,6 +634,7 @@ $(document).ready(function () { "targets": 0, "visible": false, }, { + "responsivePriority": 1, "name": "keyword", "targets": 1 }, { @@ -613,16 +651,23 @@ $(document).ready(function () { "visible": false, }, { "name": "search", + "responsivePriority": 3, "targets": 5, "visible": false, }, { "name": "domain", + "responsivePriority": 2, "targets": 6, "visible": false, }], order: [[ 0, 'desc' ]], colReorder: {}, - stateSave: true + stateSave: true, + fixedHeader: true, + responsive: true, + // scrollY: 500, + // deferRender: true, + // scroller: true }); }); diff --git a/src/less/main.less b/src/less/main.less index 4066125..e0046aa 100644 --- a/src/less/main.less +++ b/src/less/main.less @@ -57,3 +57,7 @@ .pagination { margin: 0px; } +.dataTables_wrapper .dataTables_paginate .paginate_button { + margin: 0px; + padding: 0px; +} From 518e1ac7af45e6144abfc8f68d9e86ff50ff5410 Mon Sep 17 00:00:00 2001 From: Is Isilon Date: Thu, 11 Feb 2016 10:59:59 +0800 Subject: [PATCH 4/9] Tests --- package.json | 7 +- public/index.html | 22 +- public/js/main.js | 211 ++++++++++------ public/js/test.js | 111 +++++++++ public/js/testData.js | 568 ++++++++++++++++++++++++++++++++++++++++++ public/test.html | 38 +++ 6 files changed, 870 insertions(+), 87 deletions(-) create mode 100644 public/js/test.js create mode 100644 public/js/testData.js create mode 100644 public/test.html diff --git a/package.json b/package.json index ceb9b1c..1ea7d47 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,9 @@ "version": "0.0.5", "description": "The second generation of keyword shitter. Bigger, better, and more pungent than other products.", "main": "server.js", + "repository": "https://github.com/wassname/keywordshitter2", "scripts": { - "test": "mocha", + "test": "public/test.html", "build-css": "lessc ./src/less/main.less > public/css/style.css", "build": "npm run build-css", "start": "node server.js" @@ -12,8 +13,10 @@ "author": "blackmia,wassname", "license": "MIT", "devDependencies": { + "chai": "^3.5.0", "cors": "^2.7.1", "express": "^4.13.4", - "lessc": "latest" + "lessc": "latest", + "mocha": "^2.4.5" } } diff --git a/public/index.html b/public/index.html index 31cba47..1b4080c 100644 --- a/public/index.html +++ b/public/index.html @@ -77,6 +77,8 @@ lying internet + +
@@ -84,6 +86,7 @@ lying internet + - - + + - l @@ -382,11 +384,11 @@ lying internet
- +
- +
@@ -400,7 +402,7 @@ lying internet
- +
@@ -465,6 +467,12 @@ lying internet Fork me on GitHub + + diff --git a/public/js/main.js b/public/js/main.js index ae077c2..93eb6b5 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -1,5 +1,5 @@ // quick way to check if we already have keyword -var hashMapResults = {}; +var hashMapInputs = {}; // keeping track of queue var numOfInitialKeywords = 0; @@ -10,18 +10,21 @@ var keywordsToQueryIndex = 0; var doWork = false; var queryLock = false; -var db +var maxQueueDisplay = 1000; +var db; var table; var prefixes; var suffixes; var myIp; + + /** * Get the service url based on options set in the dom. * @return {String} A jsonp url for search suggestions with query missing from the end. */ -function getUrl(){ +function getUrl(options){ // Ref: https://github.com/estivo/Instantfox/blob/master/firefox/c1hrome/content/defaultPluginList.js // Ref: https://github.com/bnoordhuis/mozilla-central/tree/master/browser/locales/en-US/searchplugins // Ref: https://developers.google.com/custom-search/json-api/v1/reference/cse/list @@ -49,38 +52,48 @@ function getUrl(){ "amazon": "http://completion.amazon.co.uk/search/complete?method=completion&search-alias=aps&mkt=4&callback=?&q=", "twitter": - "https://twitter.com/i/search/typeahead.json?count=30&result_type=topics&src=SEARCH_BOX&callback=?&q=" + "https://twitter.com/i/search/typeahead.json?count=30&result_type=topics&src=SEARCH_BOX&callback=?&q=", + "baidu": "http://suggestion.baidu.com/su?cb=?&wd=", + "yandex": "https://yandex.com/suggest/suggest-ya.cgi?callback=?&q=?&n=30&v=4&uil={lang}&part=" }; - options={ - country: $('#country').val(), - service: $('#service').val(), - lang: $('#lang').val(), - }; + + options = getOptions(options); return _.template(services[options.service])(options); } /** Parse response per service **/ -function parseServiceResponse(res){ +function parseServiceResponse(res,options){ // Each take a json response tand return a keyword array RESPONSE_TEMPLATES = { - "default": function(res){return res[1];}, // opensearch default - "google": function(res){return res[1];}, - "youtube": function(res){return res[1];}, - "yahoo": function(res){return _.map(res.gossip.results,'key');}, - "bing": function(res){return res[1];}, - "ebay": function(res){ - return res.res? res.res.sug: undefined; + // opensearch default + "default": function (res) { + return res[1]; + }, + "yahoo": function (res) { + return _.map(res.gossip.results, 'key'); + }, + "ebay": function (res) { + return res.res ? res.res.sug : undefined; + }, + "twitter": function (res) { + return _.concat(res.users, _.map(res.topics, 'topic'), res.hashtags, res.oneclick); + }, + "baidu": function (res) { + return res.s; + }, + "yandex": function(res){ + return _.map(res[1], function(r){ + return typeof r === 'string' ? r : r[1]; + }); }, - "amazon": function(res){return res[1];}, - "twitter": function(res){return _.concat(res.users,_.map(res.topics,'topic'),res.hashtags,res.oneclick);} }; - var service = $('#service').val(); - var parser = RESPONSE_TEMPLATES[service] || RESPONSE_TEMPLATES["default"]; + options = getOptions(options); + var parser = RESPONSE_TEMPLATES[options.service] || RESPONSE_TEMPLATES["default"]; return parser(res); } -function setUpDb(){ +function setUpDb(success){ // setup a db. Ref: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB var dbReq = window.indexedDB.open("KeywordShitter2", 3); /** Error handler for all child transactions as events bubbe up **/ @@ -97,6 +110,7 @@ function setUpDb(){ console.error("Database error",arguments); return this; }; + if (success) success(db,dbReq); return db; }; dbReq.onupgradeneeded = function (event) { @@ -120,32 +134,39 @@ function setUpDb(){ return db; }; - return db; + return dbReq; } function StartJob() { if (doWork === false) { - hashMapResults = {}; + hashMapInputs = {}; keywordsToQuery = []; keywordsToQueryIndex = 0; - hashMapResults[""] = 1; - // hashMapResults[" "] = 1; - hashMapResults[" "] = 1; + hashMapInputs[""] = 1; + // hashMapInputs[" "] = 1; + hashMapInputs[" "] = 1; // update config prefixes = $('#prefixes').val().split(','); suffixes = $('#suffixes').val().split(','); + // get queries from the input var ks = $('#input').val().split("\n"); for (var i = 0; i < ks.length; i++) { if (ks[i].trim().length) keywordsToQuery[keywordsToQuery.length] = ks[i]; } numOfInitialKeywords = keywordsToQuery.length; - if (!numOfInitialKeywords) permuteResultsToQueue([' ']); + + // add variations of the initial terms + // (before we start adding variations of the results) + if (!numOfInitialKeywords) + permuteResultsToQueue([' ']); + else + permuteResultsToQueue(keywordsToQuery); FilterAndDisplay(); doWork = true; @@ -193,51 +214,61 @@ function DoJob() { /** Make permutations of results and add to queue **/ function permuteResultsToQueue(retList, search){ + var chr, currentx, currentKw; // sort so the shortest is first in the queue retList.sort(function (a, b) { return a.length - b.length; }); + // add each result to list first before permutations + for (var j = 0; j < retList.length; j++) { + cleanKw = CleanVal(retList[j]); + // add base suggestion to queue if it's not already done and isn't empty + if (cleanKw && cleanKw.trim().length && cleanKw!==search && !hashMapInputs[cleanKw]) + keywordsToQuery[keywordsToQuery.length] = cleanKw; + hashMapInputs[cleanKw] = 1; + } for (var i = 0; i < retList.length; i++) { - var cleanKw = CleanVal(retList[i]); - if (cleanKw.length && !hashMapResults[cleanKw]){ - hashMapResults[cleanKw] = 1; - - // add base suggestion to queue - if (cleanKw && cleanKw.trim().length && cleanKw!==search) - keywordsToQuery[keywordsToQuery.length] = cleanKw; + cleanKw = CleanVal(retList[i]); + if (cleanKw.length){ // add prefix permutations for (var k = 0; k < prefixes.length; k++) { - var chr = prefixes[k]; - var currentx = chr + ' ' + cleanKw; - keywordsToQuery[keywordsToQuery.length] = currentx; - hashMapResults[currentx] = 1; + chr = prefixes[k]; + currentx = chr + ' ' + cleanKw; + if (!hashMapInputs[currentx]) + keywordsToQuery[keywordsToQuery.length] = currentx; + hashMapInputs[currentx] = 1; } // add suffix permutations for (var j = 0; j < prefixes.length; j++) { - var chr = prefixes[j]; - var currentx = cleanKw + ' ' + chr; - keywordsToQuery[keywordsToQuery.length] = currentx; - hashMapResults[currentx] = 1; + chr = prefixes[j]; + currentx = cleanKw + ' ' + chr; + if (!hashMapInputs[currentx]) + keywordsToQuery[keywordsToQuery.length] = currentx; + hashMapInputs[currentx] = 1; } } } } -/** xport db as json **/ -function exportDB(){ +/** export db as json **/ +function exportDB(success){ var reqObj = db.transaction(["suggestions"],"readonly") .objectStore("suggestions") .getAll(); reqObj.onsuccess = function(e) { + var blob, name; if (e.target.result.length){ var jsonData = JSON.stringify(e.target.result); - var blob = new Blob([jsonData], {type: "octet/stream"}); - saveAs(blob,'keywordshitter_'+(new Date())+'.json'); + blob = new Blob([jsonData], {type: "octet/stream"}); + var timeStamp = (new Date()).toISOString().replace(/[:\.]/g,'_').slice(0,-5); + name = 'keywordshitter_'+timeStamp+'_r'+e.target.result.length+'.json'; + saveAs(blob,name); + if (success) success(blob); } - return; + return blob; }; return reqObj; } @@ -327,17 +358,19 @@ function extractDomain(url) { //find & remove port number domain = domain.split(':')[0]; - // custom, add ds= param to distinguish googles diff searchs - var ds = url.match('ds=(..?)&')[1]; - if (ds && ds.length) domain+='&ds='+ds; + // custom, add ds= param to distinguish googles etc diff searchs + var mr = url.match('ds=(..?)&'); + if (mr && mr[1] && mr[1].length) domain+='&ds='+mr[1]; - // also country - var gl = url.match('gl=(..?)&')[1]; - if (gl && gl.length) domain+='&gl='+gl; + var mr = url.match('gl=(..?)&'); + if (mr && mr[1] && mr[1].length) domain+='&gl='+mr[1]; - // also country - var hl = url.match('hl=(..?)&')[1]; - if (hl && hl.length) domain+='&hl='+gl; + var mr = url.match('hl=(..?)&'); + if (mr && mr[1] && mr[1].length) domain+='&hl='+mr[1]; + + // lang code for yandex + var mr = url.match('uil=(..?)&'); + if (mr && mr[1] && mr[1].length) domain+='&uil='+mr[1]; return domain; } @@ -535,25 +568,39 @@ function Filter(listToFilter) { function FilterAndDisplay() { var i = 0; var sb = ''; + var outputKeywords = keywordsToQuery; - for (i = 0; i < outputKeywords.length; i++) { + for (i = 0; i < Math.min(outputKeywords.length,maxQueueDisplay); i++) { sb += outputKeywords[i]; sb += '\n'; } + if (outputKeywords.length>maxQueueDisplay) sb+='...\n'; $("#input").val(sb); - $("#numofkeywords").html('Queue:' + outputKeywords.length + ', Results: ' + table.data().length); + $("#numofkeywords").html('Queue:' + outputKeywords.length); +} + + +/** overrides default with dom options with arguments options **/ +function getOptions(argOptions){ + var defaultOptions={}; // for now defaults are set in html + if (argOptions===undefined) argOptions={}; + return _.defaults(argOptions,getDomOptions(),defaultOptions); } /** read settings from webpage **/ -// function readSettings(){ -// rateLimit = $('#service').val(); -// filterNegative = $('#filter-negative').val(); -// filterPositive = $('#filter-positive').val(); -// rateLimit = $('#rate-limit').val(); -// // input = $('#input').val(); -// prefixes = $('#prefixes').val(); -// suffixes = $('#suffixes').val(); -// } +function getDomOptions(){ + return { + service : $('#service').val(), + filterNegative: $('#filter-negative').val(), + filterPositive: $('#filter-positive').val(), + rateLimit: $('#rate-limit').val(), + // input: $('#input').val(), + prefixes: $('#prefixes').val(), + suffixes: $('#suffixes').val(), + country: $('#country').val(), + lang: $('#lang').val(), + }; +} /** load settings from localStorage **/ function loadSettings(){ @@ -590,8 +637,9 @@ function reset(){ saveSettings(); } -setUpDb(); -$(document).ready(function () { +function kws2Init(){ + setUpDb(); + window.setInterval(DoJob, 750); // // $('#startjob').on('click',StartJob); @@ -607,10 +655,16 @@ $(document).ready(function () { "<'row'<'col-sm-8'i><'col-sm-4'<'pull-right'f>>>" + "<'row'<'col-sm-12'tr>>", buttons: [ - 'copyHtml5', - 'csvHtml5', 'colvis', 'pageLength', + { + extend: 'collection', + text: 'Export', + buttons: [ + 'copyHtml5', + 'csvHtml5', + ] + } // { // extend: 'csvHtml5', // fieldBoundary: "", @@ -661,16 +715,17 @@ $(document).ready(function () { "visible": false, }], order: [[ 0, 'desc' ]], - colReorder: {}, + // colReorder: {}, stateSave: true, fixedHeader: true, - responsive: true, + // responsive: true, // scrollY: 500, // deferRender: true, // scroller: true }); -}); -$.getJSON('https://api.ipify.org?format=json', function (data) { - myIp = data.ip; -}); + $.getJSON('https://api.ipify.org?format=json', function (data) { + myIp = data.ip; + }); + +} diff --git a/public/js/test.js b/public/js/test.js new file mode 100644 index 0000000..b10bcc8 --- /dev/null +++ b/public/js/test.js @@ -0,0 +1,111 @@ +var expect = chai.expect; +var assert = chai.assert; + +describe('db', function() { + it('should correctly set up db', function() { + var dbReq=setUpDb(); + var oldonsuccess=dbReq.onsuccess; + dbReq.onsuccess=function(event){ + // override onld onsucess because it doesn't use promises + var db=oldonsuccess.bind(dbReq)(event); + assert(db); + }; + }); +}); + + +describe('extractDomain', function(){ + + it('should correctly extract domain from url', function() { + for (var expectedDomain in testData.domainUrls) { + if (testData.domainUrls.hasOwnProperty(expectedDomain)) { + var url = testData.domainUrls[expectedDomain]; + var domain = extractDomain(url); + assert.equal(expectedDomain,domain); + } + } + }); +}); + +// describe('permute', function(){ +// it('should correctly permute results and add to queue', function() { +// }); +// }); + +// TODO use localForage or most popular promise wrapper for indexedDB then test +// describe('exportDB', function(){ +// before(function() { +// // setup up test db +// }); +// +// after(function() { +// // tear down test db +// }); +// +// it('should correctly download json file', function() { +// exportDB(); +// }); +// }); + +describe('responses', function(){ + it('should correctly parse test data', function() { + for (var service in testData.responses) { + if (testData.responses.hasOwnProperty(service)) { + var res = testData.responses[service]; + var options = {service:service}; + var parsedRes = parseServiceResponse(res,options); + + assert.typeOf(parsedRes,'array'); + expect(parsedRes).to.have.length.above(1); + parsedRes.map(r => assert.typeOf(r, 'string')); + } + } + }); +}); + +describe('services', function() { + 'use strict'; + + // data + var services = ["google", "google news", "google shopping", "google books", "youtube", "google videos", "google images", "yahoo", "bing", "ebay", "amazon", "twitter", "baidu", "yandex"]; + var searches = ["where"]; + var searches2 = [""," "," * ",":)","❥","غرف شات","汉字"]; + + before(function() { + + }); + + after(function() { + + }); + + it('should correctly get and parse data', function() { + services.forEach(function(service){ + searches.forEach(function(search){ + var options = {service:service}; + var url = getUrl(options)+search; + $.ajax({ + url: url, + async: false, + jsonp: "jsonp", + dataType: "jsonp", + success: function (res, statusText, jqXHR) { + assert(statusText=="success"); + assert(res!==[]); + + + var parsedRes = parseServiceResponse(res,options); + assert.typeOf(parsedRes,'array'); + expect(parsedRes).to.have.length.above(1); + parsedRes.map(r => assert.typeOf(r, 'string')); + + }, + error: function(jqXHR,textStatus,err){ + throw(err); + } + }); + }); + }); + }); + +}); diff --git a/public/js/testData.js b/public/js/testData.js new file mode 100644 index 0000000..102eca5 --- /dev/null +++ b/public/js/testData.js @@ -0,0 +1,568 @@ +function testData() { + return { + domainUrls: { + "api.bing.com": "http://api.bing.com/osjson.aspx?JsonType=callback&JsonCallback=jQuery111&query=a &_=11111", + "autosug.ebay.com": "http://autosug.ebay.com/autosug?_jgr=1&sId=0&_ch=0&callback=jQuery111&kwd=a &_=11111", + "completion.amazon.co.uk": "http://completion.amazon.co.uk/search/complete?method=completion&search-alias=aps&mkt=4&callback=jQuery111&q=a &_=11111", + "search.yahoo.com": "https://search.yahoo.com/sugg/ff?output=jsonp&appid=ffd&callback=jQuery111&command=a &_=11111", + "suggestqueries.google.com&gl=US&hl=en": "http://suggestqueries.google.com/complete/search?client=chrome&hl=en&gl=US&callback=jQuery111&q=a &_=11111", + "suggestqueries.google.com&ds=i&gl=US&hl=en": "http://suggestqueries.google.com/complete/search?client=chrome&hl=en&ds=i&gl=US&callback=jQuery111&q=a &_=11111", + "suggestqueries.google.com&ds=sh&gl=US&hl=en": "http://suggestqueries.google.com/complete/search?client=chrome&hl=en&ds=sh&gl=US&callback=jQuery111&q=a &_=11111", + "suggestqueries.google.com&ds=yt&gl=US&hl=en": "http://suggestqueries.google.com/complete/search?client=chrome&hl=en&ds=yt&gl=US&callback=jQuery111&q=a &_=11111", + "suggestqueries.google.com&gl=us&hl=en": "http://suggestqueries.google.com/complete/search?client=chrome&hl=en&gl=us&callback=jQuery111&q=oil&_=11111", + "suggestqueries.google.com&gl=us&hl=fi": "http://suggestqueries.google.com/complete/search?client=chrome&hl=fi&gl=us&callback=jQuery111&q=a &_=11111", + "suggestqueries.google.com&gl=us&hl=he": "http://suggestqueries.google.com/complete/search?client=chrome&hl=he&gl=us&callback=jQuery111&q=a &_=11111", + "suggestqueries.google.combo": "http://suggestqueries.google.com/complete/search?client=chrome&hl=en&ds=bo&gl=US&callback=jQuery111&q=a &_=11111", + "twitter.com": "https://twitter.com/i/search/typeahead.json?count=30&result_type=topics&src=SEARCH_BOX&callback=jQuery111&q=I accidentally&_=11111" + }, + responses: { + "yahoo": { + "gossip": { + "qry": "where", + "gprid": "xfbzDJDaR9yN9o2B4J0JBA", + "results": [{ + "key": "where's my refund", + "mrk": 6 + }, { + "key": "where is super bowl 2016", + "mrk": 6 + }, { + "key": "where's my refund 2016", + "mrk": 6 + }, { + "key": "where the wild things are", + "mrk": 6 + }, { + "key": "wheresgeorge.com", + "mrk": 5 + }, { + "key": "where's waldo", + "mrk": 6 + }, { + "key": "where to get a passport", + "mrk": 6 + }, { + "key": "where the red fern grows", + "mrk": 6 + }, { + "key": "where can i watch movies online for free", + "mrk": 6 + }, { + "key": "where are they now", + "mrk": 6 + }] + } + }, + "google": ["where", ["where's my refund", "where am i", "http://www.irs.gov/individuals/article/0,,id=96596,00.html", "where is xur", "where is oj simpson now", "where is the next primary", "where to invade next", "where the wild things are", "where are you now", "where is dubai", "where is potomac", "where's waldo", "where is my phone", "where the heart is", "where the red fern grows", "where is the zika virus", "where was ted cruz born", "where is oak island", "where are you christmas", "where ya at"], + ["", "", "Where's My Refund - It's Quick, Easy, and Secure.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + [], { + "google:clientdata": { + "bpc": false, + "tlw": false + }, + "google:suggestrelevance": [1250, 602, 601, 600, 565, 564, 563, 562, 561, 560, 559, 558, 557, 556, 555, 554, 553, 552, 551, 550], + "google:suggesttype": ["QUERY", "QUERY", "NAVIGATION", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY"], + "google:verbatimrelevance": 851 + } + ], + "google news": ["where", ["where is mali", "http://www.irs.gov/individuals/article/0,,id=96596,00.html", "where is isis", "where is xur", "where is john cena", "where is ronda rousey", "where is isis now", "where is san bernardino", "where is luke skywalker", "where am i", "where she went movie", "where is josh duggar now", "where is el nino", "where is reid on criminal minds", "where will isis attack next", "where is isis located", "where is frank ocean", "where the hoes at", "where is spencer on criminal minds", "where is syria"], + ["", "Where's My Refund - It's Quick, Easy, and Secure.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + [], { + "google:clientdata": { + "bpc": false, + "tlw": false + }, + "google:suggestrelevance": [602, 601, 600, 566, 565, 564, 563, 562, 561, 560, 559, 558, 557, 556, 555, 554, 553, 552, 551, 550], + "google:suggesttype": ["QUERY", "NAVIGATION", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY"], + "google:verbatimrelevance": 851 + } + ], + "google books": ["where", ["where the red fern grows", "http://www.irs.gov/individuals/article/0,,id=96596,00.html", "where the wild things are", "where she went", "where was this book published", "where the sidewalk ends", "where rainbows end", "where am i", "where are you going where have you been", "wherever you go there you are", "where to buy", "where the red fern grows book", "where the mountain meets the moon", "where can i buy this book", "where the mind is without fear", "where is mali", "where good ideas come from", "where'd you go bernadette", "where's waldo", "whereas"], + ["", "Where's My Refund - It's Quick, Easy, and Secure.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + [], { + "google:clientdata": { + "bpc": false, + "tlw": false + }, + "google:suggestrelevance": [602, 601, 600, 566, 565, 564, 563, 562, 561, 560, 559, 558, 557, 556, 555, 554, 553, 552, 551, 550], + "google:suggesttype": ["QUERY", "NAVIGATION", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY"], + "google:verbatimrelevance": 851 + } + ], + "youtube": ["where", ["where are you now justin bieber", "where ya at future", "http://www.irs.gov/individuals/article/0,,id=96596,00.html", "where are you christmas", "where is the love", "where are you christmas faith hill", "where do the good boys go to hideaway", "where is my mind", "where they from", "where the hood at", "where are you now justin bieber lyrics", "where ya at future lyrics", "where is the love black eyed peas lyrics", "where have you been rihanna", "where is my mind piano", "wheresmychallenge", "wherever you will go the calling", "where you at", "where they at doe", "where them girls at"], + ["", "", "Where's My Refund - It's Quick, Easy, and Secure.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + [], { + "google:clientdata": { + "bpc": false, + "tlw": false + }, + "google:suggestrelevance": [950, 602, 601, 600, 565, 564, 563, 562, 561, 560, 559, 558, 557, 556, 555, 554, 553, 552, 551, 550], + "google:suggesttype": ["QUERY", "QUERY", "NAVIGATION", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY"], + "google:verbatimrelevance": 851 + } + ], + "bing": ["where", ["whereis", "where is", "whereis.com.au", "where is maps directions", "whereis nsw", "whereis.com", "where is santa", "whereis maps", "where is it", "where am i", "whereis.com.au directions", "where is.com.au"]], + "google videos": ["where", ["where are you now justin bieber", "where ya at future", "http://www.irs.gov/individuals/article/0,,id=96596,00.html", "where are you christmas", "where is the love", "where are you christmas faith hill", "where do the good boys go to hideaway", "where is my mind", "where they from", "where the hood at", "where are you now justin bieber lyrics", "where ya at future lyrics", "where is the love black eyed peas lyrics", "where have you been rihanna", "where is my mind piano", "wheresmychallenge", "wherever you will go the calling", "where you at", "where they at doe", "where them girls at"], + ["", "", "Where's My Refund - It's Quick, Easy, and Secure.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + [], { + "google:clientdata": { + "bpc": false, + "tlw": false + }, + "google:suggestrelevance": [950, 602, 601, 600, 565, 564, 563, 562, 561, 560, 559, 558, 557, 556, 555, 554, 553, 552, 551, 550], + "google:suggesttype": ["QUERY", "QUERY", "NAVIGATION", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY"], + "google:verbatimrelevance": 851 + } + ], + "google shopping": ["where", ["where to buy elf on the shelf", "http://www.irs.gov/individuals/article/0,,id=96596,00.html", "where to buy cards against humanity", "where to buy essential oils", "where to buy bean boozled", "where to buy hoverboard", "where the wild things are", "where to buy citric acid", "where to buy mistletoe", "where can i buy a hoverboard", "where can you buy bean boozled", "where to buy ugly christmas sweater", "where can i buy a lokai bracelet", "where can i buy elf on the shelf", "where to buy beard oil", "where can i buy mercer's wine ice cream", "where's waldo", "where in the world is carmen sandiego", "where the red fern grows", "where to buy garcinia cambogia"], + ["", "Where's My Refund - It's Quick, Easy, and Secure.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + [], { + "google:clientdata": { + "bpc": false, + "tlw": false + }, + "google:suggestrelevance": [602, 601, 600, 566, 565, 564, 563, 562, 561, 560, 559, 558, 557, 556, 555, 554, 553, 552, 551, 550], + "google:suggesttype": ["QUERY", "NAVIGATION", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY"], + "google:verbatimrelevance": 851 + } + ], + "ebay": { + "prefix": "where", + "dict": "0", + "res": { + "sug": ["where the wild things are", "where women create", "where the sidewalk ends", "where monsters dwell", "where eagles dare", "where the red fern grows", "where's waldo", "wheres mickey vera bradley", "wheres waldo book", "where the wild things are book"], + "categories": [ + [1093, "Children & Young Adults"], + [2624, "TV, Movie & Character Toys"] + ], + "isTopQuery": false + } + }, + "google images": ["where", ["where's waldo", "http://www.irs.gov/individuals/article/0,,id=96596,00.html", "where the wild things are", "where is mali", "where's wally", "where to shoot a deer", "where are you", "where is your liver", "where is syria", "where the red fern grows", "where is your appendix", "where are you now", "where is isis", "where is bora bora", "where is dubai", "where is the liver", "where the sidewalk ends", "where is the liver located", "where in the world is carmen sandiego", "where's the beef"], + ["", "Where's My Refund - It's Quick, Easy, and Secure.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + [], { + "google:clientdata": { + "bpc": false, + "tlw": false + }, + "google:suggestrelevance": [602, 601, 600, 566, 565, 564, 563, 562, 561, 560, 559, 558, 557, 556, 555, 554, 553, 552, 551, 550], + "google:suggesttype": ["QUERY", "NAVIGATION", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY", "QUERY"], + "google:verbatimrelevance": 851 + } + ], + "amazon": ["where", ["where she went", "where the wild things are", "where the trail ends", "this is where i leave you", "where is wally", "where to invade next", "where we are", "how to be parisian wherever you are", "where hope grows", "where is waldo"], + [{ + "nodes": [{ + "name": "Fremdsprachige Bücher", + "alias": "english-books" + }, { + "name": "Kindle-Shop", + "alias": "digital-text" + }] + }, {}, {}, {}, {}, {}, {}, {}, {}, {}], + [] + ], + "twitter": { + "num_results": 30, + "users": [], + "topics": [{ + "topic": "#WhereWereYou", + "rounded_score": 93013, + "tokens": [{ + "token": "#wherewereyou" + }], + "inline": false + }, { + "topic": "wherever you are", + "rounded_score": 75887, + "tokens": [{ + "token": "wherever" + }, { + "token": "you" + }, { + "token": "are" + }], + "inline": false + }, { + "topic": "where to invade next", + "rounded_score": 64255, + "tokens": [{ + "token": "where" + }, { + "token": "to" + }, { + "token": "invade" + }, { + "token": "next" + }], + "inline": false + }, { + "topic": "#WhereToInvadeNext", + "rounded_score": 64255, + "tokens": [{ + "token": "#wheretoinvadenext" + }], + "inline": false + }, { + "topic": "where are you now", + "rounded_score": 63587, + "tokens": [{ + "token": "where" + }, { + "token": "are" + }, { + "token": "you" + }, { + "token": "now" + }], + "inline": false + }, { + "topic": "#WhereDealsHappen", + "rounded_score": 63587, + "tokens": [{ + "token": "#wheredealshappen" + }], + "inline": false + }, { + "topic": "#WheresRey", + "rounded_score": 52917, + "tokens": [{ + "token": "#wheresrey" + }], + "inline": false + }, { + "topic": "where", + "rounded_score": 51445, + "tokens": [{ + "token": "where" + }], + "inline": false + }, { + "topic": "Wheres the coke baby", + "rounded_score": 43482, + "tokens": [{ + "token": "wheres" + }, { + "token": "the" + }, { + "token": "coke" + }, { + "token": "baby" + }], + "inline": false + }, { + "topic": "whereslloyd", + "rounded_score": 43109, + "tokens": [{ + "token": "whereslloyd" + }], + "inline": false + }, { + "topic": "where is my mind", + "rounded_score": 43074, + "tokens": [{ + "token": "where" + }, { + "token": "is" + }, { + "token": "my" + }, { + "token": "mind" + }], + "inline": false + }, { + "topic": "where do you see yourself", + "rounded_score": 40908, + "tokens": [{ + "token": "where" + }, { + "token": "do" + }, { + "token": "you" + }, { + "token": "see" + }, { + "token": "yourself" + }], + "inline": false + }, { + "topic": "WHERE FUTURES END", + "rounded_score": 40908, + "tokens": [{ + "token": "where" + }, { + "token": "futures" + }, { + "token": "end" + }], + "inline": false + }, { + "topic": "where you at", + "rounded_score": 40908, + "tokens": [{ + "token": "where" + }, { + "token": "you" + }, { + "token": "at" + }], + "inline": false + }, { + "topic": "WhereTheBlowAt", + "rounded_score": 40908, + "tokens": [{ + "token": "wheretheblowat" + }], + "inline": false + }, { + "topic": "where are we gonna meet up", + "rounded_score": 35857, + "tokens": [{ + "token": "where" + }, { + "token": "are" + }, { + "token": "we" + }, { + "token": "gonna" + }, { + "token": "meet" + }, { + "token": "up" + }], + "inline": false + }, { + "topic": "where are you at summer", + "rounded_score": 35857, + "tokens": [{ + "token": "where" + }, { + "token": "are" + }, { + "token": "you" + }, { + "token": "at" + }, { + "token": "summer" + }], + "inline": false + }, { + "topic": "wheresanna", + "rounded_score": 35857, + "tokens": [{ + "token": "wheresanna" + }], + "inline": false + }, { + "topic": "where you at donna", + "rounded_score": 35857, + "tokens": [{ + "token": "where" + }, { + "token": "you" + }, { + "token": "at" + }, { + "token": "donna" + }], + "inline": false + }, { + "topic": "where is justin bieber", + "rounded_score": 35857, + "tokens": [{ + "token": "where" + }, { + "token": "is" + }, { + "token": "justin" + }, { + "token": "bieber" + }], + "inline": false + }, { + "topic": "where the air is sweet", + "rounded_score": 35857, + "tokens": [{ + "token": "where" + }, { + "token": "the" + }, { + "token": "air" + }, { + "token": "is" + }, { + "token": "sweet" + }], + "inline": false + }, { + "topic": "where is luke", + "rounded_score": 35857, + "tokens": [{ + "token": "where" + }, { + "token": "is" + }, { + "token": "luke" + }], + "inline": false + }, { + "topic": "where is harry styles", + "rounded_score": 35857, + "tokens": [{ + "token": "where" + }, { + "token": "is" + }, { + "token": "harry" + }, { + "token": "styles" + }], + "inline": false + }, { + "topic": "where to buy beats", + "rounded_score": 35857, + "tokens": [{ + "token": "where" + }, { + "token": "to" + }, { + "token": "buy" + }, { + "token": "beats" + }], + "inline": false + }, { + "topic": "where are they", + "rounded_score": 35857, + "tokens": [{ + "token": "where" + }, { + "token": "are" + }, { + "token": "they" + }], + "inline": false + }, { + "topic": "where jc", + "rounded_score": 32980, + "tokens": [{ + "token": "where" + }, { + "token": "jc" + }], + "inline": false + }, { + "topic": "where's dylan o'brien", + "rounded_score": 32980, + "tokens": [{ + "token": "where" + }, { + "token": "s" + }, { + "token": "dylan" + }, { + "token": "o" + }, { + "token": "brien" + }], + "inline": false + }, { + "topic": "where is drenthe", + "rounded_score": 32980, + "tokens": [{ + "token": "where" + }, { + "token": "is" + }, { + "token": "drenthe" + }], + "inline": false + }, { + "topic": "where is anita", + "rounded_score": 32980, + "tokens": [{ + "token": "where" + }, { + "token": "is" + }, { + "token": "anita" + }], + "inline": false + }, { + "topic": "wheres my quarter", + "rounded_score": 32980, + "tokens": [{ + "token": "wheres" + }, { + "token": "my" + }, { + "token": "quarter" + }], + "inline": false + }], + "oneclick": [], + "hashtags": [], + "completed_in": 0.005, + "query": "where" + }, + "baidu": { + "q": "where", + "p": false, + "s": ["whereas", "where are you now", "where is the love", "where are you", "whereby", "where did you go", "wherever you are", "where did you sleep last night", "wherever", "where there is a will there is a way"] + }, + "yandex": ["where", ["where's my refund", "where is chuck norris", "where the wild things are", "where", "where's my refund 2013", "where is my tax refund", "wheresmyrefund irs refund status", "where's my state refund", "where is my state refund", "where's my water", ["", "where is my mind", { + "sg_weight": 3.69087e-9, + "search_cgi": [ + ["noreask", "1"] + ] + }], + ["", "where the wild roses grow", { + "sg_weight": 1.58832e-9, + "search_cgi": [ + ["noreask", "1"] + ] + }], + ["", "wherever you go", { + "sg_weight": 1.35346e-9, + "search_cgi": [ + ["noreask", "1"] + ] + }], + ["", "where have you been", { + "sg_weight": 1.30423e-9, + "search_cgi": [ + ["noreask", "1"] + ] + }], + ["", "where them girls at", { + "sg_weight": 1.10014e-9, + "search_cgi": [ + ["noreask", "1"] + ] + }], + ["", "where are you", { + "sg_weight": 9.90519e-10, + "search_cgi": [ + ["noreask", "1"] + ] + }], + ["", "where did you sleep last night", { + "sg_weight": 8.15709e-10, + "search_cgi": [ + ["noreask", "1"] + ] + }], + ["", "where is the love", { + "sg_weight": 7.31462e-10, + "search_cgi": [ + ["noreask", "1"] + ] + }] + ], { + "r": 211, + "log": "sgtype:BBBBBBBBBBArtArtArtArtArtArtArtArt", + "continue": "'s" + }] + } + }; +} diff --git a/public/test.html b/public/test.html new file mode 100644 index 0000000..3fe94e7 --- /dev/null +++ b/public/test.html @@ -0,0 +1,38 @@ + + + + + + Mocha Tests + + + + + + + + + + + + + + + + + + + +
+ + From 1d6c05829912bee2f0a67af3bb386918003ec5dd Mon Sep 17 00:00:00 2001 From: Is Isilon Date: Thu, 11 Feb 2016 11:57:49 +0800 Subject: [PATCH 5/9] Tests --- public/index.html | 6 +++ public/js/main.js | 2 +- public/js/test.js | 102 ++++++++++++++++++++++++++---------------- public/js/testData.js | 4 +- public/test.html | 2 + 5 files changed, 75 insertions(+), 41 deletions(-) diff --git a/public/index.html b/public/index.html index 1b4080c..1d3e543 100644 --- a/public/index.html +++ b/public/index.html @@ -14,6 +14,8 @@ + + @@ -461,6 +463,10 @@ lying internet + + + + diff --git a/public/js/main.js b/public/js/main.js index 93eb6b5..4904605 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -74,7 +74,7 @@ function parseServiceResponse(res,options){ return _.map(res.gossip.results, 'key'); }, "ebay": function (res) { - return res.res ? res.res.sug : undefined; + return res.res ? res.res.sug : []; }, "twitter": function (res) { return _.concat(res.users, _.map(res.topics, 'topic'), res.hashtags, res.oneclick); diff --git a/public/js/test.js b/public/js/test.js index b10bcc8..59a260e 100644 --- a/public/js/test.js +++ b/public/js/test.js @@ -1,14 +1,27 @@ var expect = chai.expect; var assert = chai.assert; +// TODO describe('permute', function(){ +// it('should correctly permute results and add to queue', function() { +// }); +// }); + +// TODO use localForage or most popular promise wrapper for indexedDB then test +// describe('exportDB', function(){ +// }); + describe('db', function() { - it('should correctly set up db', function() { + it('should correctly set up db', function(done) { var dbReq=setUpDb(); var oldonsuccess=dbReq.onsuccess; dbReq.onsuccess=function(event){ // override onld onsucess because it doesn't use promises var db=oldonsuccess.bind(dbReq)(event); assert(db); + assert(db.objectStoreNames.contains('suggestions'),'db should contain suggestons store'); + var indexNames =db.transaction('suggestions','readonly').objectStore('suggestions').indexNames; + assert(indexNames.contains('keyword','search')); + done(); }; }); }); @@ -16,41 +29,23 @@ describe('db', function() { describe('extractDomain', function(){ - it('should correctly extract domain from url', function() { - for (var expectedDomain in testData.domainUrls) { - if (testData.domainUrls.hasOwnProperty(expectedDomain)) { + for (var expectedDomain in testData.domainUrls) { + if (testData.domainUrls.hasOwnProperty(expectedDomain)) { + it('should correctly extract domain from url, domain='+expectedDomain, function() { var url = testData.domainUrls[expectedDomain]; var domain = extractDomain(url); assert.equal(expectedDomain,domain); - } + }); } - }); + } + }); -// describe('permute', function(){ -// it('should correctly permute results and add to queue', function() { -// }); -// }); - -// TODO use localForage or most popular promise wrapper for indexedDB then test -// describe('exportDB', function(){ -// before(function() { -// // setup up test db -// }); -// -// after(function() { -// // tear down test db -// }); -// -// it('should correctly download json file', function() { -// exportDB(); -// }); -// }); describe('responses', function(){ - it('should correctly parse test data', function() { - for (var service in testData.responses) { - if (testData.responses.hasOwnProperty(service)) { + for (var service in testData.responses) { + if (testData.responses.hasOwnProperty(service)) { + it('should correctly parse test data for service='+service, function() { var res = testData.responses[service]; var options = {service:service}; var parsedRes = parseServiceResponse(res,options); @@ -58,18 +53,22 @@ describe('responses', function(){ assert.typeOf(parsedRes,'array'); expect(parsedRes).to.have.length.above(1); parsedRes.map(r => assert.typeOf(r, 'string')); - } + }); } - }); + } }); describe('services', function() { 'use strict'; + // TODO test for each language and country and assert results!=default res + this.timeout(10000); + this.slow(2000); // data var services = ["google", "google news", "google shopping", "google books", "youtube", "google videos", "google images", "yahoo", "bing", "ebay", "amazon", "twitter", "baidu", "yandex"]; var searches = ["where"]; - var searches2 = [""," "," * ",":)","❥","غرف شات","汉字"]; + var searchesDifficult = [" * ", "❥"]; + before(function() { @@ -79,12 +78,40 @@ describe('services', function() { }); - it('should correctly get and parse data', function() { - services.forEach(function(service){ - searches.forEach(function(search){ + services.forEach(function(service){ + searches.forEach(function(search){ + it('should correctly get and parse data'+'. Service="'+service+'" search="'+search+'"', function(done) { var options = {service:service}; var url = getUrl(options)+search; - $.ajax({ + return $.ajax({ + url: url, + async: false, + jsonp: "jsonp", + dataType: "jsonp", + success: function (res, statusText, jqXHR) { + assert(statusText=="success"); + assert(res!==[]); + var parsedRes = parseServiceResponse(res,options); + assert.typeOf(parsedRes,'array'); + parsedRes.map(r => assert.typeOf(r, 'string')); + done(); + }, + error: function(jqXHR,textStatus,err){ + done(err); + return err; + } + }); + }); + }); + }); + + + services.forEach(function(service){ + searchesDifficult.forEach(function(search){ + it('should get and parse difficult data'+'. Service="'+service+'" search="'+search+'"', function(done) { + var options = {service:service}; + var url = getUrl(options)+search; + return $.ajax({ url: url, async: false, jsonp: "jsonp", @@ -93,15 +120,14 @@ describe('services', function() { assert(statusText=="success"); assert(res!==[]); - var parsedRes = parseServiceResponse(res,options); assert.typeOf(parsedRes,'array'); - expect(parsedRes).to.have.length.above(1); parsedRes.map(r => assert.typeOf(r, 'string')); + return done(); }, error: function(jqXHR,textStatus,err){ - throw(err); + return done(err); } }); }); diff --git a/public/js/testData.js b/public/js/testData.js index 102eca5..b95e701 100644 --- a/public/js/testData.js +++ b/public/js/testData.js @@ -1,4 +1,4 @@ -function testData() { +var testData = function () { return { domainUrls: { "api.bing.com": "http://api.bing.com/osjson.aspx?JsonType=callback&JsonCallback=jQuery111&query=a &_=11111", @@ -565,4 +565,4 @@ function testData() { }] } }; -} +}(); diff --git a/public/test.html b/public/test.html index 3fe94e7..dc45486 100644 --- a/public/test.html +++ b/public/test.html @@ -23,6 +23,8 @@ + + From f5b2d9eb08e1ec34f2c11da32902cc8ee411b4e5 Mon Sep 17 00:00:00 2001 From: Is Isilon Date: Thu, 11 Feb 2016 17:29:34 +0800 Subject: [PATCH 6/9] Refactored, working again --- public/index.html | 22 +- public/js/main.js | 1435 ++++++++++++++++++++++++--------------------- public/js/test.js | 41 +- 3 files changed, 798 insertions(+), 700 deletions(-) diff --git a/public/index.html b/public/index.html index 1d3e543..ecb2947 100644 --- a/public/index.html +++ b/public/index.html @@ -46,7 +46,7 @@ on purpose dismantled my lying internet
- Shit Keywords! + Shit Keywords! - Reset + Reset

- Load cache - Export cache + Load cache + Export cache
@@ -395,16 +395,22 @@ lying internet
-
- +
- + +
+ +
+
@@ -475,7 +481,7 @@ lying internet diff --git a/public/js/main.js b/public/js/main.js index 4904605..6c997f7 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -1,731 +1,804 @@ -// quick way to check if we already have keyword -var hashMapInputs = {}; +var KWS = function(){ -// keeping track of queue -var numOfInitialKeywords = 0; -var keywordsToQuery = []; -var keywordsToQueryIndex = 0; + return { + db:undefined, + table: undefined, + myIp: undefined, + options: {}, + // flags + queryLock: false, + doWork: false, + // keeping track of queue + hashMapInputs: {}, + keywordsToQuery: [], + keywordsToQueryIndex: 0, + numOfInitialKeywords: 0, + /** + * Get the service url based on options set in the dom. + * @return {String} A jsonp url for search suggestions with query missing from the end. + */ + getUrl :function(){ + // Ref: https://github.com/estivo/Instantfox/blob/master/firefox/c1hrome/content/defaultPluginList.js + // Ref: https://github.com/bnoordhuis/mozilla-central/tree/master/browser/locales/en-US/searchplugins + // Ref: https://developers.google.com/custom-search/json-api/v1/reference/cse/list + // https://developers.google.com/custom-search/docs/ref_languages + services={ + "google": + "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&gl=${country}&callback=?&q=", + "google news": + "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&ds=n&gl=${country}&callback=?&q=", + "google shopping": + "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&ds=sh&gl=${country}&callback=?&q=", + "google books": + "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&ds=bo&gl=${country}&callback=?&q=", + "youtube": + "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&ds=yt&gl=${country}&callback=?&q=", + "google videos": + "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&ds=v&gl=${country}&callback=?&q=", + "google images": + "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&ds=i&gl=${country}&callback=?&q=", + "yahoo": + "https://search.yahoo.com/sugg/ff?output=jsonp&appid=ffd&callback=?&command=", + "bing": "http://api.bing.com/osjson.aspx?JsonType=callback&JsonCallback=?&query=", + "ebay": + "http://autosug.ebay.com/autosug?_jgr=1&sId=0&_ch=0&callback=?&kwd=", + "amazon": + "http://completion.amazon.co.uk/search/complete?method=completion&search-alias=aps&mkt=4&callback=?&q=", + "twitter": + "https://twitter.com/i/search/typeahead.json?count=30&result_type=topics&src=SEARCH_BOX&callback=?&q=", + "baidu": "http://suggestion.baidu.com/su?cb=?&wd=", + "yandex": "https://yandex.com/suggest/suggest-ya.cgi?callback=?&q=?&n=30&v=4&uil={lang}&part=" + }; -// flags -var doWork = false; -var queryLock = false; - -var maxQueueDisplay = 1000; -var db; -var table; -var prefixes; -var suffixes; -var myIp; - - - - -/** - * Get the service url based on options set in the dom. - * @return {String} A jsonp url for search suggestions with query missing from the end. - */ -function getUrl(options){ - // Ref: https://github.com/estivo/Instantfox/blob/master/firefox/c1hrome/content/defaultPluginList.js - // Ref: https://github.com/bnoordhuis/mozilla-central/tree/master/browser/locales/en-US/searchplugins - // Ref: https://developers.google.com/custom-search/json-api/v1/reference/cse/list - // https://developers.google.com/custom-search/docs/ref_languages - services={ - "google": - "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&gl=${country}&callback=?&q=", - "google news": - "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&ds=n&gl=${country}&callback=?&q=", - "google shopping": - "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&ds=sh&gl=${country}&callback=?&q=", - "google books": - "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&ds=bo&gl=${country}&callback=?&q=", - "youtube": - "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&ds=yt&gl=${country}&callback=?&q=", - "google videos": - "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&ds=v&gl=${country}&callback=?&q=", - "google images": - "http://suggestqueries.google.com/complete/search?client=chrome&hl=${lang}&ds=i&gl=${country}&callback=?&q=", - "yahoo": - "https://search.yahoo.com/sugg/ff?output=jsonp&appid=ffd&callback=?&command=", - "bing": "http://api.bing.com/osjson.aspx?JsonType=callback&JsonCallback=?&query=", - "ebay": - "http://autosug.ebay.com/autosug?_jgr=1&sId=0&_ch=0&callback=?&kwd=", - "amazon": - "http://completion.amazon.co.uk/search/complete?method=completion&search-alias=aps&mkt=4&callback=?&q=", - "twitter": - "https://twitter.com/i/search/typeahead.json?count=30&result_type=topics&src=SEARCH_BOX&callback=?&q=", - "baidu": "http://suggestion.baidu.com/su?cb=?&wd=", - "yandex": "https://yandex.com/suggest/suggest-ya.cgi?callback=?&q=?&n=30&v=4&uil={lang}&part=" - }; - - options = getOptions(options); - return _.template(services[options.service])(options); -} - - -/** Parse response per service **/ -function parseServiceResponse(res,options){ - // Each take a json response tand return a keyword array - RESPONSE_TEMPLATES = { - // opensearch default - "default": function (res) { - return res[1]; + return _.template(services[this.options.service])(this.options); }, - "yahoo": function (res) { - return _.map(res.gossip.results, 'key'); - }, - "ebay": function (res) { - return res.res ? res.res.sug : []; - }, - "twitter": function (res) { - return _.concat(res.users, _.map(res.topics, 'topic'), res.hashtags, res.oneclick); - }, - "baidu": function (res) { - return res.s; - }, - "yandex": function(res){ - return _.map(res[1], function(r){ - return typeof r === 'string' ? r : r[1]; - }); - }, - }; - options = getOptions(options); - var parser = RESPONSE_TEMPLATES[options.service] || RESPONSE_TEMPLATES["default"]; - return parser(res); -} -function setUpDb(success){ - // setup a db. Ref: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB - var dbReq = window.indexedDB.open("KeywordShitter2", 3); - /** Error handler for all child transactions as events bubbe up **/ - dbReq.onerror = function (event) { - return console.error('Error opening indexedDB database KeywordShitter2', event); - }; - dbReq.onsuccess = function (event) { - db = event.target.result; - db.onerror = function (event) { - // Generic error handler requests - if (event && event.target && event.target.error) - console.error("Database error",event.target.error.errorCode, event.target.error.message, event); + + /** Parse response per service **/ + parseServiceResponse: function(res){ + // Each take a json response tand return a keyword array + RESPONSE_TEMPLATES = { + // opensearch default + "default": function (res) { + return res[1]; + }, + "yahoo": function (res) { + return _.map(res.gossip.results, 'key'); + }, + "ebay": function (res) { + return res.res ? res.res.sug : []; + }, + "twitter": function (res) { + return _.concat(res.users, _.map(res.topics, 'topic'), res.hashtags, res.oneclick); + }, + "baidu": function (res) { + return res.s; + }, + "yandex": function(res){ + return _.map(res[1], function(r){ + return typeof r === 'string' ? r : r[1]; + }); + }, + }; + var parser = RESPONSE_TEMPLATES[this.options.service] || RESPONSE_TEMPLATES["default"]; + return parser(res); + }, + + setUpDb: function(success){ + var self=this; + // setup a db. Ref: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB + var dbReq = window.indexedDB.open("KeywordShitter2", 3); + /** Error handler for all child transactions as events bubbe up **/ + dbReq.onerror = function (event) { + return console.error('Error opening indexedDB database KeywordShitter2', event); + }; + dbReq.onsuccess = function (event) { + self.db = event.target.result; + self.db.onerror = function (event) { + // Generic error handler requests + if (event && event.target && event.target.error) + console.error("Database error",event.target.error.errorCode, event.target.error.message, event); + else + console.error("Database error",arguments); + return this; + }; + if (success) success(self.db,dbReq); + return self.db; + }; + dbReq.onupgradeneeded = function (event) { + console.log("running onupgradeneeded"); + self.db = event.target.result; + + if (!self.db.objectStoreNames.contains("suggestions")) { + var objectStore = self.db.createObjectStore("suggestions", { + autoIncrement: true, keyPath: 'id' + }); + // Create an index to search suggestions by + // he query that prompted the suggestion + if (!objectStore.indexNames.contains('search')) + objectStore.createIndex("search", "search", {unique: false}); + + // and by suggestion/keyword + if (!objectStore.indexNames.contains('keyword')) + objectStore.createIndex("keyword", "keyword", {unique: false}); + + } + + return self.db; + }; + return dbReq; + }, + + toggleWork: function(){ + if (this.doWork === false) + this.StartWork(); else - console.error("Database error",arguments); - return this; - }; - if (success) success(db,dbReq); - return db; - }; - dbReq.onupgradeneeded = function (event) { - console.log("running onupgradeneeded"); - db = event.target.result; + this.StopWork(); + }, - if (!db.objectStoreNames.contains("suggestions")) { - var objectStore = db.createObjectStore("suggestions", { - autoIncrement: true, keyPath: 'id' - }); - // Create an index to search suggestions by - // he query that prompted the suggestion - if (!objectStore.indexNames.contains('search')) - objectStore.createIndex("search", "search", {unique: false}); + StartWork: function() { + if (this.doWork === false) { + // reset these + this.saveSettings(); + $('#startjob').val('Stop Job').text('Stop shitting').addClass('btn-danger'); + this.hashMapInputs = {}; + this.keywordsToQuery = []; + this.keywordsToQueryIndex = 0; - // and by suggestion/keyword - if (!objectStore.indexNames.contains('keyword')) - objectStore.createIndex("keyword", "keyword", {unique: false}); + this.hashMapInputs[""] = true; + this.hashMapInputs[" "] = true; + this.hashMapInputs[" "] = true; - } + // update config + this.options = this.getOptions(); - return db; - }; - return dbReq; -} + // get queries from the input + var ks = $('#input').val().split("\n"); + this.keywordsToQuery=ks.map(this.CleanVal); + // add variations of the initial terms + // (before we start adding variations of the results) + if (!this.keywordsToQuery.length) + this.permuteResultsToQueue([' ']); + else { + var untickedInputs = this.keywordsToQuery.filter(function(k){ + return k.slice(-1)!=='✓' && k.slice(-1)!=='❌'; + }); + this.permuteResultsToQueue(untickedInputs); + } -function StartJob() { - if (doWork === false) { - hashMapInputs = {}; - keywordsToQuery = []; - keywordsToQueryIndex = 0; + this.numOfInitialKeywords = this.keywordsToQuery.length; + // show the extended queue + this.FilterAndDisplay(); - hashMapInputs[""] = 1; - // hashMapInputs[" "] = 1; - hashMapInputs[" "] = 1; + this.doWork = true; + // $('#input').hide(); - // update config - prefixes = $('#prefixes').val().split(','); - suffixes = $('#suffixes').val().split(','); - - // get queries from the input - var ks = $('#input').val().split("\n"); - for (var i = 0; i < ks.length; i++) { - if (ks[i].trim().length) - keywordsToQuery[keywordsToQuery.length] = ks[i]; - } - numOfInitialKeywords = keywordsToQuery.length; - - // add variations of the initial terms - // (before we start adding variations of the results) - if (!numOfInitialKeywords) - permuteResultsToQueue([' ']); - else - permuteResultsToQueue(keywordsToQuery); - FilterAndDisplay(); - - doWork = true; - $('#startjob').val('Stop Job').text('Stop shitting').addClass('btn-danger'); - // $('#input').hide(); - - } else { - doWork = false; - $('#startjob').val('Start Job').text('Start shitting').removeClass('btn-danger'); - // $('#input').show(); - FilterAndDisplay(); - table.draw(); - table.columns.adjust(); - saveSettings(); - } -} - -function DoJob() { - if (doWork === true && queryLock === false) { - if (keywordsToQueryIndex < keywordsToQuery.length) { - var currentKw = keywordsToQuery[keywordsToQueryIndex]; - if (currentKw[currentKw.length - 1] != '✓') { - QueryKeyword(currentKw); - keywordsToQueryIndex++; } else { - // we didn't do a query immediatly go to next query - keywordsToQueryIndex++; - DoJob(); - } - } else { - if (numOfInitialKeywords != keywordsToQuery.length) { - doWork = false; + } + }, + + StopWork: function(){ + if (this.doWork){ $('#startjob').val('Start Job').text('Start shitting').removeClass('btn-danger'); - $('#input').show(); - FilterAndDisplay(); - table.draw(); - table.columns.adjust(); - } else { - keywordsToQueryIndex = 0; + this.doWork = false; + // $('#input').show(); + this.table.draw(); + this.table.columns.adjust(); + this.saveSettings(); + this.FilterAndDisplay(); } - } - } -} + }, -/** Make permutations of results and add to queue **/ -function permuteResultsToQueue(retList, search){ - var chr, currentx, currentKw; + DoJob: function() { + if (this.doWork === true && this.queryLock === false) { + if (this.keywordsToQueryIndex < this.numOfInitialKeywords) { + var currentKw = this.keywordsToQuery[this.keywordsToQueryIndex]; + if (currentKw.slice(-1)!=='✓' && currentKw.slice(-1)!=='❌') { + this.QueryKeyword(currentKw); + this.keywordsToQueryIndex++; + } else { + // we didn't do a query immediatly go to next query + this.keywordsToQueryIndex++; + this.DoJob(); + } - // sort so the shortest is first in the queue - retList.sort(function (a, b) { - return a.length - b.length; - }); - - // add each result to list first before permutations - for (var j = 0; j < retList.length; j++) { - cleanKw = CleanVal(retList[j]); - // add base suggestion to queue if it's not already done and isn't empty - if (cleanKw && cleanKw.trim().length && cleanKw!==search && !hashMapInputs[cleanKw]) - keywordsToQuery[keywordsToQuery.length] = cleanKw; - hashMapInputs[cleanKw] = 1; - } - for (var i = 0; i < retList.length; i++) { - cleanKw = CleanVal(retList[i]); - if (cleanKw.length){ - - // add prefix permutations - for (var k = 0; k < prefixes.length; k++) { - chr = prefixes[k]; - currentx = chr + ' ' + cleanKw; - if (!hashMapInputs[currentx]) - keywordsToQuery[keywordsToQuery.length] = currentx; - hashMapInputs[currentx] = 1; + } else { + if (this.options.keepRunning) { + console.log('finish initial queue'); + this.StopWork(); + this.StartWork(); + } else { + console.log('finish initial queue'); + this.StopWork(); + } + } } - // add suffix permutations - for (var j = 0; j < prefixes.length; j++) { - chr = prefixes[j]; - currentx = cleanKw + ' ' + chr; - if (!hashMapInputs[currentx]) - keywordsToQuery[keywordsToQuery.length] = currentx; - hashMapInputs[currentx] = 1; - } - } - } -} + }, -/** export db as json **/ -function exportDB(success){ - var reqObj = db.transaction(["suggestions"],"readonly") - .objectStore("suggestions") - .getAll(); - reqObj.onsuccess = function(e) { - var blob, name; - if (e.target.result.length){ - var jsonData = JSON.stringify(e.target.result); - blob = new Blob([jsonData], {type: "octet/stream"}); - var timeStamp = (new Date()).toISOString().replace(/[:\.]/g,'_').slice(0,-5); - name = 'keywordshitter_'+timeStamp+'_r'+e.target.result.length+'.json'; - saveAs(blob,name); - if (success) success(blob); - } - return blob; - }; - return reqObj; -} + addResultsToQueue: function(retList, search){ + retList=retList.map(this.CleanVal); -/** Display data from db upon pressing load button **/ -function loadFromDB(){ - var reqObj = db.transaction(["suggestions"],"readonly") - .objectStore("suggestions") - .getAll() - .onsuccess = function(e) { - if (e.target.result.length){ - /// grab the fields we want - var data =[]; - for (var i = 0; i < e.target.result.length; i++) { - var d = e.target.result[i]; - var da = [ - d.id, - d.keyword, - d.length, - d.volume, - d.cpc, - d.search, - d.domain + // add each result to list first before permutations + for (var j = 0; j < retList.length; j++) { + cleanKw = retList[j]; + // add base suggestion to queue if it's not already done and isn't empty + if (cleanKw && cleanKw.length && !this.hashMapInputs[cleanKw] && this.keywordsToQuery.indexOf(cleanKw)===-1) + this.keywordsToQuery.push(cleanKw); + this.hashMapInputs[cleanKw] = true; + } + + }, + + /** Make permutations of results and add to queue **/ + permuteResultsToQueue: function(retList, search){ + var chr, currentx, currentKw; + var self = this; + + this.hashMapInputs[search] = true; + + // sort so the shortest is first in the queue + // retList.sort(function (a, b) { + // return a.length - b.length; + // }); + + function addPrefix(s,prefix){ + return prefix+' '+s; + } + function addSuffix(s,suffix){ + return s+' '+suffix; + } + // clean + retList=retList.map(this.CleanVal); + + + + + + // get permutations + var newInputs = retList.reduce(function(result, keyword){ + return _.concat( + result, + self.options.prefixes.map(addPrefix.bind(self,keyword)), + self.options.suffixes.map(addSuffix.bind(self,keyword)) + ); + }, []); + + // add to queue + this.keywordsToQuery.push.apply(this.keywordsToQuery,newInputs); + + return newInputs; + }, + + /** export db as json **/ + exportDB: function(success){ + var reqObj = this.db.transaction(["suggestions"],"readonly") + .objectStore("suggestions") + .getAll(); + reqObj.onsuccess = function(e) { + var blob, name; + if (e.target.result.length){ + var jsonData = JSON.stringify(e.target.result); + blob = new Blob([jsonData], {type: "octet/stream"}); + var timeStamp = (new Date()).toISOString().replace(/[:\.]/g,'_').slice(0,-5); + name = 'keywordshitter_'+timeStamp+'_r'+e.target.result.length+'.json'; + saveAs(blob,name); + if (success) success(blob); + } + return blob; + }; + return reqObj; + }, + + /** Display data from db upon pressing load button **/ + loadFromDB: function(){ + var self = this; + var reqObj = this.db.transaction(["suggestions"],"readonly") + .objectStore("suggestions") + .getAll() + .onsuccess = function(e) { + if (e.target.result.length){ + /// grab the fields we want + var data =[]; + for (var i = 0; i < e.target.result.length; i++) { + var d = e.target.result[i]; + var da = [ + d.id, + d.keyword, + d.length, + d.volume, + d.cpc, + d.search, + d.domain + ]; + + // also remove undefined so datatables doesn't bring up alerts + da = da.map(function(v){return v===undefined ? null: v;}); + + // parse nums + // da = da.map(v => /^[\d+\.]+$/.test(v) ? Number(v): v); + // + data.push(da); + } + self.table.rows.add(data); + return self.table.draw(false); + } + }; + return; + }, + + /** Display results **/ + displayResults: function(retList, search, dontDisplay, url,data){ + + var rows=[]; + retList=retList.map(this.CleanVal); + for (var i = 0; i < retList.length; i++) { + var cleanKw = retList[i]; + + // url might be in retlist + if (url===undefined) url=data[i].url; + + var da = [ + this.table.rows()[0].length+i, + cleanKw, + cleanKw.length, + null, + null, + search, + this.extractDomain(url) ]; - // also remove undefined so datatables doesn't bring up alerts - da = da.map(function(v){return v===undefined ? null: v;}); + // remove undefined values to avoid datatable alerts + da = da.map(function(v){return v===undefined ? null: v;}); - // parse nums - // da = da.map(v => /^[\d+\.]+$/.test(v) ? Number(v): v); - // - data.push(da); - } - table.rows.add(data); - return table.draw(false); + // TODO Check if suggestion is already displayed before adding + // var matches = table.data().filter(function(v){return v[1]===cleanKw && v[5]==search;}).count(); + // if (!matches) + rows.push(da); } - }; - return; -} + this.table.rows.add(rows); + if (!dontDisplay) this.table.draw(false); + }, -/** Display results **/ -function displayResults(retList, search, dontDisplay, url,data){ - - var rows=[]; - for (var i = 0; i < retList.length; i++) { - var cleanKw = CleanVal(retList[i]); - - // url might be in retlist - if (url===undefined) url=data[i].url; - - var da = [ - table.rows()[0].length+i, - cleanKw, - cleanKw.length, - null, - null, - search, - extractDomain(url) - ]; - - // remove undefined values to avoid datatable alerts - da = da.map(function(v){return v===undefined ? null: v;}); - - // TODO Check if suggestion is already displayed before adding - // var matches = table.data().filter(function(v){return v[1]===cleanKw && v[5]==search;}).count(); - // if (!matches) - rows.push(da); - } - table.rows.add(rows); - if (!dontDisplay) table.draw(false); -} - -/** Takes url string and returns domain e.g. www.google.com or google.com - * and some extra params to identify is - **/ -function extractDomain(url) { - if (url===undefined) return null; - var domain; - //find & remove protocol (http, ftp, etc.) and get domain - if (url.indexOf("://") > -1) { - domain = url.split('/')[2]; - } - else { - domain = url.split('/')[0]; - } - - //find & remove port number - domain = domain.split(':')[0]; - - // custom, add ds= param to distinguish googles etc diff searchs - var mr = url.match('ds=(..?)&'); - if (mr && mr[1] && mr[1].length) domain+='&ds='+mr[1]; - - var mr = url.match('gl=(..?)&'); - if (mr && mr[1] && mr[1].length) domain+='&gl='+mr[1]; - - var mr = url.match('hl=(..?)&'); - if (mr && mr[1] && mr[1].length) domain+='&hl='+mr[1]; - - // lang code for yandex - var mr = url.match('uil=(..?)&'); - if (mr && mr[1] && mr[1].length) domain+='&uil='+mr[1]; - - return domain; -} - -/** Store new results in db and hashmap **/ -function storeResults(retList, search, url){ - - // We will add the items async in order - // Ref: http://stackoverflow.com/a/13666741/221742 - var transaction = db.transaction(["suggestions"], "readwrite"); - var store = transaction.objectStore("suggestions"); - var i=0; - addNext(); - - /** Like an async for loop to add each to db **/ - function addNext() { - if (i -1) { + domain = url.split('/')[2]; } else { - // search not done, lets do the query - url = getUrl()+search; - $.ajax({ - url: url, - jsonp: "jsonp", - dataType: "jsonp", - success: function (res, statusText, jqXHR) { - - var retList = parseServiceResponse(res); - var char, currentx; - if (retList && retList.length){ - storeResults(retList, search, this.url); - displayResults(retList, search, undefined, this.url); - permuteResultsToQueue(retList); - } else { - console.warn('No results for query "',search,'" '); - } - markAsDone(search); - queryLock = false; - - }, - error: function(){ - queryLock = false; - } - }); + domain = url.split('/')[0]; } - }; -} -/** Clean input, may not all be needed **/ -function CleanVal(input) { - var val = input; + //find & remove port number + domain = domain.split(':')[0]; - // legacy - // val = val.replace("", ""); - // val = val.replace("", ""); - // val = val.replace("", ""); - // val = val.replace("", ""); - // val = val.replace("", ""); - // val = val.replace("", ""); - // val = val.replace("", ""); - // val = val.replace("", ""); - // val = val.replace("&", "&"); - // val = val.replace("", ""); - // val = val.replace("&", ""); - // val = val.replace("'", "'"); - // val = val.replace("#39;", "'"); - // val = val.replace("", ""); - // val = val.replace("–", "2013"); + // custom, add ds= param to distinguish googles etc diff searchs + var mr = url.match('ds=(..?)&'); + if (mr && mr[1] && mr[1].length) domain+='&ds='+mr[1]; - // this removes navigation suggestions - if (val.length > 4 && val.substring(0, 4) == "http") val = ""; - return val; -} + var mr = url.match('gl=(..?)&'); + if (mr && mr[1] && mr[1].length) domain+='&gl='+mr[1]; -/** TODO get this working **/ -function Filter(listToFilter) { - var retList = listToFilter; + var mr = url.match('hl=(..?)&'); + if (mr && mr[1] && mr[1].length) domain+='&hl='+mr[1]; - if ($("#filter-positive").val().length > 0) { - var filteredList = []; - var filterContains = $("#filter-positive").val().split("\n"); - for (var i = 0; i < retList.length; i++) { - var currentKeyword = retList[i]; - var boolContainsKeyword = false; - for (var j = 0; j < filterContains.length; j++) { - if (filterContains[j].length > 0) { - if (currentKeyword.indexOf(filterContains[j]) != -1) { - boolContainsKeyword = true; - break; + // lang code for yandex + var mr = url.match('uil=(..?)&'); + if (mr && mr[1] && mr[1].length) domain+='&uil='+mr[1]; + + return domain; + }, + + /** Store new results in db and hashmap **/ + storeResults: function(retList, search, url){ + var self = this; + retList=retList.map(this.CleanVal); + + // We will add the items async in order + // Ref: http://stackoverflow.com/a/13666741/221742 + var transaction = this.db.transaction(["suggestions"], "readwrite"); + var store = transaction.objectStore("suggestions"); + var i=0; + addNext(); + + /** Like an async for loop to add each to db **/ + function addNext() { + if (i 0) { - var filteredList = []; - var filterContains = $("#filter-negative").val().split("\n"); - for (var i = 0; i < retList.length; i++) { - var currentKeyword = retList[i]; - var boolCleanKeyword = true; - for (var j = 0; j < filterContains.length; j++) { - if (filterContains[j].length > 0) { - if (currentKeyword.indexOf(filterContains[j]) >= 0) { - boolCleanKeyword = false; - break; + /** mark a search as done in the queue **/ + markAsNone: function(search){ + // mark as done in queue + if (this.keywordsToQuery[this.keywordsToQueryIndex]===search) + this.keywordsToQuery[this.keywordsToQueryIndex]+=' ❌'; + else if (this.keywordsToQuery[this.keywordsToQueryIndex-1]===search) + this.keywordsToQuery[this.keywordsToQueryIndex-1]+=' ❌'; + else + console.warn('Cant find ',search,'in keywordsToQuery'); + }, + + /** Get search suggestions for a keyword **/ + QueryKeyword: function(search) { + var self = this; + this.queryLock = true; + + // first check in db + var reqObj = this.db.transaction(["suggestions"],"readonly") + .objectStore("suggestions") + .index("search") + .getAll(search); + reqObj.onsuccess = function(e) { + var domain = self.extractDomain(self.getUrl()); + var results = e.target.result.filter(function(r){return r.domain==domain;}); + // console.log(e.target.result); + if (results.length){ + // search was done previously so display results from db + var retList=_.map(results,'keyword'); + self.markAsDone(search); + self.displayResults(retList,search,undefined,undefined,results); + self.addResultsToQueue(retList); + if (self.options.keepRunning) self.permuteResultsToQueue(retList); + self.queryLock = false; } - } - } + else { + // search not done, lets do the query + url = self.getUrl()+search; + $.ajax({ + url: url, + jsonp: "jsonp", + dataType: "jsonp", + success: function (res, statusText, jqXHR) { - if (boolCleanKeyword) { - filteredList[filteredList.length] = currentKeyword; - } - } + var retList = self.parseServiceResponse(res); + var char, currentx; + if (retList && retList.length){ + self.storeResults(retList, search, this.url); + self.displayResults(retList, search, undefined, this.url); + self.addResultsToQueue(retList); + if (self.options.keepRunning) self.permuteResultsToQueue(retList); + self.markAsDone(search); + } else { + // console.debug('No suggestions for query: "',search,'"'); + self.markAsNone(search); + } + self.queryLock = false; + return; - retList = filteredList; - } + }, + error: function(e){ + console.error(e); + self.queryLock = false; + return; + } + }); + } + }; + return reqObj; + }, - return retList; -} + /** Clean input, may not all be needed **/ + CleanVal: function(input) { -/** display the queue, and update description of it **/ -function FilterAndDisplay() { - var i = 0; - var sb = ''; - - var outputKeywords = keywordsToQuery; - for (i = 0; i < Math.min(outputKeywords.length,maxQueueDisplay); i++) { - sb += outputKeywords[i]; - sb += '\n'; - } - if (outputKeywords.length>maxQueueDisplay) sb+='...\n'; - $("#input").val(sb); - $("#numofkeywords").html('Queue:' + outputKeywords.length); -} - - -/** overrides default with dom options with arguments options **/ -function getOptions(argOptions){ - var defaultOptions={}; // for now defaults are set in html - if (argOptions===undefined) argOptions={}; - return _.defaults(argOptions,getDomOptions(),defaultOptions); -} - -/** read settings from webpage **/ -function getDomOptions(){ - return { - service : $('#service').val(), - filterNegative: $('#filter-negative').val(), - filterPositive: $('#filter-positive').val(), - rateLimit: $('#rate-limit').val(), - // input: $('#input').val(), - prefixes: $('#prefixes').val(), - suffixes: $('#suffixes').val(), - country: $('#country').val(), - lang: $('#lang').val(), - }; -} - -/** load settings from localStorage **/ -function loadSettings(){ - // TODO do table settings as well, e.g. column visibilitity - if (localStorage.service) $("#service").val( localStorage.service ); - if (localStorage.country) $('#country').val(localStorage.country); - if (localStorage.lang) $('#lang').val(localStorage.lang); - if (localStorage.filterNegative) $("#filter-negative").val( localStorage.filterNegative ); - if (localStorage.filterPositive) $("#filter-positive").val( localStorage.filterPositive ); - if (localStorage.rateLimit) $("#rate-limit").val( localStorage.rateLimit ); - if (localStorage.input) $("#input").val( localStorage.input ); - if (localStorage.prefixes) $("#prefixes").val( localStorage.prefixes ); - if (localStorage.suffixes) $("#suffixes").val( localStorage.suffixes ); - -} -/** save settings to localStorage. **/ -function saveSettings(){ - localStorage.service = $('#service').val(); - localStorage.country = $('#country').val(); - localStorage.lang = $('#lang').val(); - localStorage.filterNegative = $('#filter-negative').val(); - localStorage.filterPositive = $('#filter-positive').val(); - localStorage.rateLimit = $('#rate-limit').val(); - localStorage.input = $('#input').val(); - localStorage.prefixes = $('#prefixes').val(); - localStorage.suffixes = $('#suffixes').val(); -} - -/** reset inputs and results, but not settings **/ -function reset(){ - table.clear(); - table.draw(); - $('#input').val(''); - saveSettings(); -} - -function kws2Init(){ - setUpDb(); - - window.setInterval(DoJob, 750); - // - // $('#startjob').on('click',StartJob); - // $('#reset').on('click',reset); - // $('#load-from-cache').on('click',loadFromDB); - - loadSettings(); - table = $('#outtable').DataTable({ - pageLength: 50, - "lengthMenu": [ 10, 25, 50, 75, 100,800], - dom: - "<'row'<'col-sm-5'B><'col-sm-7'<'pull-right'p>>>" + - "<'row'<'col-sm-8'i><'col-sm-4'<'pull-right'f>>>" + - "<'row'<'col-sm-12'tr>>", - buttons: [ - 'colvis', - 'pageLength', - { - extend: 'collection', - text: 'Export', - buttons: [ - 'copyHtml5', - 'csvHtml5', - ] + function escapeHtml(unsafe) { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); } - // { - // extend: 'csvHtml5', - // fieldBoundary: "", - // text: 'Copy keywords', - // // 'customize': function(data,options){return data.split('\n').join(',');}, - // header: false, - // exportOptions: { - // stripNewlines: true, - // stripHtml: true, - // decodeEntities: true, - // columns: 1, - // // format:{ - // // body: function(html,i){console.log(html);return html} - // // } - // } - // }, - ], - "columnDefs": [ - { - "name": "id", - "targets": 0, - "visible": false, - }, { - "responsivePriority": 1, - "name": "keyword", - "targets": 1 - }, { - "name": "length", - "targets": 2, - "visible": false, - }, { - "name": "volume", - "targets": 3, - "visible": false, - }, { - "name": "cpc", - "targets": 4, - "visible": false, - }, { - "name": "search", - "responsivePriority": 3, - "targets": 5, - "visible": false, - }, { - "name": "domain", - "responsivePriority": 2, - "targets": 6, - "visible": false, - }], - order: [[ 0, 'desc' ]], - // colReorder: {}, - stateSave: true, - fixedHeader: true, - // responsive: true, - // scrollY: 500, - // deferRender: true, - // scroller: true - }); - $.getJSON('https://api.ipify.org?format=json', function (data) { - myIp = data.ip; - }); + // google returns lowercase + input = input.toLowerCase(); + input = escapeHtml(input); -} + // not sure if we want to trim, the search engines are sensitive to + // whitespace. (and also last search) + // input = input.trim(); + + // this removes navigation suggestions + if (input.length > 4 && input.substring(0, 4) == "http") input = ""; + return input; + }, + + /** TODO get this working **/ + Filter: function(listToFilter) { + var retList = listToFilter; + + if ($("#filter-positive").val().length > 0) { + var filteredList = []; + var filterContains = $("#filter-positive").val().split("\n"); + for (var i = 0; i < retList.length; i++) { + var currentKeyword = retList[i]; + var boolContainsKeyword = false; + for (var j = 0; j < filterContains.length; j++) { + if (filterContains[j].length > 0) { + if (currentKeyword.indexOf(filterContains[j]) != -1) { + boolContainsKeyword = true; + break; + } + } + } + + if (boolContainsKeyword) { + filteredList[filteredList.length] = currentKeyword; + } + } + + retList = filteredList; + } + + if ($("#filter-negative").val().length > 0) { + var filteredList = []; + var filterContains = $("#filter-negative").val().split("\n"); + for (var l = 0; l < retList.length; l++) { + var currentKeyword = retList[l]; + var boolCleanKeyword = true; + for (var k = 0; k < filterContains.length; k++) { + if (filterContains[k].length > 0) { + if (currentKeyword.indexOf(filterContains[k]) >= 0) { + boolCleanKeyword = false; + break; + } + } + } + + if (boolCleanKeyword) { + filteredList[filteredList.length] = currentKeyword; + } + } + + retList = filteredList; + } + + return retList; + }, + + /** display the queue, and update description of it **/ + FilterAndDisplay: function() { + var i = 0; + var sb = ''; + + var outputKeywords = this.keywordsToQuery; + for (i = 0; i < Math.min(outputKeywords.length,this.options.maxQueueDisplay); i++) { + sb += outputKeywords[i]; + sb += '\n'; + } + if (outputKeywords.length>this.options.maxQueueDisplay) sb+='...\n'; + $("#input").val(sb); + $("#numofkeywords").html('Queue:' + outputKeywords.length); + }, + + + /** overrides default with dom options with arguments options **/ + getOptions: function(argOptions){ + var defaultOptions={ + keepRunning: false, + maxQueueDisplay: 10000, + country: "", + filterNegative: "", + filterPositive: "", + lang: "", + prefixes: [" ", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "y", "x", "y", "z", "how", "which", "why", "where", "who", "when", "are", "what"], + rateLimit: 750, + service: "google", + suffixes: [" ", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "y", "x", "y", "z", "like", "for", "without", "with", "versus", "vs", "to", "near", "except", "has"] + }; // for now defaults are set in html + if (argOptions===undefined) argOptions={}; + return _.defaults(argOptions,this.getDomOptions(),defaultOptions); + }, + + /** read settings from webpage **/ + getDomOptions: function(){ + + var service= $('#service').val(), + filterNegative = $('#filter-negative').val(), + filterPositive = $('#filter-positive').val(), + rateLimit = parseInt($('#rate-limit').val()), + // input: $('#input').val(), + prefixes = $('#prefixes').val(), + suffixes = $('#suffixes').val(), + country = $('#country').val(), + lang = $('#lang').val(), + keepRunning = $('#keep-running').prop('checked'); + if (prefixes && prefixes.length) + prefixes=prefixes.split(','); + else + prefixes=undefined; + if (suffixes && suffixes.length) + suffixes=suffixes.split(','); + else + suffixes=undefined; + + var options={}; + if (service) options.service=service; + if (filterNegative) options.filterNegative=filterNegative; + if (filterPositive) ooptions.filterPositive=filterPositive; + if (rateLimit) options.rateLimit=rateLimit; + if (prefixes) options.prefixes=prefixes; + if (suffixes) options.suffixes=suffixes; + if (country) options.country=country; + if (lang) options.lang=lang; + if (keepRunning) options.keepRunning=keepRunning; + return options; + }, + + /** load settings from localStorage **/ + loadSettings: function(){ + // TODO do table settings as well, e.g. column visibilitity + if (localStorage.service) $("#service").val( localStorage.service ); + if (localStorage.country) $('#country').val(localStorage.country); + if (localStorage.lang) $('#lang').val(localStorage.lang); + if (localStorage.filterNegative) $("#filter-negative").val( localStorage.filterNegative ); + if (localStorage.filterPositive) $("#filter-positive").val( localStorage.filterPositive ); + if (localStorage.rateLimit) $("#rate-limit").val( localStorage.rateLimit ); + if (localStorage.input) $("#input").val( localStorage.input ); + if (localStorage.prefixes) $("#prefixes").val( localStorage.prefixes ); + if (localStorage.suffixes) $("#suffixes").val( localStorage.suffixes ); + if (localStorage.keepRunning) $('#keep-running').prop('checked',localStorage.keepRunning=="true"); + + }, + /** save settings to localStorage. **/ + saveSettings: function(){ + localStorage.service = $('#service').val(); + localStorage.country = $('#country').val(); + localStorage.lang = $('#lang').val(); + localStorage.filterNegative = $('#filter-negative').val(); + localStorage.filterPositive = $('#filter-positive').val(); + localStorage.rateLimit = $('#rate-limit').val(); + localStorage.input = $('#input').val(); + localStorage.prefixes = $('#prefixes').val(); + localStorage.suffixes = $('#suffixes').val(); + localStorage.keepRunning = $('#keep-running').prop('checked'); + }, + + /** reset inputs and results, but not settings **/ + reset: function(){ + this.table.clear(); + this.table.draw(); + $('#input').val(''); + this.saveSettings(); + }, + + init: function(){ + this.setUpDb(); + this.loadSettings(); + this.options = this.getOptions(); + + window.setInterval(this.DoJob.bind(this), this.options.rateLimit); + + $('#startjob').on('click',this.toggleWork.bind(this)); + $('#reset').on('click',this.reset.bind(this)); + $('#load-from-cache').on('click',this.loadFromDB.bind(this)); + $('#export-from-cache').on('click',this.exportDB.bind(this)); + // $('#filter-positive').on('click',this.FilterIfNotWorking.bind(this)); + // $('#filter-negative').on('click',this.FilterIfNotWorking.bind(this)); + + + this.table = $('#outtable').DataTable({ + pageLength: 50, + "lengthMenu": [ 10, 25, 50, 75, 100,800], + dom: + "<'row'<'col-sm-5'B><'col-sm-7'<'pull-right'p>>>" + + "<'row'<'col-sm-8'i><'col-sm-4'<'pull-right'f>>>" + + "<'row'<'col-sm-12'tr>>", + buttons: [ + 'colvis', + 'pageLength', + { + extend: 'collection', + text: 'Export', + buttons: [ + 'copyHtml5', + 'csvHtml5', + ] + } + // { + // extend: 'csvHtml5', + // fieldBoundary: "", + // text: 'Copy keywords', + // // 'customize': function(data,options){return data.split('\n').join(',');}, + // header: false, + // exportOptions: { + // stripNewlines: true, + // stripHtml: true, + // decodeEntities: true, + // columns: 1, + // // format:{ + // // body: function(html,i){console.log(html);return html} + // // } + // } + // }, + ], + "columnDefs": [ + { + "name": "id", + "targets": 0, + "visible": false, + }, { + "responsivePriority": 1, + "name": "keyword", + "targets": 1 + }, { + "name": "length", + "targets": 2, + "visible": false, + }, { + "name": "volume", + "targets": 3, + "visible": false, + }, { + "name": "cpc", + "targets": 4, + "visible": false, + }, { + "name": "search", + "responsivePriority": 3, + "targets": 5, + "visible": false, + }, { + "name": "domain", + "responsivePriority": 2, + "targets": 6, + "visible": false, + }], + order: [[ 0, 'desc' ]], + // colReorder: {}, + stateSave: true, + fixedHeader: true, + // responsive: true, + // scrollY: 500, + // deferRender: true, + // scroller: true + }); + + $.getJSON('https://api.ipify.org?format=json', function (data) { + this.myIp = data.ip; + }); + + } + }; +}(); diff --git a/public/js/test.js b/public/js/test.js index 59a260e..e27081a 100644 --- a/public/js/test.js +++ b/public/js/test.js @@ -1,10 +1,29 @@ var expect = chai.expect; var assert = chai.assert; -// TODO describe('permute', function(){ -// it('should correctly permute results and add to queue', function() { -// }); -// }); +// TODO +describe('permute', function(){ + before(function() { + KWS.hashMapInputs = {}; + KWS.keywordsToQuery = []; + }); + + after(function() { + KWS.hashMapInputs = {}; + KWS.keywordsToQuery = []; + }); + + it('should correctly permute results and add to queue', function() { + var retList = ['a','b','c d',' a longer one']; + KWS.permuteResultsToQueue(retList,'test'); + assert(KWS.keywordsToQuery.length>0); + assert(KWS.hashMapInputs['b']); + assert(KWS.hashMapInputs['test']); + // var options = KWS.getOptions(); + // var expectedLength = options.prefixes.length*retList.length+options.suffixes.length*retList.length + // assert(KWS.keywordsToQuery.length==expectedLength,''+expectedLength+'!='+KWS.keywordsToQuery.length); + }); +}); // TODO use localForage or most popular promise wrapper for indexedDB then test // describe('exportDB', function(){ @@ -12,7 +31,7 @@ var assert = chai.assert; describe('db', function() { it('should correctly set up db', function(done) { - var dbReq=setUpDb(); + var dbReq=KWS.setUpDb(); var oldonsuccess=dbReq.onsuccess; dbReq.onsuccess=function(event){ // override onld onsucess because it doesn't use promises @@ -33,7 +52,7 @@ describe('extractDomain', function(){ if (testData.domainUrls.hasOwnProperty(expectedDomain)) { it('should correctly extract domain from url, domain='+expectedDomain, function() { var url = testData.domainUrls[expectedDomain]; - var domain = extractDomain(url); + var domain = KWS.extractDomain(url); assert.equal(expectedDomain,domain); }); } @@ -48,7 +67,7 @@ describe('responses', function(){ it('should correctly parse test data for service='+service, function() { var res = testData.responses[service]; var options = {service:service}; - var parsedRes = parseServiceResponse(res,options); + var parsedRes = KWS.parseServiceResponse(res,options); assert.typeOf(parsedRes,'array'); expect(parsedRes).to.have.length.above(1); @@ -82,7 +101,7 @@ describe('services', function() { searches.forEach(function(search){ it('should correctly get and parse data'+'. Service="'+service+'" search="'+search+'"', function(done) { var options = {service:service}; - var url = getUrl(options)+search; + var url = KWS.getUrl(options)+search; return $.ajax({ url: url, async: false, @@ -91,7 +110,7 @@ describe('services', function() { success: function (res, statusText, jqXHR) { assert(statusText=="success"); assert(res!==[]); - var parsedRes = parseServiceResponse(res,options); + var parsedRes = KWS.parseServiceResponse(res,options); assert.typeOf(parsedRes,'array'); parsedRes.map(r => assert.typeOf(r, 'string')); done(); @@ -110,7 +129,7 @@ describe('services', function() { searchesDifficult.forEach(function(search){ it('should get and parse difficult data'+'. Service="'+service+'" search="'+search+'"', function(done) { var options = {service:service}; - var url = getUrl(options)+search; + var url = KWS.getUrl(options)+search; return $.ajax({ url: url, async: false, @@ -120,7 +139,7 @@ describe('services', function() { assert(statusText=="success"); assert(res!==[]); - var parsedRes = parseServiceResponse(res,options); + var parsedRes = KWS.parseServiceResponse(res,options); assert.typeOf(parsedRes,'array'); parsedRes.map(r => assert.typeOf(r, 'string')); return done(); From e3e832e9baff7cb45a4643abf0705f1fca2a8647 Mon Sep 17 00:00:00 2001 From: Is Isilon Date: Mon, 15 Feb 2016 16:06:12 +0800 Subject: [PATCH 7/9] Table inputs are now object now arrays --- .gitignore | 2 + public/index.html | 48 +++++++++------- public/js/main.js | 135 +++++++++++++++++++++++++++++---------------- src/less/main.less | 2 +- 4 files changed, 118 insertions(+), 69 deletions(-) diff --git a/.gitignore b/.gitignore index 58c3d86..e1174fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ +notes + # Created by https://www.gitignore.io/api/node,bower,linux ### Node ### diff --git a/public/index.html b/public/index.html index ecb2947..db41bdc 100644 --- a/public/index.html +++ b/public/index.html @@ -7,12 +7,13 @@ - + - - - - + + + + + @@ -36,16 +37,17 @@
-
+
-
+ Shit Keywords! @@ -58,11 +60,14 @@ lying internet Reset - +
+

- Load cache - Export cache + Load cache + Export cache + + Delete cache
@@ -340,7 +345,7 @@ lying internet
- +
-
+ -
+ -
+
@@ -433,15 +438,16 @@ lying internet
-
+
+
-
+

Results

-
id Volume CPC SearchSource
+
@@ -470,6 +476,8 @@ lying internet + + diff --git a/public/js/main.js b/public/js/main.js index 6c997f7..4701e75 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -173,7 +173,10 @@ var KWS = function(){ this.FilterAndDisplay(); this.doWork = true; + this.progress1.start(); + // $('#input').hide(); + // $('#advanced').collapse("hide"); } else { @@ -189,6 +192,7 @@ var KWS = function(){ this.table.columns.adjust(); this.saveSettings(); this.FilterAndDisplay(); + this.progress1.end(); } }, @@ -205,6 +209,10 @@ var KWS = function(){ this.DoJob(); } + var prog = parseInt(this.keywordsToQueryIndex/this.numOfInitialKeywords*100); + this.progress1.set(prog); + this.FilterAndDisplay(); + } else { if (this.options.keepRunning) { console.log('finish initial queue'); @@ -267,7 +275,7 @@ var KWS = function(){ }, []); // add to queue - this.keywordsToQuery.push.apply(this.keywordsToQuery,newInputs); + this.keywordsToQuery=_.concat(this.keywordsToQuery,newInputs); return newInputs; }, @@ -285,13 +293,20 @@ var KWS = function(){ var timeStamp = (new Date()).toISOString().replace(/[:\.]/g,'_').slice(0,-5); name = 'keywordshitter_'+timeStamp+'_r'+e.target.result.length+'.json'; saveAs(blob,name); - if (success) success(blob); + if (success instanceof Function) success(blob); } return blob; }; return reqObj; }, + clearDB: function(){ + this.db.transaction(["suggestions"], "readwrite") + .objectStore("suggestions") + .clear(); + console.warn('cleared all indexedDB data'); + }, + /** Display data from db upon pressing load button **/ loadFromDB: function(){ var self = this; @@ -303,19 +318,10 @@ var KWS = function(){ /// grab the fields we want var data =[]; for (var i = 0; i < e.target.result.length; i++) { - var d = e.target.result[i]; - var da = [ - d.id, - d.keyword, - d.length, - d.volume, - d.cpc, - d.search, - d.domain - ]; + var da = e.target.result[i]; // also remove undefined so datatables doesn't bring up alerts - da = da.map(function(v){return v===undefined ? null: v;}); + da = _.mapValues(da, function(v){return v===undefined ? null: v;}); // parse nums // da = da.map(v => /^[\d+\.]+$/.test(v) ? Number(v): v); @@ -339,19 +345,28 @@ var KWS = function(){ // url might be in retlist if (url===undefined) url=data[i].url; - - var da = [ - this.table.rows()[0].length+i, - cleanKw, - cleanKw.length, - null, - null, - search, - this.extractDomain(url) - ]; + // + // var da = [ + // this.table.rows()[0].length+i, + // cleanKw, + // cleanKw.length, + // null, + // null, + // search, + // this.extractDomain(url) + // ]; + var da = { + id: this.table.rows()[0].length+i, + keyword: cleanKw, + length: cleanKw.length, + volume: null, + cpc: null, + search: search, + domain: this.extractDomain(url) + }; // remove undefined values to avoid datatable alerts - da = da.map(function(v){return v===undefined ? null: v;}); + da = _.mapValues(da, function(v){return v===undefined ? null: v;}); // TODO Check if suggestion is already displayed before adding // var matches = table.data().filter(function(v){return v[1]===cleanKw && v[5]==search;}).count(); @@ -359,7 +374,8 @@ var KWS = function(){ rows.push(da); } this.table.rows.add(rows); - if (!dontDisplay) this.table.draw(false); + // if table is large lets defer rending to end to speed it up + if (!dontDisplay && this.table.data().length<'col-sm-7'<'pull-right'p>>>" + @@ -734,53 +756,64 @@ var KWS = function(){ buttons: [ 'copyHtml5', 'csvHtml5', + { + extend: 'csvHtml5', + fieldBoundary: "", + text: 'Copy keywords', + // 'customize': function(data,options){return data.split('\n').join(',');}, + header: false, + exportOptions: { + stripNewlines: true, + stripHtml: true, + decodeEntities: true, + columns: 1, + // format:{ + // body: function(html,i){console.log(html);return html} + // } + } + }, ] - } - // { - // extend: 'csvHtml5', - // fieldBoundary: "", - // text: 'Copy keywords', - // // 'customize': function(data,options){return data.split('\n').join(',');}, - // header: false, - // exportOptions: { - // stripNewlines: true, - // stripHtml: true, - // decodeEntities: true, - // columns: 1, - // // format:{ - // // body: function(html,i){console.log(html);return html} - // // } - // } - // }, + }, + ], "columnDefs": [ { "name": "id", + "data": "id", "targets": 0, "visible": false, }, { - "responsivePriority": 1, "name": "keyword", - "targets": 1 + "data": "keyword", + "responsivePriority": 1, + "targets": 1, }, { "name": "length", + "data": "length", "targets": 2, "visible": false, + "type": "num" }, { "name": "volume", + "data": "volume", "targets": 3, "visible": false, + "type": "num" }, { "name": "cpc", + "data": "cpc", "targets": 4, "visible": false, + "type": "num" }, { "name": "search", + "data": "search", "responsivePriority": 3, "targets": 5, "visible": false, }, { "name": "domain", + "data": "domain", "responsivePriority": 2, "targets": 6, "visible": false, @@ -788,8 +821,14 @@ var KWS = function(){ order: [[ 0, 'desc' ]], // colReorder: {}, stateSave: true, - fixedHeader: true, - // responsive: true, + "bDeferRender": true, + // fixedHeader: true, + // responsive: { + // details: { + // type: 'column', + // target: 'tr' + // }, + // }, // scrollY: 500, // deferRender: true, // scroller: true diff --git a/src/less/main.less b/src/less/main.less index e0046aa..befb033 100644 --- a/src/less/main.less +++ b/src/less/main.less @@ -12,7 +12,7 @@ // clean minimalist tables .table.dataTable { - min-height: 150px; + min-height: 600px; } .table { // Cells From 6bdc01b309c6bfc1039cc01a944eec80c29bea82 Mon Sep 17 00:00:00 2001 From: Is Isilon Date: Mon, 15 Feb 2016 16:13:16 +0800 Subject: [PATCH 8/9] Added words column --- public/index.html | 1 + public/js/main.js | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/public/index.html b/public/index.html index db41bdc..f31b0db 100644 --- a/public/index.html +++ b/public/index.html @@ -457,6 +457,7 @@ lying internet + diff --git a/public/js/main.js b/public/js/main.js index 4701e75..ee5653d 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -359,6 +359,7 @@ var KWS = function(){ id: this.table.rows()[0].length+i, keyword: cleanKw, length: cleanKw.length, + words: cleanKw.trim().split(/ +/).length, volume: null, cpc: null, search: search, @@ -432,11 +433,12 @@ var KWS = function(){ addReq = store.add({ keyword: cleanKw, length: cleanKw.length, + words: cleanKw.trim().split(/ +/).length, search: search, ip: this.myIp, url: url, domain: self.extractDomain(url), - time: (new Date()).toUTCString() + time: (new Date()).toUTCString(), }); addReq.onsuccess = addNext; @@ -817,7 +819,14 @@ var KWS = function(){ "responsivePriority": 2, "targets": 6, "visible": false, - }], + }, { + "name": "words", + "data": "words", + "targets": 7, + "visible": false, + "type": "num" + }, + ], order: [[ 0, 'desc' ]], // colReorder: {}, stateSave: true, From 5af44075addd6242fabccbe2bab104d18b39f1e4 Mon Sep 17 00:00:00 2001 From: Is Isilon Date: Fri, 19 Feb 2016 10:01:29 +0800 Subject: [PATCH 9/9] Dynamic table titles --- public/index.html | 4 ++-- public/js/main.js | 39 ++++++++++++--------------------------- 2 files changed, 14 insertions(+), 29 deletions(-) diff --git a/public/index.html b/public/index.html index f31b0db..f77934d 100644 --- a/public/index.html +++ b/public/index.html @@ -450,14 +450,14 @@ lying internet
idCPC Search SourceWords
- + diff --git a/public/js/main.js b/public/js/main.js index ee5653d..21d37b8 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -47,7 +47,7 @@ var KWS = function(){ "twitter": "https://twitter.com/i/search/typeahead.json?count=30&result_type=topics&src=SEARCH_BOX&callback=?&q=", "baidu": "http://suggestion.baidu.com/su?cb=?&wd=", - "yandex": "https://yandex.com/suggest/suggest-ya.cgi?callback=?&q=?&n=30&v=4&uil={lang}&part=" + "yandex": "https://yandex.com/suggest/suggest-ya.cgi?callback=?&q=?&n=30&v=4&uil={lang}&part=", }; return _.template(services[this.options.service])(this.options); @@ -78,7 +78,7 @@ var KWS = function(){ return _.map(res[1], function(r){ return typeof r === 'string' ? r : r[1]; }); - }, + } }; var parser = RESPONSE_TEMPLATES[this.options.service] || RESPONSE_TEMPLATES["default"]; return parser(res); @@ -247,7 +247,7 @@ var KWS = function(){ this.hashMapInputs[search] = true; - // sort so the shortest is first in the queue + // sort so the shortest is first in the queue TODO add option? // retList.sort(function (a, b) { // return a.length - b.length; // }); @@ -261,10 +261,6 @@ var KWS = function(){ // clean retList=retList.map(this.CleanVal); - - - - // get permutations var newInputs = retList.reduce(function(result, keyword){ return _.concat( @@ -345,16 +341,7 @@ var KWS = function(){ // url might be in retlist if (url===undefined) url=data[i].url; - // - // var da = [ - // this.table.rows()[0].length+i, - // cleanKw, - // cleanKw.length, - // null, - // null, - // search, - // this.extractDomain(url) - // ]; + var da = { id: this.table.rows()[0].length+i, keyword: cleanKw, @@ -738,8 +725,6 @@ var KWS = function(){ $('#load-from-cache').on('click',this.loadFromDB.bind(this)); $('#export-from-cache').on('click',this.exportDB.bind(this)); $('#clear-cache').on('click',this.clearDB.bind(this)); - // $('#filter-positive').on('click',this.FilterIfNotWorking.bind(this)); - // $('#filter-negative').on('click',this.FilterIfNotWorking.bind(this)); this.table = $('#outtable').DataTable({ @@ -768,7 +753,7 @@ var KWS = function(){ stripNewlines: true, stripHtml: true, decodeEntities: true, - columns: 1, + columns: 'keyword', // format:{ // body: function(html,i){console.log(html);return html} // } @@ -780,17 +765,17 @@ var KWS = function(){ ], "columnDefs": [ { - "name": "id", + "title": "id", "data": "id", "targets": 0, "visible": false, }, { - "name": "keyword", + "title": "Keyword", "data": "keyword", "responsivePriority": 1, "targets": 1, }, { - "name": "length", + "title": "Length", "data": "length", "targets": 2, "visible": false, @@ -802,25 +787,25 @@ var KWS = function(){ "visible": false, "type": "num" }, { - "name": "cpc", + "title": "CPC", "data": "cpc", "targets": 4, "visible": false, "type": "num" }, { - "name": "search", + "title": "Search", "data": "search", "responsivePriority": 3, "targets": 5, "visible": false, }, { - "name": "domain", + "title": "Domain", "data": "domain", "responsivePriority": 2, "targets": 6, "visible": false, }, { - "name": "words", + "title": "Words", "data": "words", "targets": 7, "visible": false,
id