cheatr 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +173 -0
- data/Rakefile +1 -0
- data/bin/cheatr +108 -0
- data/cheatr.gemspec +37 -0
- data/lib/cheatr.rb +6 -0
- data/lib/cheatr/client.rb +133 -0
- data/lib/cheatr/client/sheet.rb +137 -0
- data/lib/cheatr/error.rb +7 -0
- data/lib/cheatr/server.rb +22 -0
- data/lib/cheatr/server/app.rb +80 -0
- data/lib/cheatr/server/helpers.rb +47 -0
- data/lib/cheatr/server/public/404.html +157 -0
- data/lib/cheatr/server/public/css/main.css +300 -0
- data/lib/cheatr/server/public/css/normalize.css +533 -0
- data/lib/cheatr/server/public/favicon.ico +0 -0
- data/lib/cheatr/server/public/humans.txt +13 -0
- data/lib/cheatr/server/public/robots.txt +3 -0
- data/lib/cheatr/server/sheet.rb +139 -0
- data/lib/cheatr/server/views/index.md.erb +5 -0
- data/lib/cheatr/server/views/layout.html.erb +24 -0
- data/lib/cheatr/server/views/sheet.md.erb +1 -0
- data/lib/cheatr/version.rb +3 -0
- metadata +239 -0
@@ -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
|
data/lib/cheatr/error.rb
ADDED
@@ -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>
|