cheatr 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,137 @@
1
+ require "rest_client"
2
+
3
+ module Cheatr::Client
4
+ class Sheet
5
+
6
+ attr_reader :name, :contents
7
+ attr_reader :errors, :uri, :cache_file
8
+
9
+ def self.all(query = nil)
10
+ RestClient.get "http://#{Cheatr::Client.server}/#{query}"
11
+ end
12
+
13
+ def initialize(name, opts = {})
14
+ raise "Sheet name '#{name}' is not valid" unless name =~ Cheatr::SHEET_NAME_REGEXP
15
+ @name = name
16
+ @cache_file = File.join(Cheatr::Client.cache_dir, "#{name}.md")
17
+ @uri = "http://#{Cheatr::Client.server}/#{name}"
18
+ fetch(opts)
19
+ end
20
+
21
+ #
22
+ # Sets new contents to be saved.
23
+ #
24
+ def contents=(new_contents)
25
+ if new_contents != contents
26
+ @old_contents = contents
27
+ @contents = new_contents
28
+ end
29
+ new_contents
30
+ end
31
+
32
+ #
33
+ # Returns true if the contents were last fetched from the remote server,
34
+ # false if fetched from cache.
35
+ #
36
+ def remote?
37
+ @remote == true
38
+ end
39
+
40
+ #
41
+ # Returns true if the contents have been modified and need to be saved,
42
+ # false otherwise.
43
+ #
44
+ def changed?
45
+ @old_contents != @contents
46
+ end
47
+
48
+ #
49
+ # Saves the contents if changed.
50
+ #
51
+ # Cache is updated if saving to remote server was successful.
52
+ #
53
+ # Returns true if successful, false otherwise.
54
+ #
55
+ def save
56
+ return false if contents.nil?
57
+ if changed? && save_remote
58
+ # Re-fetch because the server may modify contents on saving.
59
+ # Cached version will be saved during re-fetching below.
60
+ fetch(ignore_cache: true)
61
+ end
62
+ !changed?
63
+ end
64
+
65
+ #
66
+ # Fetches the contents, either from cache if available, or from the remote
67
+ # server.
68
+ #
69
+ # If ignore_cache is true, the cache is ignored, and contents are updated
70
+ # from the remote server.
71
+ #
72
+ # If contents are fetched from the remote server, the cache is updated.
73
+ # Returns true if successful, false otherwise.
74
+ #
75
+ def fetch(opts = {})
76
+ fetched = opts[:ignore_cache] ? nil : fetch_cache
77
+ fetched ||= fetch_remote
78
+ if fetched
79
+ @contents = fetched
80
+ @old_contents = @contents
81
+ save_cache if remote?
82
+ end
83
+ !!fetched
84
+ end
85
+
86
+ private
87
+
88
+ #
89
+ # Saves contents to the cache file.
90
+ #
91
+ def save_cache
92
+ File.open(cache_file, 'w') { |f| f.write(contents) } unless contents.nil?
93
+ end
94
+
95
+ #
96
+ # Returns the cached contents, or nil.
97
+ #
98
+ def fetch_cache
99
+ @remote = false
100
+ File.read(cache_file) rescue nil
101
+ end
102
+
103
+ #
104
+ # Saves the contents to remote server.
105
+ #
106
+ # Returns true if successful, false otherwise.
107
+ #
108
+ def save_remote
109
+ RestClient.put uri, contents
110
+ @errors = nil
111
+ true
112
+ rescue RestClient::BadRequest => e
113
+ @errors = e.response.body.strip.split("\n")
114
+ false
115
+ rescue Errno::ECONNREFUSED => e
116
+ @errors = ["Could not connect to server (#{e.message})"]
117
+ false
118
+ end
119
+
120
+ #
121
+ # Fetches contents from remote server.
122
+ #
123
+ # Returns the contents, or nil if unsuccessful.
124
+ #
125
+ def fetch_remote
126
+ @remote = true
127
+ RestClient.get uri
128
+ rescue RestClient::ResourceNotFound
129
+ @errors = ["Sheet '#{name}' does not exist."]
130
+ nil
131
+ rescue Errno::ECONNREFUSED => e
132
+ @errors = ["Could not connect to server (#{e.message})"]
133
+ nil
134
+ end
135
+
136
+ end
137
+ end
@@ -0,0 +1,7 @@
1
+ module Cheatr
2
+ class Error < StandardError
3
+ def initialize(message)
4
+ super(message)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+
2
+ module Cheatr
3
+ module Server
4
+ autoload :Sheet, "cheatr/server/sheet"
5
+ autoload :App, "cheatr/server/app"
6
+
7
+ def self.run(repository, opts = {})
8
+ @@config = @@config.merge(opts.to_hash)
9
+ Sheet.repository = File.expand_path(repository)
10
+ puts "Serving cheatr repository at #{Sheet.repository}"
11
+ App.run!
12
+ rescue Cheatr::Error => e
13
+ puts "Error: #{e.message}"
14
+ end
15
+
16
+ @@config = {}
17
+
18
+ def self.config
19
+ @@config
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,80 @@
1
+ require 'erb'
2
+ require 'redcarpet'
3
+ require 'sinatra/base'
4
+ require 'cheatr/server/sheet'
5
+ require 'cheatr/server/helpers'
6
+
7
+ module Cheatr::Server
8
+ class App < Sinatra::Base
9
+ include Helpers
10
+
11
+ # Routes
12
+
13
+ get '/' do
14
+ @title = 'Cheat sheets'
15
+ @sheets = Sheet.all
16
+ template :index
17
+ end
18
+
19
+ get '/:name' do |name|
20
+ if query? name
21
+ @sheets = Sheet.all(name)
22
+ @title = "Cheat sheets matching '#{name}'"
23
+ template :index
24
+ else
25
+ @sheet = Sheet.find!(name)
26
+ @title = @sheet.human_name
27
+ template :sheet
28
+ end
29
+ end
30
+
31
+ put '/:name' do |name|
32
+ @sheet = Sheet.new(name)
33
+ @sheet.contents = request.body.read
34
+ if @sheet.save
35
+ text "Sheet #{name} updated successfully"
36
+ else
37
+ text @sheet.errors.full_messages, status: 400
38
+ end
39
+ end
40
+
41
+ # Configuration
42
+
43
+ def self.base_path(append = nil)
44
+ append ? File.join(path, append) : path
45
+ end
46
+
47
+ configure do
48
+ set :app_file, __FILE__
49
+ set :base_path, File.dirname(__FILE__)
50
+ set :views, File.join(settings.base_path, 'views')
51
+ set :public_folder, File.join(settings.base_path, 'public')
52
+ Cheatr::Server.config.each_pair do |option, value|
53
+ set option.to_sym, value
54
+ end
55
+ end
56
+
57
+ configure :production, :development do
58
+ enable :logging
59
+ end
60
+
61
+ configure :development do
62
+ enable :logging, :dump_errors, :raise_errors
63
+ end
64
+
65
+ configure :production do
66
+ set :raise_errors, false
67
+ set :show_exceptions, false
68
+ end
69
+
70
+ error do
71
+ status 500
72
+ template :error
73
+ end
74
+
75
+ not_found do
76
+ send_file File.join(settings.public_folder, '404.html'), status: 404
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,47 @@
1
+ module Cheatr::Server
2
+ module Helpers
3
+ def query?(name)
4
+ name.include? '*'
5
+ end
6
+
7
+ def html?
8
+ request.accept? 'text/html'
9
+ end
10
+
11
+ def default_content_type
12
+ html? ? 'text/html' : 'text/markdown'
13
+ end
14
+
15
+ #
16
+ # Processes the given text as markdown, additionally processing cheatr links as well.
17
+ #
18
+ def md(text)
19
+ markdown text.
20
+ gsub(/{{([a-z]+([\.\-\_][a-z]+)*)}}/, '[\1](/\1)'). # {{name}} -> [name](/name)
21
+ gsub(/{{([^\|}]+)\|([a-z]+([\.\-\_][a-z]+)*)}}/, '[\1](/\2)'). # {{link text|name}} -> [link text](/name)
22
+ to_s
23
+ end
24
+
25
+ def text(output, opts = {})
26
+ content_type 'text/plain'
27
+ status opts[:status] if opts[:status]
28
+ if output.is_a?(Array)
29
+ output = output.map { |s| "#{s}\n" }
30
+ end
31
+ logger.info output
32
+ output
33
+ end
34
+
35
+ def template(name, opts = {})
36
+ status opts[:status] if opts[:status]
37
+ content_type opts[:content_type] || default_content_type
38
+ logger.info "Rendering template #{name}"
39
+ output = erb :"#{name}.md", layout: false
40
+ if html?
41
+ erb md(output), layout: :"layout.html"
42
+ else
43
+ output
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,157 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Page Not Found :( - Cheatr</title>
6
+ <style>
7
+ ::-moz-selection {
8
+ background: #b3d4fc;
9
+ text-shadow: none;
10
+ }
11
+
12
+ ::selection {
13
+ background: #b3d4fc;
14
+ text-shadow: none;
15
+ }
16
+
17
+ html {
18
+ padding: 30px 10px;
19
+ font-size: 20px;
20
+ line-height: 1.4;
21
+ color: #737373;
22
+ background: #f0f0f0;
23
+ -webkit-text-size-adjust: 100%;
24
+ -ms-text-size-adjust: 100%;
25
+ }
26
+
27
+ html,
28
+ input {
29
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
30
+ }
31
+
32
+ body {
33
+ max-width: 500px;
34
+ _width: 500px;
35
+ padding: 30px 20px 50px;
36
+ border: 1px solid #b3b3b3;
37
+ border-radius: 4px;
38
+ margin: 0 auto;
39
+ box-shadow: 0 1px 10px #a7a7a7, inset 0 1px 0 #fff;
40
+ background: #fcfcfc;
41
+ }
42
+
43
+ h1 {
44
+ margin: 0 10px;
45
+ font-size: 50px;
46
+ text-align: center;
47
+ }
48
+
49
+ h1 span {
50
+ color: #bbb;
51
+ }
52
+
53
+ h3 {
54
+ margin: 1.5em 0 0.5em;
55
+ }
56
+
57
+ p {
58
+ margin: 1em 0;
59
+ }
60
+
61
+ ul {
62
+ padding: 0 0 0 40px;
63
+ margin: 1em 0;
64
+ }
65
+
66
+ .container {
67
+ max-width: 380px;
68
+ _width: 380px;
69
+ margin: 0 auto;
70
+ }
71
+
72
+ /* google search */
73
+
74
+ #goog-fixurl ul {
75
+ list-style: none;
76
+ padding: 0;
77
+ margin: 0;
78
+ }
79
+
80
+ #goog-fixurl form {
81
+ margin: 0;
82
+ }
83
+
84
+ #goog-wm-qt,
85
+ #goog-wm-sb {
86
+ border: 1px solid #bbb;
87
+ font-size: 16px;
88
+ line-height: normal;
89
+ vertical-align: top;
90
+ color: #444;
91
+ border-radius: 2px;
92
+ }
93
+
94
+ #goog-wm-qt {
95
+ width: 220px;
96
+ height: 20px;
97
+ padding: 5px;
98
+ margin: 5px 10px 0 0;
99
+ box-shadow: inset 0 1px 1px #ccc;
100
+ }
101
+
102
+ #goog-wm-sb {
103
+ display: inline-block;
104
+ height: 32px;
105
+ padding: 0 10px;
106
+ margin: 5px 0 0;
107
+ white-space: nowrap;
108
+ cursor: pointer;
109
+ background-color: #f5f5f5;
110
+ background-image: -webkit-linear-gradient(rgba(255,255,255,0), #f1f1f1);
111
+ background-image: -moz-linear-gradient(rgba(255,255,255,0), #f1f1f1);
112
+ background-image: -ms-linear-gradient(rgba(255,255,255,0), #f1f1f1);
113
+ background-image: -o-linear-gradient(rgba(255,255,255,0), #f1f1f1);
114
+ -webkit-appearance: none;
115
+ -moz-appearance: none;
116
+ appearance: none;
117
+ *overflow: visible;
118
+ *display: inline;
119
+ *zoom: 1;
120
+ }
121
+
122
+ #goog-wm-sb:hover,
123
+ #goog-wm-sb:focus {
124
+ border-color: #aaa;
125
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
126
+ background-color: #f8f8f8;
127
+ }
128
+
129
+ #goog-wm-qt:hover,
130
+ #goog-wm-qt:focus {
131
+ border-color: #105cb6;
132
+ outline: 0;
133
+ color: #222;
134
+ }
135
+
136
+ input::-moz-focus-inner {
137
+ padding: 0;
138
+ border: 0;
139
+ }
140
+ </style>
141
+ </head>
142
+ <body>
143
+ <div class="container">
144
+ <h1>Not found <span>:(</span></h1>
145
+ <p>Sorry, but the page you were trying to view does not exist.</p>
146
+ <p>It looks like this was the result of either:</p>
147
+ <ul>
148
+ <li>a mistyped address</li>
149
+ <li>an out-of-date link</li>
150
+ </ul>
151
+ <script>
152
+ var GOOG_FIXURL_LANG = (navigator.language || '').slice(0,2),GOOG_FIXURL_SITE = location.host;
153
+ </script>
154
+ <script src="//linkhelp.clients.google.com/tbproxy/lh/wm/fixurl.js"></script>
155
+ </div>
156
+ </body>
157
+ </html>