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.
- 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>
|