Front end with docs and analytics

James Peret 10 years ago
parent
commit
c12cea5af0

BIN
.DS_Store


+ 1 - 0
Gemfile

@@ -9,6 +9,7 @@ gem 'haml'
9 9
 gem 'json'
10 10
 gem 'rack-cors', :require => 'rack/cors'
11 11
 gem 'unirest'
12
+gem 'mixpanel-ruby'
12 13
 
13 14
 group :test, :development do
14 15
   gem 'pry'

+ 2 - 0
Gemfile.lock

@@ -14,6 +14,7 @@ GEM
14 14
     kgio (2.7.4)
15 15
     method_source (0.8.2)
16 16
     mime-types (2.6.1)
17
+    mixpanel-ruby (2.1.0)
17 18
     netrc (0.10.3)
18 19
     pry (0.10.1)
19 20
       coderay (~> 1.1.0)
@@ -63,6 +64,7 @@ PLATFORMS
63 64
 DEPENDENCIES
64 65
   haml
65 66
   json
67
+  mixpanel-ruby
66 68
   pry
67 69
   rack-cors
68 70
   rb-readline

+ 18 - 0
analytics.rb

@@ -0,0 +1,18 @@
1
+require 'mixpanel-ruby'
2
+
3
+class Analytics
4
+
5
+  def self.track(user_id, event_name, url = '')
6
+    tracker = Mixpanel::Tracker.new('8f581bbd244b7ee0f4762d322ac617e8')
7
+
8
+    # Tracks an event, 'Sent Message',
9
+    # with distinct_id user_id
10
+    if url != ''
11
+      tracker.track(user_id, event_name, { 'lookup_url' => url})
12
+    else
13
+      tracker.track(user_id, event_name)
14
+    end
15
+
16
+  end
17
+
18
+end

+ 1 - 1
json_parser.rb

@@ -56,7 +56,7 @@ class WhoisParser
56 56
       # Owner
57 57
       owner_data = whois_lookup.to_s.scan(/Registrant\sName:\s([^\\]+)/).first
58 58
       if owner_data != nil
59
-        owner_data = spliter(owner_data, "\n").downcase.split.map(&:capitalize).join(' ')
59
+        owner_data = spliter(owner_data, "\r").downcase.split.map(&:capitalize).join(' ')
60 60
         if owner_data
61 61
           @parsed_data["owner"] = owner_data
62 62
         end

+ 6 - 38
main.rb

@@ -5,8 +5,10 @@ require 'json'
5 5
 require 'ostruct'
6 6
 require "sinatra/reloader" if development?
7 7
 
8
+
8 9
 require_relative 'json_parser'
9 10
 require_relative 'unirest_parser'
11
+require_relative 'analytics'
10 12
 
11 13
 # ruby crimes
12 14
 class Whois::Record
@@ -61,46 +63,12 @@ set :views, File.dirname(__FILE__) + '/views'
61 63
 set :public_folder, File.dirname(__FILE__) + '/public'
62 64
 
63 65
 get '/' do
64
-  #cache_for_day
66
+  Analytics.track(request.ip, 'Page View')
65 67
   File.read(File.join('public', 'index.html'))
66 68
 end
67 69
 
68
-get '/lookup' do
69
-  begin
70
-    cache_for_day
71
-    @whois = whois_lookup
72
-    haml :lookup
73
-  rescue Exception => e
74
-    @error = e
75
-    haml :error
76
-  end
77
-end
78
-
79
-get '/lookup.json' do
80
-  content_type 'application/json'
81
-  response["Content-Type"] = "application/json"
82
-  headers  'Access-Control-Allow-Origin' => '*',
83
-           'Access-Control-Allow-Methods' => ['OPTIONS', 'GET', 'POST']
84
-  begin
85
-    #cache_for_day
86
-    parser = WhoisParser.new
87
-    if whois_lookup.class != Array
88
-      data = whois_lookup.to_h
89
-    else
90
-      if whois_lookup.first.class != Array
91
-        data = whois_lookup.first.to_h
92
-      else
93
-        data = ""
94
-      end
95
-    end
96
-    parser.parse(data, whois_lookup, params[:url], params[:raw], params[:dev])
97
-  rescue Exception => e
98
-    @error = e
99
-    { :error => @error }.to_json
100
-  end
101
-end
102
-
103
-get '/:url' do
70
+get '/:lookup_url' do
71
+  Analytics.track(request.ip, 'Domain Lookup', params[:lookup_url])
104 72
   content_type 'application/json'
105 73
   response["Content-Type"] = "application/json"
106 74
   headers  'Access-Control-Allow-Origin' => '*',
@@ -118,7 +86,7 @@ get '/:url' do
118 86
         data = ""
119 87
       end
120 88
     end
121
-    parser.parse(data, whois_lookup, params[:url], params[:raw], params[:dev])
89
+    parser.parse(data, whois_lookup, params[:lookup_url], params[:raw], params[:dev])
122 90
   rescue Exception => e
123 91
     @error = e
124 92
     { :error => @error }.to_json

BIN
public/.DS_Store


+ 39 - 0
public/css/sticky-footer-navbar.css

@@ -0,0 +1,39 @@
1
+/* Sticky footer styles
2
+-------------------------------------------------- */
3
+html {
4
+  position: relative;
5
+  min-height: 100%;
6
+}
7
+body {
8
+  /* Margin bottom by footer height */
9
+  margin-bottom: 60px;
10
+}
11
+.footer {
12
+  position: absolute;
13
+  bottom: 0;
14
+  width: 100%;
15
+  /* Set the fixed height of the footer here */
16
+  height: 60px;
17
+  background-color: #f5f5f5;
18
+}
19
+
20
+
21
+/* Custom page CSS
22
+-------------------------------------------------- */
23
+/* Not required for template or sticky footer method. */
24
+
25
+body > .container {
26
+  padding: 60px 15px 0;
27
+}
28
+.container .text-muted {
29
+  margin: 20px 0;
30
+}
31
+
32
+.footer > .container {
33
+  padding-right: 15px;
34
+  padding-left: 15px;
35
+}
36
+
37
+code {
38
+  font-size: 80%;
39
+}

+ 17 - 3
public/css/styles.css

@@ -11,9 +11,6 @@ body {
11 11
   background: #dedede;
12 12
 }
13 13
 
14
-p {
15
-  margin: 0;
16
-}
17 14
 
