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