artifact 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/artifact.gemspec +31 -0
- data/bin/artifact +4 -0
- data/lib/artifact.rb +337 -0
- data/lib/artifact/app.rb +55 -0
- data/lib/artifact/files.rb +60 -0
- data/lib/artifact/helpers.rb +130 -0
- data/lib/artifact/middleman.rb +213 -0
- data/lib/artifact/public/css/dropzone.css +155 -0
- data/lib/artifact/public/css/main.css +222 -0
- data/lib/artifact/public/css/pagoda.css +782 -0
- data/lib/artifact/public/js/app.js +123 -0
- data/lib/artifact/public/js/dropzone.js +1919 -0
- data/lib/artifact/static.rb +41 -0
- data/lib/artifact/version.rb +3 -0
- data/lib/artifact/views/errors/404.erb +41 -0
- data/lib/artifact/views/errors/500.erb +43 -0
- data/lib/artifact/views/files/edit.erb +11 -0
- data/lib/artifact/views/files/index.erb +20 -0
- data/lib/artifact/views/files/new.erb +14 -0
- data/lib/artifact/views/layout.erb +36 -0
- data/lib/artifact/views/posts/_list.erb +21 -0
- data/lib/artifact/views/posts/_meta.erb +15 -0
- data/lib/artifact/views/posts/_navbar.erb +49 -0
- data/lib/artifact/views/posts/_table.erb +23 -0
- data/lib/artifact/views/posts/edit.erb +27 -0
- data/lib/artifact/views/posts/index.erb +39 -0
- data/lib/artifact/views/posts/new.erb +22 -0
- data/lib/artifact/views/shared/_menu.erb +18 -0
- data/lib/artifact/views/uploads/_form.erb +27 -0
- data/lib/artifact/views/uploads/_list.erb +17 -0
- metadata +207 -0
data/lib/artifact/app.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'helpers')
|
2
|
+
%w(sinatra/base active_support/inflector json).each { |lib| require lib }
|
3
|
+
|
4
|
+
module Artifact
|
5
|
+
|
6
|
+
class App < Sinatra::Base
|
7
|
+
|
8
|
+
set :method_override, true
|
9
|
+
set :sessions, true
|
10
|
+
set :views, File.expand_path(File.join(File.dirname(__FILE__), 'views'))
|
11
|
+
|
12
|
+
use Rack::ShowExceptions
|
13
|
+
# use Rack::CommonLogger, LOGGER
|
14
|
+
# use Rack::Flash
|
15
|
+
use Rack::Static,
|
16
|
+
:urls => %w(/css /img /js),
|
17
|
+
:root => File.expand_path(File.join(File.dirname(__FILE__), 'public'))
|
18
|
+
|
19
|
+
helpers do
|
20
|
+
# include Rack::Utils # provides escape, parse_query and unescape
|
21
|
+
# include Sinatra::ContentFor
|
22
|
+
include Helpers
|
23
|
+
def logger; LOGGER; end
|
24
|
+
end
|
25
|
+
|
26
|
+
error do
|
27
|
+
if request.env['sinatra.error'].class.name['DocumentNotFound']
|
28
|
+
not_found
|
29
|
+
else
|
30
|
+
erb :'errors/500', :layout => false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
not_found do
|
35
|
+
not_found
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# FIXME: SOON!
|
41
|
+
def current_user
|
42
|
+
author = {
|
43
|
+
:email => "ofofofof@gmail.com",
|
44
|
+
:time => Time.now,
|
45
|
+
:name => "The User"
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def not_found
|
50
|
+
erb :'errors/404', :layout => false
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'app')
|
2
|
+
|
3
|
+
module Artifact
|
4
|
+
|
5
|
+
def self.files
|
6
|
+
@files ||= Tree.new(config.root)
|
7
|
+
end
|
8
|
+
|
9
|
+
class Files < App
|
10
|
+
|
11
|
+
get '/' do
|
12
|
+
redirect to('/files')
|
13
|
+
end
|
14
|
+
|
15
|
+
get '/files' do
|
16
|
+
@files = Artifact.files.all
|
17
|
+
view 'files/index'
|
18
|
+
end
|
19
|
+
|
20
|
+
get '/files/new' do
|
21
|
+
view 'files/new'
|
22
|
+
end
|
23
|
+
|
24
|
+
post '/files/create' do
|
25
|
+
@file = Artifact.files.new(params[:path])
|
26
|
+
@file.update(params[:content], params[:meta])
|
27
|
+
redirect to('/')
|
28
|
+
end
|
29
|
+
|
30
|
+
get '/files/edit/*' do
|
31
|
+
get_file
|
32
|
+
view 'files/edit'
|
33
|
+
end
|
34
|
+
|
35
|
+
post '/files/update/*' do
|
36
|
+
get_file
|
37
|
+
@file.update(params[:content], params[:meta])
|
38
|
+
|
39
|
+
if params[:submit] and params[:submit][/push/i]
|
40
|
+
return redirect to("/commit/#{params[:splat][0]}")
|
41
|
+
end
|
42
|
+
|
43
|
+
redirect to('/')
|
44
|
+
end
|
45
|
+
|
46
|
+
get '/commit/*' do
|
47
|
+
get_file
|
48
|
+
Artifact.repo.save(@file.full_path, current_user)
|
49
|
+
redirect to('/')
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def get_file
|
55
|
+
Artifact.open_file(params[:splat][0]) or raise 'File not found!'
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Artifact
|
4
|
+
|
5
|
+
module Helpers
|
6
|
+
|
7
|
+
#########################################
|
8
|
+
# main
|
9
|
+
#########################################
|
10
|
+
|
11
|
+
def app_name
|
12
|
+
'Artifact'
|
13
|
+
end
|
14
|
+
|
15
|
+
def action
|
16
|
+
request.path_info.gsub('/','').empty? ? 'home' : request.path_info.gsub('/',' ')
|
17
|
+
end
|
18
|
+
|
19
|
+
def requires!(required, hash = params)
|
20
|
+
if required.is_a?(Hash)
|
21
|
+
required.each { |k, vals| requires!(vals, hash[k]) }
|
22
|
+
elsif hash.nil? or hash.is_a?(String) or required.any?{ |p| hash[p].nil? }
|
23
|
+
halt(400, 'Missing parameters.')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
#########################################
|
28
|
+
# rendering
|
29
|
+
#########################################
|
30
|
+
|
31
|
+
def partial(name, *args)
|
32
|
+
partial_name = name.to_s["/"] ? name.to_s.reverse.sub("/", "_/").reverse : "_#{name}"
|
33
|
+
erb(partial_name.to_sym, {:layout => false}, *args)
|
34
|
+
end
|
35
|
+
|
36
|
+
def view(view_name, *args)
|
37
|
+
layout = request.xhr? ? false : @mobile_host ? :'layout.mobile' : true
|
38
|
+
erb(view_name.to_sym, {:layout => layout}, *args)
|
39
|
+
end
|
40
|
+
|
41
|
+
#########################################
|
42
|
+
# paths, links
|
43
|
+
#########################################
|
44
|
+
|
45
|
+
def base_path
|
46
|
+
env['SCRIPT_NAME']
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_class_name(klass)
|
50
|
+
klass.name.to_s.split("::").last
|
51
|
+
end
|
52
|
+
|
53
|
+
def guess_model_path(m)
|
54
|
+
base_path + '/' + get_class_name(m).pluralize.downcase
|
55
|
+
end
|
56
|
+
|
57
|
+
def url_for(object)
|
58
|
+
object.nil? ? "#" : object.is_a?(String) ? object \
|
59
|
+
: object.respond_to?(:path) ? object.path : "#{guess_model_path(object.class)}/#{object.to_param}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def link_to(string, resource)
|
63
|
+
"<a href='#{url_for(resource)}'>#{string}</a>"
|
64
|
+
end
|
65
|
+
|
66
|
+
def active?(link)
|
67
|
+
request.path[link] ? 'active' : ''
|
68
|
+
end
|
69
|
+
|
70
|
+
def view_upload_path(file)
|
71
|
+
url(upload_relative_path(file))
|
72
|
+
end
|
73
|
+
|
74
|
+
def upload_relative_path(file)
|
75
|
+
# we need to start with a /, otherwise middleman will insert his image_path as prefix
|
76
|
+
'/' + Artifact.config.uploads_path + file.path.sub(Artifact.uploads.path, '')
|
77
|
+
end
|
78
|
+
|
79
|
+
def is_image?(filename)
|
80
|
+
filename.match(/\.(png|jpg|jpeg|gif|bmp)$/i)
|
81
|
+
end
|
82
|
+
|
83
|
+
def post_preview_path(post)
|
84
|
+
str = post.path.sub(Artifact.config.source_root, '')
|
85
|
+
if Artifact.config.directory_indexes
|
86
|
+
str.sub(post.filename, post.filename.split('.').first)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def delete_link(resource, options = {})
|
91
|
+
css_class = options[:class] || ''
|
92
|
+
text = options[:text] || 'Delete'
|
93
|
+
path = url_for(resource)
|
94
|
+
'<form class="destroy" style="display:inline" method="post" action="'+ path +'" onsubmit="return confirm(\'Are you sure?\');">
|
95
|
+
<input type="hidden" name="_method" value="delete" />
|
96
|
+
<button type="submit" class="' + css_class +' btn btn-danger">' + text + '</button>
|
97
|
+
</form>'
|
98
|
+
end
|
99
|
+
|
100
|
+
def in_words(time)
|
101
|
+
minutes = (((Time.now - time).abs)/60).round
|
102
|
+
return nil if minutes < 0
|
103
|
+
|
104
|
+
case minutes
|
105
|
+
when 0..1 then 'less than a minute'
|
106
|
+
when 2..4 then 'less than 5 minutes'
|
107
|
+
when 5..14 then 'less than 15 minutes'
|
108
|
+
when 15..29 then "half an hour"
|
109
|
+
when 30..59 then "#{minutes} minutes"
|
110
|
+
when 60..119 then '1 hour'
|
111
|
+
when 120..239 then '2 hours'
|
112
|
+
when 240..479 then '4 hours'
|
113
|
+
when 480..719 then '8 hours'
|
114
|
+
when 720..1439 then '12 hours'
|
115
|
+
when 1440..11519 then "#{(minutes/1440).floor} days"
|
116
|
+
when 11520..43199 then "#{(minutes/11520).floor} weeks"
|
117
|
+
when 43200..525599 then "#{(minutes/43200).floor} months"
|
118
|
+
else "#{(minutes/525600).floor} years"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def time_ago_in_words(time)
|
123
|
+
if str = in_words(time)
|
124
|
+
"#{str} ago"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'app')
|
2
|
+
require 'active_support'
|
3
|
+
|
4
|
+
class Middleman::Sitemap::Store
|
5
|
+
|
6
|
+
alias_method :original_initialize, :initialize
|
7
|
+
|
8
|
+
def initialize(app)
|
9
|
+
Artifact.middleman = app
|
10
|
+
original_initialize(app)
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
class Middleman::CoreExtensions::FrontMatter
|
16
|
+
|
17
|
+
# extension's clear_data method doesn't work
|
18
|
+
# because it uses the full file path, while
|
19
|
+
# the cache setter uses only the relative path
|
20
|
+
def flush_cache!
|
21
|
+
@cache = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
module Artifact
|
27
|
+
|
28
|
+
def self.middleman=(app)
|
29
|
+
@app = app
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.middleman
|
33
|
+
@app
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.source_root
|
37
|
+
File.join(config.root, config.source_root)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.posts
|
41
|
+
@posts ||= Tree.new(File.join(source_root, Artifact.config.posts_path))
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.drafts
|
45
|
+
opts = {:new_file_path => ''} # flat, no date
|
46
|
+
@drafts ||= Tree.new(File.join(source_root, Artifact.config.drafts_path), opts)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.uploads
|
50
|
+
@uploads ||= Tree.new(File.join(source_root, Artifact.config.uploads_path))
|
51
|
+
end
|
52
|
+
|
53
|
+
class Middleman < App
|
54
|
+
|
55
|
+
POST_EXTENSION = '.html.markdown'
|
56
|
+
|
57
|
+
get '/' do
|
58
|
+
redirect to('/posts')
|
59
|
+
end
|
60
|
+
|
61
|
+
post '/build' do
|
62
|
+
# updated! and rebuild!
|
63
|
+
rebuild!
|
64
|
+
redirect to('/')
|
65
|
+
end
|
66
|
+
|
67
|
+
get '/search' do
|
68
|
+
if params[:q].present?
|
69
|
+
normalized = params[:q].gsub(' ', '-').downcase
|
70
|
+
@list = filter_and_sort(Artifact.posts.find(normalized), :date)
|
71
|
+
else
|
72
|
+
@list = filter_and_sort(Artifact.posts.all.last(10), :date)
|
73
|
+
end
|
74
|
+
partial 'posts/list'
|
75
|
+
end
|
76
|
+
|
77
|
+
get '/posts' do
|
78
|
+
@posts = filter_and_sort(Artifact.posts.all.last(10), :date)
|
79
|
+
@drafts = filter_and_sort(Artifact.drafts.all, :last_modified)
|
80
|
+
view 'posts/index'
|
81
|
+
end
|
82
|
+
|
83
|
+
get '/posts/new' do
|
84
|
+
view 'posts/new'
|
85
|
+
end
|
86
|
+
|
87
|
+
post '/posts' do
|
88
|
+
data = params[:meta] # includes title and maybe something else
|
89
|
+
filename = data[:title].parameterize
|
90
|
+
|
91
|
+
@file = Artifact.drafts.new(filename, POST_EXTENSION)
|
92
|
+
@file.update(params[:content], data)
|
93
|
+
redirect to('/posts')
|
94
|
+
end
|
95
|
+
|
96
|
+
get '/drafts/*' do
|
97
|
+
@post = MarkdownFile.new(params[:splat][0])
|
98
|
+
@draft = true
|
99
|
+
view 'posts/edit'
|
100
|
+
end
|
101
|
+
|
102
|
+
get '/posts/*' do
|
103
|
+
@post = MarkdownFile.new(params[:splat][0])
|
104
|
+
view 'posts/edit'
|
105
|
+
end
|
106
|
+
|
107
|
+
post '/posts/*' do
|
108
|
+
@post = MarkdownFile.new(params[:splat][0])
|
109
|
+
|
110
|
+
meta = (params[:meta] || {}).merge('last_updated_by' => current_user[:email])
|
111
|
+
@post.update(params[:content].gsub("\r\n", "\n"), meta)
|
112
|
+
|
113
|
+
if params[:save] # user clicked on 'save' or 'published'
|
114
|
+
# if draft was previously saved this will raise 'coz no changes.
|
115
|
+
Artifact.repo.save(@post.path, current_user, false)
|
116
|
+
|
117
|
+
if params[:save] == 'Publish'
|
118
|
+
@post.update_meta(:date, Time.now.utc.strftime("%Y-%m-%d %H:%M %Z"))
|
119
|
+
publish_post!(@post)
|
120
|
+
elsif params[:save] == 'Unpublish'
|
121
|
+
unpublish_post!(@post)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
updated!
|
126
|
+
redirect to('/posts')
|
127
|
+
end
|
128
|
+
|
129
|
+
delete '/posts/*' do
|
130
|
+
@post = MarkdownFile.new(params[:splat][0])
|
131
|
+
@post.delete
|
132
|
+
Artifact.repo.remove(@post.path, current_user) if Artifact.repo.exists?(@post.path)
|
133
|
+
updated!
|
134
|
+
|
135
|
+
redirect to('/posts')
|
136
|
+
end
|
137
|
+
|
138
|
+
get '/uploads/*' do
|
139
|
+
file = params[:splat].first
|
140
|
+
send_file File.join(Artifact.uploads.path, file)
|
141
|
+
end
|
142
|
+
|
143
|
+
get '/uploads' do
|
144
|
+
get_uploads
|
145
|
+
# @uploads.select! {|i| i.match(/\.(png|jpg|jpeg|gif)/i) }
|
146
|
+
partial 'uploads/list'
|
147
|
+
end
|
148
|
+
|
149
|
+
post '/uploads' do
|
150
|
+
unless params[:file] &&
|
151
|
+
(tmpfile = params[:file][:tempfile]) &&
|
152
|
+
(name = params[:file][:filename])
|
153
|
+
raise 'No file uploaded.'
|
154
|
+
else
|
155
|
+
filename = File.basename(name, File.extname(name)).parameterize # without extension
|
156
|
+
file = Artifact.uploads.new(filename, File.extname(name))
|
157
|
+
file.update(tmpfile.read)
|
158
|
+
Artifact.repo.save(file.path, current_user) # pass relative path
|
159
|
+
|
160
|
+
updated!
|
161
|
+
redirect to('/')
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
def get_uploads
|
168
|
+
@uploads = Artifact.uploads.all.last(10).reverse
|
169
|
+
end
|
170
|
+
|
171
|
+
def filter_and_sort(posts, criteria)
|
172
|
+
posts.select { |f| f.is_a?(WritableFile) }.sort do |a,b|
|
173
|
+
b.send(criteria) <=> a.send(criteria)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def publish_post!(post)
|
178
|
+
filename = post.title.parameterize
|
179
|
+
new_file = Artifact.posts.new(filename, POST_EXTENSION)
|
180
|
+
Artifact.repo.move(post.path, new_file.path, current_user)
|
181
|
+
end
|
182
|
+
|
183
|
+
def unpublish_post!(post)
|
184
|
+
filename = post.title.parameterize
|
185
|
+
new_file = Artifact.drafts.new(filename, POST_EXTENSION)
|
186
|
+
Artifact.repo.move(post.path, new_file.path, current_user)
|
187
|
+
end
|
188
|
+
|
189
|
+
def rebuild!
|
190
|
+
puts `bundle exec middleman build`
|
191
|
+
end
|
192
|
+
|
193
|
+
def updated!
|
194
|
+
return if Artifact.middleman.nil? # not loaded yet!
|
195
|
+
|
196
|
+
puts "Reloading Middleman..."
|
197
|
+
Artifact.middleman.files.reload_path(Artifact.drafts.path)
|
198
|
+
Artifact.middleman.files.reload_path(Artifact.posts.path)
|
199
|
+
Artifact.middleman.files.reload_path(Artifact.uploads.path)
|
200
|
+
|
201
|
+
Artifact.middleman.cache.clear # Tilt (template) cache
|
202
|
+
# Artifact.middleman.sitemap.send(:reset_lookup_cache!)
|
203
|
+
Artifact.middleman.sitemap.rebuild_resource_list!
|
204
|
+
|
205
|
+
if Artifact.middleman.extensions[:frontmatter]
|
206
|
+
puts "Flushing frontmatter cache..."
|
207
|
+
Artifact.middleman.extensions[:frontmatter].flush_cache!
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|