18 15
 .info {
19 16
   width: 800px;
@@ -52,3 +49,20 @@ p.title {
52 49
   float: left;
53 50
   margin-left: 15px;
54 51
 }
52
+
53
+h1 { margin-bottom: 40px;}
54
+h2 { margin-bottom: 20px; margin-top: 40px}
55
+h4 { margin-bottom: 15px; margin-top: 25px}
56
+
57
+.markdown-page {
58
+  margin-bottom: 70px;
59
+}
60
+
61
+pre {
62
+  padding: 0;
63
+}
64
+
65
+code {
66
+  color: white;
67
+  background-color: #2F2F2F;
68
+}

+ 44 - 0
public/css/tomorrow-night-eighties.css

@@ -0,0 +1,44 @@
1
+/* Tomorrow Night Eighties Theme */
2
+/* Original theme - https://github.com/chriskempson/tomorrow-theme */
3
+/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
4
+.tomorrow-comment, pre .comment, pre .title {
5
+  color: #999999;
6
+}
7
+
8
+.tomorrow-red, pre .variable,  pre .regexp, pre .ruby .constant,  pre .xml .pi, pre .xml .doctype, pre .html .doctype, pre .css .id, pre .css .class, pre .css .pseudo {
9
+  color: #f2777a;
10
+}
11
+
12
+.tomorrow-orange, pre .number, pre .preprocessor, pre .built_in, pre .literal, pre .params, pre .constant, pre .ruby .function .title, pre .ruby .title .keyword, pre .keyword {
13
+  color: #D58B49;
14
+}
15
+
16
+.tomorrow-yellow, pre .class, pre .ruby .class .title, pre .css .rules .attribute, pre .attribute, pre .tag, pre .xml .tag .title {
17
+  color: #ECC982;
18
+}
19
+
20
+.tomorrow-green, pre .string, pre .value, pre .inheritance, pre .header, pre .xml .cdata {
21
+  color: #B4C978;
22
+}
23
+
24
+.tomorrow-aqua, pre .css .hexcolor {
25
+  color: #66cccc;
26
+}
27
+
28
+.tomorrow-blue, pre .function, pre .python .decorator, pre .python .title, pre .ruby .symbol, pre .perl .sub, pre .javascript .title, pre .coffeescript .title {
29
+  color: #7AA3BC;
30
+}
31
+
32
+.tomorrow-purple, pre .javascript .function {
33
+  color: #cc99cc;
34
+}
35
+
36
+pre code {
37
+  display: block;
38
+  background: #2F2F2F;
39
+  color: #cccccc;
40
+  font-family: Menlo, Monaco, Consolas, monospace;
41
+  line-height: 1.5;
42
+  border: 1px solid #ccc;
43
+  padding: 10px;
44
+}

+ 48 - 43
public/index.html

@@ -7,73 +7,78 @@
7 7
     <meta name="viewport" content="width=device-width">
8 8
 
9 9
     <link rel="stylesheet" href="css/bootstrap.min.css" />
10
+    <link rel="stylesheet" href="css/sticky-footer-navbar.css" />
10 11
     <link rel="stylesheet" href="css/styles.css">
12
+    <link href="css//tomorrow-night-eighties.css?body=1" media="all" rel="stylesheet">
13
+
14
+
15
+
11 16
 
12 17
   </head>
13
-  <body ng-app="domainManagerApp">
18
+  <body ng-app="WhoisAPI">
14 19
     <!--[if lt IE 7]>
15 20
       <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
16 21
     <![endif]-->
17 22
 
18
-    <!-- Add your site or application content here -->
19
-    <div class="header">
20
-      <div class="navbar navbar-default" role="navigation">
21
-        <div class="container">
22
-          <div class="navbar-header">
23
-
24
-            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#js-navbar-collapse">
25
-              <span class="sr-only">Toggle navigation</span>
26
-              <span class="icon-bar"></span>
27
-              <span class="icon-bar"></span>
28
-              <span class="icon-bar"></span>
29
-            </button>
30
-
31
-            <a class="navbar-brand" href="#/">Whois JSON API</a>
32
-          </div>
23
+    <!-- Fixed navbar -->
24
+    <nav class="navbar navbar-default navbar-fixed-top">
25
+      <div class="container">
26
+        <div class="navbar-header">
27
+          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
28
+            <span class="sr-only">Toggle navigation</span>
29
+            <span class="icon-bar"></span>
30
+            <span class="icon-bar"></span>
31
+            <span class="icon-bar"></span>
32
+          </button>
33
+          <a class="navbar-brand" href="#/">Whois JSON API</a>
33 34
         </div>
35
+        <div id="navbar" class="collapse navbar-collapse" id="js-navbar-collapse" ng-controller="HeaderController">
36
+          <ul class="nav navbar-nav">
37
+            <li ng-class="{ active: isActive('/')}"><a href="#/">Lookup</a></li>
38
+            <li ng-class="{ active: isActive('/docs')}"><a href="#/docs">Docs</a></li>
39
+          </ul>
40
+        </div><!--/.nav-collapse -->
34 41
       </div>
35
-    </div>
42
+    </nav>
43
+
44
+
36 45
 
37 46
     <div class="container">
38
-      <div ng-controller="WhoisController">
39
-        <form class="form-horizontal" ng-submit="lookup()">
40
-            <fieldset class="domain-search centered">
41
-
42
-                <div class="domain-input">
43
-                  <div class="input-group input-group-lg">
44
-                    <span class="input-group-addon" id="basic-addon1">http://whois.j1x.co/</span>
45
-                    <input id="domainTitle" ng-model="url" name="txtTitle" type="text" placeholder="example.com" class="form-control input-md">
46
-                  </div>
47
-                </div>
48
-                <input id="singlebutton" ng-disabled="!url" name="singlebutton" class="btn btn-primary btn-lg domain-btn"type="submit"value="Search">
49
-
50
-
51
-            </fieldset>
52
-        </form>
53
-        <div ng-show="showResult"><pre><code>{{domain}}</code></pre></div>
54
-      </div>
47
+      <div ng-view></div>
55 48
     </div>
56 49
 
57
-    <div footer></div>
50
+    <footer class="footer">
51
+      <div class="container">
52
+        <p style="text-align: center" class="text-muted">Whois JSON API website created by <a href="http://jamesperet.com">James Peret</a> - &copy; <a href="http://j1x.co">J1X</a> 2015</p>
53
+      </div>
54
+    </footer>
55
+
56
+
57
+    <script src="js/lib/angular.min.js"></script>
58
+    <script src="js/lib/angular-route.min.js"></script>
59
+    <script src="js/lib/angular-sanitize.min.js"></script>
60
+    <script type="text/javascript" src="http://fgnass.github.io/spin.js/spin.min.js"></script>
61
+    <script src="js/lib/angular-spinner.js"></script>
62
+    <script src="http://pc035860.github.io/angular-highlightjs/angular-highlightjs.min.js"></script>
63
+    <script src="js/lib/showdown.min.js"></script>
64
+    <script src="js/lib/ng-showdown.js"></script>
65
+    <script src="js/lib/jquery.min.js"></script>
66
+    <script src="js/lib/bootstrap.min.js"></script>
67
+    <script src="js/scripts/app.js"></script>
68
+
58 69
 
70
+    <script src="http://yandex.st/highlightjs/7.3/highlight.min.js"></script>
59 71
 
60
-    <!-- Google Analytics: change UA-XXXXX-X to be your site's ID
61 72
      <script>
62 73
        !function(A,n,g,u,l,a,r){A.GoogleAnalyticsObject=l,A[l]=A[l]||function(){
63 74
        (A[l].q=A[l].q||[]).push(arguments)},A[l].l=+new Date,a=n.createElement(g),
64 75
        r=n.getElementsByTagName(g)[0],a.src=u,r.parentNode.insertBefore(a,r)
65 76
        }(window,document,'script','//www.google-analytics.com/analytics.js','ga');
66 77
 
67
-       ga('create', 'UA-XXXXX-X');
78
+       ga('create', 'UA-63859108-2');
68 79
        ga('send', 'pageview');
69 80
     </script>
70
-    -->
71 81
 
72 82
 
73
-    <script src="js/lib/angular.min.js"></script>
74
-    <script src="js/lib/jquery.min.js"></script>
75
-    <script src="js/lib/bootstrap.min.js"></script>
76
-    <script src="js/scripts/app.js"></script>
77
-
78 83
 </body>
79 84
 </html>

+ 15 - 0
public/js/lib/angular-route.min.js

@@ -0,0 +1,15 @@
1
+/*
2
+ AngularJS v1.3.15
3
+ (c) 2010-2014 Google, Inc. http://angularjs.org
4
+ License: MIT
5
+*/
6
+(function(q,d,C){'use strict';function v(r,k,h){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(a,f,b,c,y){function z(){l&&(h.cancel(l),l=null);m&&(m.$destroy(),m=null);n&&(l=h.leave(n),l.then(function(){l=null}),n=null)}function x(){var b=r.current&&r.current.locals;if(d.isDefined(b&&b.$template)){var b=a.$new(),c=r.current;n=y(b,function(b){h.enter(b,null,n||f).then(function(){!d.isDefined(t)||t&&!a.$eval(t)||k()});z()});m=c.scope=b;m.$emit("$viewContentLoaded");
7
+m.$eval(w)}else z()}var m,n,l,t=b.autoscroll,w=b.onload||"";a.$on("$routeChangeSuccess",x);x()}}}function A(d,k,h){return{restrict:"ECA",priority:-400,link:function(a,f){var b=h.current,c=b.locals;f.html(c.$template);var y=d(f.contents());b.controller&&(c.$scope=a,c=k(b.controller,c),b.controllerAs&&(a[b.controllerAs]=c),f.data("$ngControllerController",c),f.children().data("$ngControllerController",c));y(a)}}}q=d.module("ngRoute",["ng"]).provider("$route",function(){function r(a,f){return d.extend(Object.create(a),
8
+f)}function k(a,d){var b=d.caseInsensitiveMatch,c={originalPath:a,regexp:a},h=c.keys=[];a=a.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)([\?\*])?/g,function(a,d,b,c){a="?"===c?c:null;c="*"===c?c:null;h.push({name:b,optional:!!a});d=d||"";return""+(a?"":d)+"(?:"+(a?d:"")+(c&&"(.+?)"||"([^/]+)")+(a||"")+")"+(a||"")}).replace(/([\/$\*])/g,"\\$1");c.regexp=new RegExp("^"+a+"$",b?"i":"");return c}var h={};this.when=function(a,f){var b=d.copy(f);d.isUndefined(b.reloadOnSearch)&&(b.reloadOnSearch=!0);
9
+d.isUndefined(b.caseInsensitiveMatch)&&(b.caseInsensitiveMatch=this.caseInsensitiveMatch);h[a]=d.extend(b,a&&k(a,b));if(a){var c="/"==a[a.length-1]?a.substr(0,a.length-1):a+"/";h[c]=d.extend({redirectTo:a},k(c,b))}return this};this.caseInsensitiveMatch=!1;this.otherwise=function(a){"string"===typeof a&&(a={redirectTo:a});this.when(null,a);return this};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$templateRequest","$sce",function(a,f,b,c,k,q,x){function m(b){var e=s.current;
10
+(v=(p=l())&&e&&p.$$route===e.$$route&&d.equals(p.pathParams,e.pathParams)&&!p.reloadOnSearch&&!w)||!e&&!p||a.$broadcast("$routeChangeStart",p,e).defaultPrevented&&b&&b.preventDefault()}function n(){var u=s.current,e=p;if(v)u.params=e.params,d.copy(u.params,b),a.$broadcast("$routeUpdate",u);else if(e||u)w=!1,(s.current=e)&&e.redirectTo&&(d.isString(e.redirectTo)?f.path(t(e.redirectTo,e.params)).search(e.params).replace():f.url(e.redirectTo(e.pathParams,f.path(),f.search())).replace()),c.when(e).then(function(){if(e){var a=
11
+d.extend({},e.resolve),b,g;d.forEach(a,function(b,e){a[e]=d.isString(b)?k.get(b):k.invoke(b,null,null,e)});d.isDefined(b=e.template)?d.isFunction(b)&&(b=b(e.params)):d.isDefined(g=e.templateUrl)&&(d.isFunction(g)&&(g=g(e.params)),g=x.getTrustedResourceUrl(g),d.isDefined(g)&&(e.loadedTemplateUrl=g,b=q(g)));d.isDefined(b)&&(a.$template=b);return c.all(a)}}).then(function(c){e==s.current&&(e&&(e.locals=c,d.copy(e.params,b)),a.$broadcast("$routeChangeSuccess",e,u))},function(b){e==s.current&&a.$broadcast("$routeChangeError",
12
+e,u,b)})}function l(){var a,b;d.forEach(h,function(c,h){var g;if(g=!b){var k=f.path();g=c.keys;var m={};if(c.regexp)if(k=c.regexp.exec(k)){for(var l=1,n=k.length;l<n;++l){var p=g[l-1],q=k[l];p&&q&&(m[p.name]=q)}g=m}else g=null;else g=null;g=a=g}g&&(b=r(c,{params:d.extend({},f.search(),a),pathParams:a}),b.$$route=c)});return b||h[null]&&r(h[null],{params:{},pathParams:{}})}function t(a,b){var c=[];d.forEach((a||"").split(":"),function(a,d){if(0===d)c.push(a);else{var f=a.match(/(\w+)(?:[?*])?(.*)/),
13
+h=f[1];c.push(b[h]);c.push(f[2]||"");delete b[h]}});return c.join("")}var w=!1,p,v,s={routes:h,reload:function(){w=!0;a.$evalAsync(function(){m();n()})},updateParams:function(a){if(this.current&&this.current.$$route)a=d.extend({},this.current.params,a),f.path(t(this.current.$$route.originalPath,a)),f.search(a);else throw B("norout");}};a.$on("$locationChangeStart",m);a.$on("$locationChangeSuccess",n);return s}]});var B=d.$$minErr("ngRoute");q.provider("$routeParams",function(){this.$get=function(){return{}}});
14
+q.directive("ngView",v);q.directive("ngView",A);v.$inject=["$route","$anchorScroll","$animate"];A.$inject=["$compile","$controller","$route"]})(window,window.angular);
15
+//# sourceMappingURL=angular-route.min.js.map

+ 16 - 0
public/js/lib/angular-sanitize.min.js

@@ -0,0 +1,16 @@
1
+/*
2
+ AngularJS v1.4.1-build.4042+sha.071be60
3
+ (c) 2010-2015 Google, Inc. http://angularjs.org
4
+ License: MIT
5
+*/
6
+(function(n,h,p){'use strict';function E(a){var f=[];r(f,h.noop).chars(a);return f.join("")}function g(a,f){var d={},c=a.split(","),b;for(b=0;b<c.length;b++)d[f?h.lowercase(c[b]):c[b]]=!0;return d}function F(a,f){function d(a,b,d,l){b=h.lowercase(b);if(s[b])for(;e.last()&&t[e.last()];)c("",e.last());u[b]&&e.last()==b&&c("",b);(l=v[b]||!!l)||e.push(b);var m={};d.replace(G,function(b,a,f,c,d){m[a]=q(f||c||d||"")});f.start&&f.start(b,m,l)}function c(b,a){var c=0,d;if(a=h.lowercase(a))for(c=e.length-
7
+1;0<=c&&e[c]!=a;c--);if(0<=c){for(d=e.length-1;d>=c;d--)f.end&&f.end(e[d]);e.length=c}}"string"!==typeof a&&(a=null===a||"undefined"===typeof a?"":""+a);var b,k,e=[],m=a,l;for(e.last=function(){return e[e.length-1]};a;){l="";k=!0;if(e.last()&&w[e.last()])a=a.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*"+e.last()+"[^>]*>","i"),function(a,b){b=b.replace(H,"$1").replace(I,"$1");f.chars&&f.chars(q(b));return""}),c("",e.last());else{if(0===a.indexOf("\x3c!--"))b=a.indexOf("--",4),0<=b&&a.lastIndexOf("--\x3e",
8
+b)===b&&(f.comment&&f.comment(a.substring(4,b)),a=a.substring(b+3),k=!1);else if(x.test(a)){if(b=a.match(x))a=a.replace(b[0],""),k=!1}else if(J.test(a)){if(b=a.match(y))a=a.substring(b[0].length),b[0].replace(y,c),k=!1}else K.test(a)&&((b=a.match(z))?(b[4]&&(a=a.substring(b[0].length),b[0].replace(z,d)),k=!1):(l+="<",a=a.substring(1)));k&&(b=a.indexOf("<"),l+=0>b?a:a.substring(0,b),a=0>b?"":a.substring(b),f.chars&&f.chars(q(l)))}if(a==m)throw L("badparse",a);m=a}c()}function q(a){if(!a)return"";A.innerHTML=
9
+a.replace(/</g,"&lt;");return A.textContent}function B(a){return a.replace(/&/g,"&amp;").replace(M,function(a){var d=a.charCodeAt(0);a=a.charCodeAt(1);return"&#"+(1024*(d-55296)+(a-56320)+65536)+";"}).replace(N,function(a){return"&#"+a.charCodeAt(0)+";"}).replace(/</g,"&lt;").replace(/>/g,"&gt;")}function r(a,f){var d=!1,c=h.bind(a,a.push);return{start:function(a,k,e){a=h.lowercase(a);!d&&w[a]&&(d=a);d||!0!==C[a]||(c("<"),c(a),h.forEach(k,function(d,e){var k=h.lowercase(e),g="img"===a&&"src"===k||
10
+"background"===k;!0!==O[k]||!0===D[k]&&!f(d,g)||(c(" "),c(e),c('="'),c(B(d)),c('"'))}),c(e?"/>":">"))},end:function(a){a=h.lowercase(a);d||!0!==C[a]||(c("</"),c(a),c(">"));a==d&&(d=!1)},chars:function(a){d||c(B(a))}}}var L=h.$$minErr("$sanitize"),z=/^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,y=/^<\/\s*([\w:-]+)[^>]*>/,G=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,K=/^</,J=/^<\//,H=/\x3c!--(.*?)--\x3e/g,x=/<!DOCTYPE([^>]*?)>/i,
11
+I=/<!\[CDATA\[(.*?)]]\x3e/g,M=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,N=/([^\#-~| |!])/g,v=g("area,br,col,hr,img,wbr");n=g("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr");p=g("rp,rt");var u=h.extend({},p,n),s=h.extend({},n,g("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")),t=h.extend({},p,g("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
12
+n=g("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,stop,svg,switch,text,title,tspan,use");var w=g("script,style"),C=h.extend({},v,s,t,u,n),D=g("background,cite,href,longdesc,src,usemap,xlink:href");n=g("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,size,span,start,summary,target,title,type,valign,value,vspace,width");
13
+p=g("accent-height,accumulate,additive,alphabetic,arabic-form,ascent,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan",
14
+!0);var O=h.extend({},D,p,n),A=document.createElement("pre");h.module("ngSanitize",[]).provider("$sanitize",function(){this.$get=["$$sanitizeUri",function(a){return function(f){var d=[];F(f,r(d,function(c,b){return!/^unsafe/.test(a(c,b))}));return d.join("")}}]});h.module("ngSanitize").filter("linky",["$sanitize",function(a){var f=/((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/,d=/^mailto:/;return function(c,b){function k(a){a&&g.push(E(a))}function e(a,c){g.push("<a ");
15
+h.isDefined(b)&&g.push('target="',b,'" ');g.push('href="',a.replace(/"/g,"&quot;"),'">');k(c);g.push("</a>")}if(!c)return c;for(var m,l=c,g=[],n,p;m=l.match(f);)n=m[0],m[2]||m[4]||(n=(m[3]?"http://":"mailto:")+n),p=m.index,k(l.substr(0,p)),e(n,m[0].replace(d,"")),l=l.substring(p+m[0].length);k(l);return a(g.join(""))}}])})(window,window.angular);
16
+//# sourceMappingURL=angular-sanitize.min.js.map

+ 91 - 0
public/js/lib/angular-spinner.js

@@ -0,0 +1,91 @@
1
+/**
2
+ * angular-spinner version 0.5.0
3
+ * License: MIT.
4
+ * Copyright (C) 2013, 2014, Uri Shaked and contributors.
5
+ */
6
+
7
+(function (root) {
8
+	'use strict';
9
+
10
+	function factory(angular, Spinner) {
11
+
12
+		angular
13
+			.module('angularSpinner', [])
14
+
15
+			.factory('usSpinnerService', ['$rootScope', function ($rootScope) {
16
+				var config = {};
17
+
18
+				config.spin = function (key) {
19
+					$rootScope.$broadcast('us-spinner:spin', key);
20
+				};
21
+
22
+				config.stop = function (key) {
23
+					$rootScope.$broadcast('us-spinner:stop', key);
24
+				};
25
+
26
+				return config;
27
+			}])
28
+
29
+			.directive('usSpinner', ['$window', function ($window) {
30
+				return {
31
+					scope: true,
32
+					link: function (scope, element, attr) {
33
+						var SpinnerConstructor = Spinner || $window.Spinner;
34
+
35
+						scope.spinner = null;
36
+
37
+						scope.key = angular.isDefined(attr.spinnerKey) ? attr.spinnerKey : false;
38
+
39
+						scope.startActive = angular.isDefined(attr.spinnerStartActive) ?
40
+							attr.spinnerStartActive : scope.key ?
41
+							false : true;
42
+
43
+						scope.spin = function () {
44
+							if (scope.spinner) {
45
+								scope.spinner.spin(element[0]);
46
+							}
47
+						};
48
+
49
+						scope.stop = function () {
50
+							if (scope.spinner) {
51
+								scope.spinner.stop();
52
+							}
53
+						};
54
+
55
+						scope.$watch(attr.usSpinner, function (options) {
56
+							scope.stop();
57
+							scope.spinner = new SpinnerConstructor(options);
58
+							if (!scope.key || scope.startActive) {
59
+								scope.spinner.spin(element[0]);
60
+							}
61
+						}, true);
62
+
63
+						scope.$on('us-spinner:spin', function (event, key) {
64
+							if (key === scope.key) {
65
+								scope.spin();
66
+							}
67
+						});
68
+
69
+						scope.$on('us-spinner:stop', function (event, key) {
70
+							if (key === scope.key) {
71
+								scope.stop();
72
+							}
73
+						});
74
+
75
+						scope.$on('$destroy', function () {
76
+							scope.stop();
77
+							scope.spinner = null;
78
+						});
79
+					}
80
+				};
81
+			}]);
82
+	}
83
+
84
+	if (typeof define === 'function' && define.amd) {
85
+		/* AMD module */
86
+		define(['angular', 'spin'], factory);
87
+	} else {
88
+		/* Browser global */
89
+		factory(root.angular);
90
+	}
91
+}(window));

+ 150 - 0
public/js/lib/ng-showdown.js

@@ -0,0 +1,150 @@
1
+// Conditional load for NodeJS
2
+if (typeof require !== 'undefined') {
3
+  var angular = require('angular'),
4
+      showdown = require('showdown');
5
+}
6
+
7
+//Check if AngularJs and Showdown is defined and only load ng-Showdown if both are present
8
+if (typeof angular !== 'undefined' && typeof showdown !== 'undefined') {
9
+
10
+  (function (module, showdown) {
11
+    'use strict';
12
+
13
+    module
14
+      .provider('$showdown', provider)
15
+      .directive('sdModelToHtml', ['$showdown', '$sanitize', markdownToHtmlDirective])
16
+      .filter('sdStripHtml', stripHtmlFilter);
17
+
18
+    /**
19
+     * Angular Provider
20
+     * Enables configuration of showdown via angular.config and Dependency Injection into controllers, views
21
+     * directives, etc... This assures the directives and filters provided by the library itself stay consistent
22
+     * with the user configurations.
23
+     * If the user wants to use a different configuration in a determined context, he can use the "classic" Showdown
24
+     * object instead.
25
+     */
26
+    function provider() {
27
+
28
+      // Configuration parameters for Showdown
29
+      var config = {
30
+        extensions: [],
31
+        stripHtml:  true
32
+      };
33
+
34
+      /**
35
+       * Sets a configuration option
36
+       *
37
+       * @param {string} key Config parameter key
38
+       * @param {string} value Config parameter value
39
+       */
40
+      /* jshint validthis: true */
41
+      this.setOption = function (key, value) {
42
+        config[key] = value;
43
+
44
+        return this;
45
+      };
46
+
47
+      /**
48
+       * Gets the value of the configuration parameter specified by key
49
+       *
50
+       * @param {string} key The config parameter key
51
+       * @returns {string|null} Returns the value of the config parameter. (or null if the config parameter is not set)
52
+       */
53
+      this.getOption = function (key) {
54
+        if (config.hasOwnProperty(key)) {
55
+          return config.key;
56
+        } else {
57
+          return null;
58
+        }
59
+      };
60
+
61
+      /**
62
+       * Loads a Showdown Extension
63
+       *
64
+       * @param {string} extensionName The name of the extension to load
65
+       */
66
+      this.loadExtension = function (extensionName) {
67
+        config.extensions.push(extensionName);
68
+
69
+        return this;
70
+      };
71
+
72
+      function SDObject() {
73
+        var converter = new showdown.Converter(config);
74
+
75
+        /**
76
+         * Converts a markdown text into HTML
77
+         *
78
+         * @param {string} markdown The markdown string to be converted to HTML
79
+         * @returns {string} The converted HTML
80
+         */
81
+        this.makeHtml = function (markdown) {
82
+          return converter.makeHtml(markdown);
83
+        };
84
+
85
+        /**
86
+         * Strips a text of it's HTML tags
87
+         *
88
+         * @param {string} text
89
+         * @returns {string}
90
+         */
91
+        this.stripHtml = function (text) {
92
+          return String(text).replace(/<[^>]+>/gm, '');
93
+        };
94
+      }
95
+
96
+      // The object returned by service provider
97
+      this.$get = function () {
98
+        return new SDObject();
99
+      };
100
+    }
101
+
102
+    /**
103
+     * AngularJS Directive to Md to HTML transformation
104
+     *
105
+     * Usage example:
106
+     * <div sd-model-to-html="markdownText" ></div>
107
+     *
108
+     * @param {showdown.Converter} $showdown
109
+     * @param $sanitize
110
+     * @returns {*}
111
+     */
112
+    function markdownToHtmlDirective($showdown, $sanitize) {
113
+
114
+      var link = function (scope, element) {
115
+        scope.$watch('model', function (newValue) {
116
+          var val;
117
+          if (typeof newValue === 'string') {
118
+            val = $sanitize($showdown.makeHtml(newValue));
119
+          } else {
120
+            val = typeof newValue;
121
+          }
122
+          element.html(val);
123
+        });
124
+      };
125
+
126
+      return {
127
+        restrict: 'A',
128
+        link:     link,
129
+        scope:    {
130
+          model: '=sdModelToHtml'
131
+        }
132
+      };
133
+    }
134
+
135
+    /**
136
+     * AngularJS Filter to Strip HTML tags from text
137
+     *
138
+     * @returns {Function}
139
+     */
140
+    function stripHtmlFilter() {
141
+      return function (text) {
142
+        return String(text).replace(/<[^>]+>/gm, '');
143
+      };
144
+    }
145
+
146
+  })(angular.module('showdown', ['ngSanitize']), showdown);
147
+
148
+} else {
149
+  throw new Error('ng-showdown was not loaded because one of its dependencies (AngularJS or Showdown) was not met');
150
+}

+ 4 - 0
public/js/lib/showdown.min.js

@@ -0,0 +1,4 @@
1
+/*! showdown 07-06-2015 */
2
+
3
+(function(){function a(a,b){"use strict";var d=b?"Error in "+b+" extension->":"Error in unnamed extension",e={valid:!0,error:""};c.helper.isArray(a)||(a=[a]);for(var f=0;f<a.length;++f){var g=d+"sub-extension "+f+": ",h=a[f];if("object"!=typeof h)return e.valid=!1,e.error=g+"must be an object, but "+typeof h+" given",e;if(!c.helper.isString(h.type))return e.valid=!1,e.error=g+'property "type" must be a string, but '+typeof h.type+" given",e;var i=h.type=h.type.toLowerCase();if("language"===i&&(i=h.type="lang"),"html"===i&&(i=h.type="output"),"lang"!==i&&"output"!==i)return e.valid=!1,e.error=g+"type "+i+' is not recognized. Valid values: "lang" or "output"',e;if(h.filter){if("function"!=typeof h.filter)return e.valid=!1,e.error=g+'"filter" must be a function, but '+typeof h.filter+" given",e}else{if(!h.regex)return e.valid=!1,e.error=g+'extensions must define either a "regex" property or a "filter" method',e;if(c.helper.isString(h.regex)&&(h.regex=new RegExp(h.regex,"g")),!h.regex instanceof RegExp)return e.valid=!1,e.error=g+'"regex" property must either be a string or a RegExp object, but '+typeof h.regex+" given",e;if(c.helper.isUndefined(h.replace))return e.valid=!1,e.error=g+'"regex" extensions must implement a replace string or function',e}if(c.helper.isUndefined(h.filter)&&c.helper.isUndefined(h.regex))return e.valid=!1,e.error=g+"output extensions must define a filter property",e}return e}function b(a,b){"use strict";var c=b.charCodeAt(0);return"~E"+c+"E"}var c={},d={},e={},f={omitExtraWLInCodeBlocks:!1,prefixHeaderId:!1},g=JSON.parse(JSON.stringify(f));c.helper={},c.extensions={},c.setOption=function(a,b){"use strict";return g[a]=b,this},c.getOption=function(a){"use strict";return g[a]},c.getOptions=function(){"use strict";return g},c.resetOptions=function(){"use strict";g=JSON.parse(JSON.stringify(f))},c.subParser=function(a,b){"use strict";if(c.helper.isString(a)){if("undefined"==typeof b){if(d.hasOwnProperty(a))return d[a];throw Error("SubParser named "+a+" not registered!")}d[a]=b}},c.extension=function(b,d){"use strict";if(!c.helper.isString(b))throw Error("Extension 'name' must be a string");if(b=c.helper.stdExtName(b),c.helper.isUndefined(d)){if(!e.hasOwnProperty(b))throw Error("Extension named "+b+" is not registered!");return e[b]}"function"==typeof d&&(d=d()),c.helper.isArray(d)||(d=[d]);var f=a(d,b);if(!f.valid)throw Error(f.error);e[b]=d},c.getAllExtensions=function(){"use strict";return e},c.removeExtension=function(a){"use strict";delete e[a]},c.resetExtensions=function(){"use strict";e={}},c.validateExtension=function(b){"use strict";var c=a(b,null);return c.valid?!0:(console.warn(c.error),!1)},c.hasOwnProperty("helper")||(c.helper={}),c.helper.isString=function(a){"use strict";return"string"==typeof a||a instanceof String},c.helper.forEach=function(a,b){"use strict";if("function"==typeof a.forEach)a.forEach(b);else for(var c=0;c<a.length;c++)b(a[c],c,a)},c.helper.isArray=function(a){"use strict";return a.constructor===Array},c.helper.isUndefined=function(a){"use strict";return"undefined"==typeof a},c.helper.stdExtName=function(a){"use strict";return a.replace(/[_-]||\s/g,"").toLowerCase()},c.helper.escapeCharactersCallback=b,c.helper.escapeCharacters=function(a,c,d){"use strict";var e="(["+c.replace(/([\[\]\\])/g,"\\$1")+"])";d&&(e="\\\\"+e);var f=new RegExp(e,"g");return a=a.replace(f,b)},c.helper.isUndefined(console)&&(console={warn:function(a){"use strict";alert(a)},log:function(a){"use strict";alert(a)}}),c.Converter=function(b){"use strict";function f(){b=b||{};for(var a in g)g.hasOwnProperty(a)&&(j[a]=g[a]);if("object"!=typeof b)throw Error("Converter expects the passed parameter to be an object, but "+typeof b+" was passed instead.");for(var d in b)b.hasOwnProperty(d)&&(j[d]=b[d]);j.extensions&&c.helper.forEach(j.extensions,h)}function h(a){if(c.helper.isString(a)){if(a=c.helper.stdExtName(a),c.extensions[a])return console.warn("DEPRECATION WARNING: "+a+" is an old extension that uses a deprecated loading method.Please inform the developer that the extension should be updated!"),void i(c.extensions[a],a);if(c.helper.isUndefined(e[a]))throw Error('Extension "'+a+'" could not be loaded. It was either not found or is not a valid extension.');a=e[a]}if("function"==typeof a&&(a=a()),c.helper.isArray(a)||(a=[a]),c.validateExtension(a))for(var b=0;b<a.length;++b)switch(a[b].type){case"lang":k.push(a[b]);break;case"output":l.push(a[b]);break;default:throw Error("Extension loader error: Type unrecognized!!!")}}function i(b,d){"function"==typeof b&&(b=b(new c.Converter)),c.helper.isArray(b)||(b=[b]);var e=a(b,d);if(!e.valid)throw Error(e.error);for(var f=0;f<b.length;++f)switch(b[f].type){case"lang":k.push(b[f]);break;case"output":l.push(b[f]);break;default:throw Error("Extension loader error: Type unrecognized!!!")}}var j={omitExtraWLInCodeBlocks:!1,prefixHeaderId:!1},k=[],l=[],m=["githubCodeBlocks","hashHTMLBlocks","stripLinkDefinitions","blockGamut","unescapeSpecialChars"];f(),this.makeHtml=function(a){if(!a)return a;var b={gHtmlBlocks:[],gUrls:{},gTitles:{},gListLevel:0,hashLinkCounts:{},langExtensions:k,outputModifiers:l,converter:this};a=a.replace(/~/g,"~T"),a=a.replace(/\$/g,"~D"),a=a.replace(/\r\n/g,"\n"),a=a.replace(/\r/g,"\n"),a="\n\n"+a+"\n\n",a=c.subParser("detab")(a,j,b),a=c.subParser("stripBlankLines")(a,j,b),c.helper.forEach(k,function(d){a=c.subParser("runExtension")(d,a,j,b)});for(var e=0;e<m.length;++e){var f=m[e];a=d[f](a,j,b)}return a=a.replace(/~D/g,"$$"),a=a.replace(/~T/g,"~"),c.helper.forEach(l,function(d){a=c.subParser("runExtension")(d,a,j,b)}),a},this.setOption=function(a,b){j[a]=b},this.getOption=function(a){return j[a]},this.getOptions=function(){return j},this.addExtension=function(a){h(a)},this.useExtension=function(a){h(a)},this.removeExtension=function(a){c.helper.isArray(a)||(a=[a]);for(var b=0;b<a.length;++b){for(var d=a[b],e=0;e<k.length;++e)k[e]===d&&k[e].splice(e,1);for(var f=0;f<l.length;++e)l[f]===d&&l[f].splice(e,1)}},this.getAllExtensions=function(){return{language:k,output:l}}},c.subParser("anchors",function(a,b,d){"use strict";var e=function(a,b,e,f,g,h,i,j){c.helper.isUndefined(j)&&(j=""),a=b;var k=e,l=f.toLowerCase(),m=g,n=j;if(!m)if(l||(l=k.toLowerCase().replace(/ ?\n/g," ")),m="#"+l,c.helper.isUndefined(d.gUrls[l])){if(!(a.search(/\(\s*\)$/m)>-1))return a;m=""}else m=d.gUrls[l],c.helper.isUndefined(d.gTitles[l])||(n=d.gTitles[l]);m=c.helper.escapeCharacters(m,"*_",!1);var o='<a href="'+m+'"';return""!==n&&null!==n&&(n=n.replace(/"/g,"&quot;"),n=c.helper.escapeCharacters(n,"*_",!1),o+=' title="'+n+'"'),o+=">"+k+"</a>"};return a=a.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,e),a=a.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()<?(.*?(?:\(.*?\).*?)?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,e),a=a.replace(/(\[([^\[\]]+)\])()()()()()/g,e)}),c.subParser("autoLinks",function(a){"use strict";a=a.replace(/<((https?|ftp|dict):[^'">\s]+)>/gi,'<a href="$1">$1</a>');var b=/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi;return a=a.replace(b,function(a,b){var d=c.subParser("unescapeSpecialChars")(b);return c.subParser("encodeEmailAddress")(d)})}),c.subParser("blockGamut",function(a,b,d){"use strict";a=c.subParser("headers")(a,b,d);var e=c.subParser("hashBlock")("<hr />",b,d);return a=a.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm,e),a=a.replace(/^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$/gm,e),a=a.replace(/^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$/gm,e),a=c.subParser("lists")(a,b,d),a=c.subParser("codeBlocks")(a,b,d),a=c.subParser("blockQuotes")(a,b,d),a=c.subParser("hashHTMLBlocks")(a,b,d),a=c.subParser("paragraphs")(a,b,d)}),c.subParser("blockQuotes",function(a,b,d){"use strict";return a=a.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm,function(a,e){var f=e;return f=f.replace(/^[ \t]*>[ \t]?/gm,"~0"),f=f.replace(/~0/g,""),f=f.replace(/^[ \t]+$/gm,""),f=c.subParser("blockGamut")(f,b,d),f=f.replace(/(^|\n)/g,"$1  "),f=f.replace(/(\s*<pre>[^\r]+?<\/pre>)/gm,function(a,b){var c=b;return c=c.replace(/^  /gm,"~0"),c=c.replace(/~0/g,"")}),c.subParser("hashBlock")("<blockquote>\n"+f+"\n</blockquote>",b,d)})}),c.subParser("codeBlocks",function(a,b,d){"use strict";a+="~0";var e=/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g;return a=a.replace(e,function(a,e,f){var g=e,h=f,i="\n";return g=c.subParser("outdent")(g),g=c.subParser("encodeCode")(g),g=c.subParser("detab")(g),g=g.replace(/^\n+/g,""),g=g.replace(/\n+$/g,""),b.omitExtraWLInCodeBlocks&&(i=""),g="<pre><code>"+g+i+"</code></pre>",c.subParser("hashBlock")(g,b,d)+h}),a=a.replace(/~0/,"")}),c.subParser("codeSpans",function(a){"use strict";return a=a.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,function(a,b,d,e){var f=e;return f=f.replace(/^([ \t]*)/g,""),f=f.replace(/[ \t]*$/g,""),f=c.subParser("encodeCode")(f),b+"<code>"+f+"</code>"})}),c.subParser("detab",function(a){"use strict";return a=a.replace(/\t(?=\t)/g,"    "),a=a.replace(/\t/g,"~A~B"),a=a.replace(/~B(.+?)~A/g,function(a,b){for(var c=b,d=4-c.length%4,e=0;d>e;e++)c+=" ";return c}),a=a.replace(/~A/g,"    "),a=a.replace(/~B/g,"")}),c.subParser("encodeAmpsAndAngles",function(a){"use strict";return a=a.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g,"&amp;"),a=a.replace(/<(?![a-z\/?\$!])/gi,"&lt;")}),c.subParser("encodeBackslashEscapes",function(a){"use strict";return a=a.replace(/\\(\\)/g,c.helper.escapeCharactersCallback),a=a.replace(/\\([`*_{}\[\]()>#+-.!])/g,c.helper.escapeCharactersCallback)}),c.subParser("encodeCode",function(a){"use strict";return a=a.replace(/&/g,"&amp;"),a=a.replace(/</g,"&lt;"),a=a.replace(/>/g,"&gt;"),a=c.helper.escapeCharacters(a,"*_{}[]\\",!1)}),c.subParser("encodeEmailAddress",function(a){"use strict";var b=[function(a){return"&#"+a.charCodeAt(0)+";"},function(a){return"&#x"+a.charCodeAt(0).toString(16)+";"},function(a){return a}];return a="mailto:"+a,a=a.replace(/./g,function(a){if("@"===a)a=b[Math.floor(2*Math.random())](a);else if(":"!==a){var c=Math.random();a=c>.9?b[2](a):c>.45?b[1](a):b[0](a)}return a}),a='<a href="'+a+'">'+a+"</a>",a=a.replace(/">.+:/g,'">')}),c.subParser("escapeSpecialCharsWithinTagAttributes",function(a){"use strict";var b=/(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|<!(--.*?--\s*)+>)/gi;return a=a.replace(b,function(a){var b=a.replace(/(.)<\/?code>(?=.)/g,"$1`");return b=c.helper.escapeCharacters(b,"\\`*_",!1)})}),c.subParser("githubCodeBlocks",function(a,b,d){"use strict";return a+="~0",a=a.replace(/(?:^|\n)```(.*)\n([\s\S]*?)\n```/g,function(a,e,f){var g=e,h=f,i="\n";return b.omitExtraWLInCodeBlocks&&(i=""),h=c.subParser("encodeCode")(h),h=c.subParser("detab")(h),h=h.replace(/^\n+/g,""),h=h.replace(/\n+$/g,""),h="<pre><code"+(g?' class="'+g+'"':"")+">"+h+i+"</code></pre>",c.subParser("hashBlock")(h,b,d)}),a=a.replace(/~0/,"")}),c.subParser("hashBlock",function(a,b,c){"use strict";return a=a.replace(/(^\n+|\n+$)/g,""),"\n\n~K"+(c.gHtmlBlocks.push(a)-1)+"K\n\n"}),c.subParser("hashElement",function(a,b,c){"use strict";return function(a,b){var d=b;return d=d.replace(/\n\n/g,"\n"),d=d.replace(/^\n/,""),d=d.replace(/\n+$/g,""),d="\n\n~K"+(c.gHtmlBlocks.push(d)-1)+"K\n\n"}}),c.subParser("hashHTMLBlocks",function(a,b,d){"use strict";return a=a.replace(/\n/g,"\n\n"),a=a.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm,c.subParser("hashElement")(a,b,d)),a=a.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|style|section|header|footer|nav|article|aside|address|audio|canvas|figure|hgroup|output|video)\b[^\r]*?<\/\2>[ \t]*(?=\n+)\n)/gm,c.subParser("hashElement")(a,b,d)),a=a.replace(/(\n[ ]{0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,c.subParser("hashElement")(a,b,d)),a=a.replace(/(\n\n[ ]{0,3}<!(--[^\r]*?--\s*)+>[ \t]*(?=\n{2,}))/g,c.subParser("hashElement")(a,b,d)),a=a.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,c.subParser("hashElement")(a,b,d)),a=a.replace(/\n\n/g,"\n")}),c.subParser("headers",function(a,b,d){"use strict";function e(a){var b,e=a.replace(/[^\w]/g,"").toLowerCase();return d.hashLinkCounts[e]?b=e+"-"+d.hashLinkCounts[e]++:(b=e,d.hashLinkCounts[e]=1),f===!0&&(f="section"),c.helper.isString(f)?f+b:b}var f=b.prefixHeaderId;return a=a.replace(/^(.+)[ \t]*\n=+[ \t]*\n+/gm,function(a,f){var g=c.subParser("spanGamut")(f,b,d),h='<h1 id="'+e(f)+'">'+g+"</h1>";return c.subParser("hashBlock")(h,b,d)}),a=a.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm,function(a,f){var g=c.subParser("spanGamut")(f,b,d),h='<h2 id="'+e(f)+'">'+g+"</h2>";return c.subParser("hashBlock")(h,b,d)}),a=a.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm,function(a,f,g){var h=c.subParser("spanGamut")(g,b,d),i="<h"+f.length+' id="'+e(g)+'">'+h+"</h"+f.length+">";return c.subParser("hashBlock")(i,b,d)})}),c.subParser("images",function(a,b,d){"use strict";var e=function(a,b,e,f,g,h,i,j){a=b;var k=e,l=f.toLowerCase(),m=g,n=j,o=d.gUrls,p=d.gTitles;if(n||(n=""),""===m||null===m){if((""===l||null===l)&&(l=k.toLowerCase().replace(/ ?\n/g," ")),m="#"+l,"undefined"==typeof o[l])return a;m=o[l],"undefined"!=typeof p[l]&&(n=p[l])}k=k.replace(/"/g,"&quot;"),m=c.helper.escapeCharacters(m,"*_",!1);var q='<img src="'+m+'" alt="'+k+'"';return n=n.replace(/"/g,"&quot;"),n=c.helper.escapeCharacters(n,"*_",!1),q+=' title="'+n+'"',q+=" />"};return a=a.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,e),a=a.replace(/(!\[(.*?)\]\s?\([ \t]*()<?(\S+?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,e)}),c.subParser("italicsAndBold",function(a){"use strict";return a=a.replace(/(\*\*|__)(?=\S)([^\r]*?\S[*_]*)\1/g,"<strong>$2</strong>"),a=a.replace(/(\*|_)(?=\S)([^\r]*?\S)\1/g,"<em>$2</em>")}),c.subParser("lists",function(a,b,d){"use strict";var e=function(a){return d.gListLevel++,a=a.replace(/\n{2,}$/,"\n"),a+="~0",a=a.replace(/(\n)?(^[ \t]*)([*+-]|\d+[.])[ \t]+([^\r]+?(\n{1,2}))(?=\n*(~0|\2([*+-]|\d+[.])[ \t]+))/gm,function(a,e,f,g,h){var i=c.subParser("outdent")(h,b,d);return e||i.search(/\n{2,}/)>-1?i=c.subParser("blockGamut")(i,b,d):(i=c.subParser("lists")(i,b,d),i=i.replace(/\n$/,""),i=c.subParser("spanGamut")(i,b,d)),"<li>"+i+"</li>\n"}),a=a.replace(/~0/g,""),d.gListLevel--,a};a+="~0";var f=/^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm;return d.gListLevel?a=a.replace(f,function(a,b,c){var d=b,f=c.search(/[*+-]/g)>-1?"ul":"ol";d=d.replace(/\n{2,}/g,"\n\n\n");var g=e(d);return g=g.replace(/\s+$/,""),g="<"+f+">"+g+"</"+f+">\n"}):(f=/(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g,a=a.replace(f,function(a,b,c,d){var f=c.replace(/\n{2,}/g,"\n\n\n"),g=d.search(/[*+-]/g)>-1?"ul":"ol",h=e(f);return b+"<"+g+">\n"+h+"</"+g+">\n"})),a=a.replace(/~0/,"")}),c.subParser("outdent",function(a){"use strict";return a=a.replace(/^(\t|[ ]{1,4})/gm,"~0"),a=a.replace(/~0/g,"")}),c.subParser("paragraphs",function(a,b,d){"use strict";a=a.replace(/^\n+/g,""),a=a.replace(/\n+$/g,"");for(var e=a.split(/\n{2,}/g),f=[],g=e.length,h=0;g>h;h++){var i=e[h];i.search(/~K(\d+)K/g)>=0?f.push(i):i.search(/\S/)>=0&&(i=c.subParser("spanGamut")(i,b,d),i=i.replace(/^([ \t]*)/g,"<p>"),i+="</p>",f.push(i))}for(g=f.length,h=0;g>h;h++)for(;f[h].search(/~K(\d+)K/)>=0;){var j=d.gHtmlBlocks[RegExp.$1];j=j.replace(/\$/g,"$$$$"),f[h]=f[h].replace(/~K\d+K/,j)}return f.join("\n\n")}),c.subParser("runExtension",function(a,b,c,d){"use strict";if(a.filter)b=a.filter(b,d.converter,c);else if(a.regex){var e=a.regex;!e instanceof RegExp&&(e=new RegExp(e,"g")),b=b.replace(e,a.replace)}return b}),c.subParser("spanGamut",function(a,b,d){"use strict";return a=c.subParser("codeSpans")(a,b,d),a=c.subParser("escapeSpecialCharsWithinTagAttributes")(a,b,d),a=c.subParser("encodeBackslashEscapes")(a,b,d),a=c.subParser("images")(a,b,d),a=c.subParser("anchors")(a,b,d),a=c.subParser("autoLinks")(a,b,d),a=c.subParser("encodeAmpsAndAngles")(a,b,d),a=c.subParser("italicsAndBold")(a,b,d),a=a.replace(/  +\n/g," <br />\n")}),c.subParser("stripBlankLines",function(a){"use strict";return a.replace(/^[ \t]+$/gm,"")}),c.subParser("stripLinkDefinitions",function(a,b,d){"use strict";var e=/^[ ]{0,3}\[(.+)]:[ \t]*\n?[ \t]*<?(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|(?=~0))/gm;return a+="~0",a=a.replace(e,function(a,b,e,f,g){return b=b.toLowerCase(),d.gUrls[b]=c.subParser("encodeAmpsAndAngles")(e),f?f+g:(g&&(d.gTitles[b]=g.replace(/"/g,"&quot;")),"")}),a=a.replace(/~0/,"")}),c.subParser("unescapeSpecialChars",function(a){"use strict";return a=a.replace(/~E(\d+)E/g,function(a,b){var c=parseInt(b);return String.fromCharCode(c)})});var h=this;"undefined"!=typeof module&&module.exports?module.exports=c:"function"==typeof define&&define.amd?define("showdown",function(){"use strict";return c}):h.showdown=c}).call(this);
4
+//# sourceMappingURL=showdown.min.js.map

+ 84 - 14
public/js/scripts/app.js

@@ -9,30 +9,100 @@
9 9
  * Main module of the application.
10 10
  */
11 11
 angular
12
-  .module('domainManagerApp', [])
12
+  .module('WhoisAPI', ['ngRoute', 'ngSanitize', 'showdown', 'hljs', 'angularSpinner'])
13
+
14
+  .config(['$routeProvider', '$httpProvider', function($routeProvider, $httpProvider) {
15
+    // Configs
16
+    //Enable cross domain calls
17
+    $httpProvider.defaults.useXDomain = true;
18
+    //Remove the header used to identify ajax call  that would prevent CORS from working
19
+    delete $httpProvider.defaults.headers.common['X-Requested-With'];
20
+    // Routes
21
+    $routeProvider
22
+      .when('/', {
23
+        templateUrl: 'views/lookup.html',
24
+        controller: 'WhoisController'
25
+      })
26
+      .when('/docs', {
27
+        templateUrl: 'views/docs.html',
28
+        controller: 'DocsController'
29
+      })
30
+      .otherwise({
31
+        redirectTo: '/'
32
+      });
33
+  }])
13 34
 
14
-  .controller('WhoisController',['$scope', '$http', function ($scope, $http) {
15 35
 
16
-    $scope.domain = ""
17
-    $scope.showResult = false
36
+  .controller('HeaderController', ['$scope', '$location', function ($scope, $location) {
37
+    $scope.isActive = function (viewLocation) {
38
+        return viewLocation === $location.path();
39
+    };
18 40
 
41
+  }])
19 42
 
43
+  .controller('WhoisController',['$scope', '$rootScope', 'CurrentLookup', 'usSpinnerService', function ($scope, $rootScope, CurrentLookup, usSpinnerService) {
44
+    if(CurrentLookup.get() == "") {
45
+      $scope.domain = "";
46
+      $scope.showResult = false;
47
+    } else {
48
+      $scope.domain = JSON.stringify(CurrentLookup.get(), null, 4);
49
+      $scope.showResult = true;
50
+      $scope.url = CurrentLookup.getUrl();
51
+    }
20 52
     $scope.lookup = function(){
53
+      usSpinnerService.spin('spinner-1');
54
+      CurrentLookup.domainLookup($scope.url)
55
+    }
56
+    $rootScope.$on('lookup:finished', function() {
57
+      if(!$scope.$$phase) {
58
+        $scope.$apply(function(){
59
+          $scope.domain = JSON.stringify(CurrentLookup.get(), null, 4);
60
+          $scope.showResult = true
61
+        });
62
+      } else {
63
+          $scope.domain = JSON.stringify(CurrentLookup.get(), null, 4);
64
+          $scope.showResult = true
65
+      }
66
+      usSpinnerService.stop('spinner-1');
67
+    });
68
+
69
+  }])
70
+
71
+  .controller('DocsController', ['$scope', '$window', function ($scope, $window) {
72
+
73
+  }])
74
+
75
+  .service('CurrentLookup', [ '$rootScope', '$http',  function($rootScope, $http) {
76
+    var domain = "";
77
+    var lastUrl = ""
78
+    this.domainLookup = function(url){
79
+      console.log("Starting Domain Lookup...")
21 80
       $http({
22 81
         method: 'GET',
23
-        url: 'http://whois.j1x.co/' + $scope.url
82
+        url: 'http://whois.j1x.co/' + url
24 83
       }).success(function(data) {
25 84
         console.log("Domain Lookup Successfull")
26 85
         console.log(data)
27
-        if(!$scope.$$phase) {
28
-          $scope.$apply(function(){
29
-            $scope.domain = JSON.stringify(data, null, 4);
30
-            $scope.showResult = true
31
-          });
32
-        } else {
33
-          $scope.domain = JSON.stringify(data, null, 4);
34
-          $scope.showResult = true
35
-        }
86
+        domain = data;
87
+        lastUrl = url
88
+        $rootScope.$broadcast('lookup:finished');
36 89
       });
37 90
     }
91
+    this.get = function() {
92
+      return domain;
93
+    }
94
+    this.getUrl = function() {
95
+      return lastUrl;
96
+    }
97
+
98
+  }])
99
+
100
+  .directive('markdown', ['$showdown', function ($showdown) {
101
+    return {
102
+        restrict: 'A',
103
+        link: function (scope, element, attrs) {
104
+            var htmlText = $showdown.makeHtml(element.text());
105
+            element.html(htmlText);
106
+        }
107
+    };
38 108
   }])

+ 77 - 0
public/views/docs.html

@@ -0,0 +1,77 @@
1
+<h1>Introduction</h1>
2
+
3
+<p>A simplified <b>Whoislookup API</b>  with a <b>JSON</b> response. The API returns less information, but with better accuracy than other whois lookup services.</p>
4
+
5
+<p>Created by <a href="http://jamesperet.com">James Peret</a>, based on the <a href="https://github.com/okor/whoiz">Whoiz</a> application by <a href="http://jasonormand.com/2012/06/10/a-free-whois-api/">Jason Ormand</a> and using the <a href="http://whoisrb.org/">Ruby Whois</a> gem.
6
+
7
+<h2>Accessing the API</h2>
8
+
9
+<p>Provide a <b>URL</b> for the <i>API</i> and get a nicely formatted <i>JSON</i> response with the domain information. You can query information in many diferent ways like directly thru the browser, with curl on the command line or with javascript. Here are some examples:</p>
10
+
11
+<h4>Browser</h4>
12
+<div hljs>http://whois.j1x.co/jamesperet.com</div>
13
+
14
+<h4>curl</h4>
15
+
16
+<div hljs>curl http://whois.j1x.co/jamesperet.com</div>
17
+
18
+<h4>Angular JS Directive</h4>
19
+
20
+<div hljs>
21
+angular.module('ExampleApp', [])
22
+  .service('DomainLookup', [ '$http',  function($http) {
23
+    domain = ""
24
+    this.domainLookup = function(url){
25
+      $http({
26
+        method: 'GET',
27
+        url: 'http://whois.j1x.co/' + url
28
+      }).success(function(data) {
29
+        console.log("Domain Lookup Successfull")
30
+        console.log(data)
31
+        domain = data;
32
+        return domain;
33
+      });
34
+    }
35
+    this.get = function() {
36
+      return domain;
37
+    }
38
+  }])
39
+</div>
40
+
41
+<h2>Query Options</h2>
42
+
43
+<p>This are the options for querying the API:</p>
44
+<ul>
45
+<li> <code>url</code> - The domain url that will be queried. Omit the <code>http://www.</code>.</li>
46
+<li> <code>&raw=true</code> - the <i>JSON</i> response will include a <i>raw</i> version of the data received by the <b>registrar</b></li>
47
+<li> <code>&dev=true</code> - Only basic information and the *raw* version will be included in the *JSON* response. No parsing will be done with the data. This is useful for <b>debugging</b>b>.</li>
48
+</ul>
49
+
50
+<p>Example:</p>
51
+
52
+<div hljs source="'http://whois.j1x.co/google.com&raw=true'"></div>
53
+
54
+
55
+<h2>Example Domain Lookup response</h2>
56
+
57
+<div hljs>
58
+{
59
+  "domain": "jamesperet.com",
60
+  "owner": "James Peret",
61
+  "registrar": "eNom Inc.",
62
+  "expires_on": "2016-06-07T16:01:00.00Z",
63
+  "update_on": "2015-05-13 00:00:00 -0300",
64
+  "registered?": true,
65
+  "available?": false
66
+}
67
+</div>
68
+
69
+<h2>Domain Support</h2>
70
+
71
+<p>For now the supported domains are: <code>.com</code>, <code>.net</code>, <code>.com.br</code> and <code>.network</code>. Support for more domains will come in the future.</p>
72
+
73
+<p>The queries for <code>.com.br</code> domains usually come with limited information because of the registrar's API call allowance.</p>
74
+
75
+<p>There is also a bug where some <code>.com</code> domains return an error (ex: google.com).</p>
76
+
77
+<div style="margin-bottom: 70px;"></div>

+ 16 - 0
public/views/lookup.html

@@ -0,0 +1,16 @@
1
+<form class="form-horizontal" ng-submit="lookup()">
2
+    <fieldset class="domain-search centered">
3
+
4
+        <div class="domain-input">
5
+          <div class="input-group input-group-lg">
6
+            <span class="input-group-addon" id="basic-addon1">http://whois.j1x.co/</span>
7
+            <input id="domainTitle" ng-model="url" name="txtTitle" type="text" placeholder="example.com" class="form-control input-md">
8
+          </div>
9
+        </div>
10
+        <input id="singlebutton" ng-disabled="!url" name="singlebutton" class="btn btn-primary btn-lg domain-btn"type="submit"value="Search">
11
+
12
+
13
+    </fieldset>
14
+</form>
15
+<span us-spinner="{radius:30, width:8, length: 16}" spinner-key="spinner-1"></span>
16
+<div ng-show="showResult"><div hljs source="domain"></div></div>