serif 0.4 → 0.5
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/Gemfile.lock +29 -14
- data/README.md +28 -14
- data/bin/serif +2 -69
- data/lib/serif.rb +1 -0
- data/lib/serif/admin_server.rb +55 -26
- data/lib/serif/commands.rb +150 -0
- data/lib/serif/content_file.rb +4 -0
- data/lib/serif/draft.rb +24 -2
- data/lib/serif/errors.rb +10 -0
- data/lib/serif/post.rb +4 -3
- data/lib/serif/site.rb +73 -4
- data/rakefile +10 -0
- data/serif.gemspec +5 -3
- data/statics/skeleton/_layouts/default.html +1 -1
- data/statics/templates/admin/bookmarks.liquid +17 -13
- data/statics/templates/admin/edit_draft.liquid +1 -1
- data/statics/templates/admin/edit_post.liquid +1 -1
- data/statics/templates/admin/index.liquid +2 -2
- data/statics/templates/admin/layout.liquid +16 -0
- data/test/commands_spec.rb +77 -0
- data/test/content_file_spec.rb +32 -1
- data/test/draft_spec.rb +50 -3
- data/test/post_spec.rb +31 -2
- data/test/site_dir/_layouts/default.html +2 -0
- data/test/site_dir/_site/archive.html +2 -0
- data/test/site_dir/_site/drafts/another-sample-draft/{481da12b79709bfa0547fa9b5754c9506fbed29afd0334e07a8c95e76850.html → add25848a94509103cb492c47e3a04b7b2a56299de207155fbffec42dc4b.html} +5 -2
- data/test/site_dir/_site/drafts/sample-draft/{a986a62ad5f6edd1fcac3d08f5b461b92bcb667a2af69505230c291d405c.html → 0b6fc164b8534d5d5a9fcfc5c709265d33f1577cd0fe2f4e23042e92f0c1.html} +5 -2
- data/test/site_dir/_site/index.html +2 -0
- data/test/site_dir/_site/page-header-but-no-layout.html +2 -0
- data/test/site_dir/_site/test-archive/2012/11.html +2 -0
- data/test/site_dir/_site/test-archive/2012/12.html +2 -0
- data/test/site_dir/_site/test-archive/2013/01.html +2 -0
- data/test/site_dir/_site/test-archive/2013/03.html +2 -0
- data/test/site_dir/_site/test-archive/2399/01.html +2 -0
- data/test/site_dir/_site/test-archive/2400/01.html +2 -0
- data/test/site_dir/_site/test-blog/final-post.html +4 -1
- data/test/site_dir/_site/test-blog/penultimate-post.html +4 -1
- data/test/site_dir/_site/test-blog/post-to-be-published-on-generate.html +4 -1
- data/test/site_dir/_site/test-blog/post-with-custom-layout.html +1 -0
- data/test/site_dir/_site/test-blog/sample-post.html +4 -1
- data/test/site_dir/_site/test-blog/second-post.html +4 -1
- data/test/site_dir/_site/test-smarty-filter.html +2 -0
- data/test/site_dir/_templates/post.html +1 -0
- data/test/site_dir/_trash/1364747613-autopublish-draft +5 -0
- data/test/site_dir/_trash/{1363633154-test-draft → 1364747613-test-draft} +1 -1
- data/test/site_generation_spec.rb +40 -9
- data/test/site_spec.rb +63 -0
- data/test/test_helper.rb +9 -0
- metadata +46 -10
- data/test/site_dir/_trash/1363633154-autopublish-draft +0 -5
data/lib/serif/content_file.rb
CHANGED
data/lib/serif/draft.rb
CHANGED
@@ -11,6 +11,27 @@ class Draft < ContentFile
|
|
11
11
|
File.rename("#{site.directory}/#{dirname}/#{original_slug}", "#{site.directory}/#{dirname}/#{new_slug}")
|
12
12
|
end
|
13
13
|
|
14
|
+
# Returns the URL that would be used for this post if it were
|
15
|
+
# to be published now.
|
16
|
+
def url
|
17
|
+
permalink_style = headers[:permalink] || site.config.permalink
|
18
|
+
|
19
|
+
parts = {
|
20
|
+
"title" => slug.to_s,
|
21
|
+
"year" => Time.now.year.to_s,
|
22
|
+
"month" => Time.now.month.to_s.rjust(2, "0"),
|
23
|
+
"day" => Time.now.day.to_s.rjust(2, "0")
|
24
|
+
}
|
25
|
+
|
26
|
+
output = permalink_style
|
27
|
+
|
28
|
+
parts.each do |placeholder, value|
|
29
|
+
output = output.gsub(Regexp.quote(":" + placeholder), value)
|
30
|
+
end
|
31
|
+
|
32
|
+
output
|
33
|
+
end
|
34
|
+
|
14
35
|
def delete!
|
15
36
|
FileUtils.mkdir_p("#{site.directory}/_trash")
|
16
37
|
File.rename(@path, File.expand_path("#{site.directory}/_trash/#{Time.now.to_i}-#{slug}"))
|
@@ -33,7 +54,7 @@ class Draft < ContentFile
|
|
33
54
|
File.rename(path, full_published_path)
|
34
55
|
|
35
56
|
# update the path since the file has now changed
|
36
|
-
@path = Post.
|
57
|
+
@path = Post.new(site, full_published_path).path
|
37
58
|
end
|
38
59
|
|
39
60
|
# if the assigned value is truthy, the "publish" header
|
@@ -63,7 +84,8 @@ class Draft < ContentFile
|
|
63
84
|
"slug" => slug,
|
64
85
|
"type" => "draft",
|
65
86
|
"draft" => draft?,
|
66
|
-
"published" => published
|
87
|
+
"published" => published?,
|
88
|
+
"url" => url
|
67
89
|
}
|
68
90
|
|
69
91
|
headers.each do |key, value|
|
data/lib/serif/errors.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
module Serif
|
2
|
+
# General error class. Allows capturing general errors
|
3
|
+
# applicable only to Serif.
|
4
|
+
class Error < RuntimeError; end
|
5
|
+
|
6
|
+
# Represents a conflict between published posts and drafts.
|
7
|
+
# This should be used whenever two posts would occupy the same
|
8
|
+
# URL / file path.
|
9
|
+
class PostConflictError < Error; end
|
10
|
+
end
|
data/lib/serif/post.rb
CHANGED
@@ -62,8 +62,8 @@ class Post < ContentFile
|
|
62
62
|
files.map { |f| new(site, f) }
|
63
63
|
end
|
64
64
|
|
65
|
-
def self.
|
66
|
-
all(site).find { |p| p.
|
65
|
+
def self.from_basename(site, filename)
|
66
|
+
all(site).find { |p| p.basename == filename }
|
67
67
|
end
|
68
68
|
|
69
69
|
def to_liquid
|
@@ -76,7 +76,8 @@ class Post < ContentFile
|
|
76
76
|
"url" => url,
|
77
77
|
"type" => "post",
|
78
78
|
"draft" => draft?,
|
79
|
-
"published" => published
|
79
|
+
"published" => published?,
|
80
|
+
"basename" => basename
|
80
81
|
}
|
81
82
|
|
82
83
|
headers.each do |key, value|
|
data/lib/serif/site.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
|
2
|
-
module Liquid
|
2
|
+
module Liquid #:nodoc:#
|
3
|
+
module StandardFilters #:nodoc:#
|
3
4
|
alias_method :date_orig, :date
|
4
5
|
|
5
6
|
def date(input, format)
|
6
7
|
input == "now" ? date_orig(Time.now, format) : date_orig(input, format)
|
7
8
|
end
|
8
9
|
end
|
10
|
+
end
|
9
11
|
|
10
12
|
module Serif
|
11
13
|
module Filters
|
@@ -225,6 +227,67 @@ class Site
|
|
225
227
|
h
|
226
228
|
end
|
227
229
|
|
230
|
+
# Returns a hash representing any conflicting URLs,
|
231
|
+
# in the form
|
232
|
+
#
|
233
|
+
# { "/url_1" => [e_1, e_2, ..., e_n], ... }
|
234
|
+
#
|
235
|
+
# The elements e_i are the conflicting Post and
|
236
|
+
# Draft instances that share the URL "/url_1".
|
237
|
+
#
|
238
|
+
# Note that if n = 1 (that is, the array value is
|
239
|
+
# [e_1], containing a single element), then it is
|
240
|
+
# not included in the Hash, since it does not
|
241
|
+
# contribute to a conflict.
|
242
|
+
#
|
243
|
+
# If there are no conflicts found, returns nil.
|
244
|
+
#
|
245
|
+
# If an argument is specified, its #url value is
|
246
|
+
# compared against all post and draft URLs, and
|
247
|
+
# the value returned is either:
|
248
|
+
#
|
249
|
+
# 1. an array of post/draft instances that
|
250
|
+
# conflict, _including_ the argument given; or,
|
251
|
+
# 2. nil if there is no conflict.
|
252
|
+
def conflicts(content_to_check = nil)
|
253
|
+
if content_to_check
|
254
|
+
content = drafts + posts + [content_to_check]
|
255
|
+
|
256
|
+
# In the event that the given argument is actually one of the
|
257
|
+
# drafts + posts, we need to de-duplicate, otherwise our return
|
258
|
+
# value will contain two of the same Draft/Post, which isn't
|
259
|
+
# actually a conflict.
|
260
|
+
#
|
261
|
+
# So to do that, we can use the path on the filesystem. However,
|
262
|
+
# we can't just rely on calling #path, because if content_to_check
|
263
|
+
# doesn't have a #path value, it'll be nil and it's possible that
|
264
|
+
# we might expand checking to multiple files/Drafts/Posts.
|
265
|
+
#
|
266
|
+
# Thus, if #path is nil, simply rely on object_id.
|
267
|
+
#
|
268
|
+
# FIXME: Replace this with a proper implementation of
|
269
|
+
# ContentFile equality/hashing.
|
270
|
+
content.uniq! { |e| e.path ? e.path : e.object_id }
|
271
|
+
|
272
|
+
conflicts = content.select { |e| e.url == content_to_check.url }
|
273
|
+
|
274
|
+
if conflicts.length <= 1
|
275
|
+
return nil
|
276
|
+
else
|
277
|
+
return conflicts
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
conflicts = (drafts + posts).group_by { |e| e.url }
|
282
|
+
conflicts.reject! { |k, v| v.length == 1 }
|
283
|
+
|
284
|
+
if conflicts.empty?
|
285
|
+
nil
|
286
|
+
else
|
287
|
+
conflicts
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
228
291
|
def to_liquid
|
229
292
|
@liquid_cache_store ||= TimeoutCache.new(1)
|
230
293
|
|
@@ -249,6 +312,10 @@ class Site
|
|
249
312
|
|
250
313
|
default_layout = Liquid::Template.parse(File.read("_layouts/default.html"))
|
251
314
|
|
315
|
+
if conflicts
|
316
|
+
raise PostConflictError, "Generating would cause a conflict."
|
317
|
+
end
|
318
|
+
|
252
319
|
# preprocess any drafts marked for autopublish, before grabbing the posts
|
253
320
|
# to operate on.
|
254
321
|
preprocess_autopublish_drafts
|
@@ -292,7 +359,7 @@ class Site
|
|
292
359
|
layout_file = File.join(self.directory, "_layouts", "#{layout_option}.html")
|
293
360
|
layout = Liquid::Template.parse(File.read(layout_file))
|
294
361
|
end
|
295
|
-
f.puts layout.render!("site" => self, "page" => { "title" =>
|
362
|
+
f.puts layout.render!("site" => self, "page" => { "title" => title }, "content" => Liquid::Template.parse(file.to_s).render!("site" => self))
|
296
363
|
end
|
297
364
|
end
|
298
365
|
end
|
@@ -323,13 +390,15 @@ class Site
|
|
323
390
|
# variables available in the post template
|
324
391
|
post_template_variables = {
|
325
392
|
"post" => post,
|
393
|
+
"post_page" => true,
|
326
394
|
"prev_post" => prev_post,
|
327
395
|
"next_post" => next_post
|
328
396
|
}
|
329
397
|
|
330
398
|
f.puts post_layout.render!(
|
331
399
|
"site" => self,
|
332
|
-
"page" => { "title" =>
|
400
|
+
"page" => { "title" => post.title },
|
401
|
+
"post_page" => true,
|
333
402
|
"content" => Liquid::Template.parse(File.read("_templates/post.html")).render!(post_template_variables)
|
334
403
|
)
|
335
404
|
end
|
@@ -393,7 +462,7 @@ class Site
|
|
393
462
|
f.puts layout.render!(
|
394
463
|
"site" => self,
|
395
464
|
"draft_preview" => true,
|
396
|
-
"page" => { "title" =>
|
465
|
+
"page" => { "title" => draft.title },
|
397
466
|
"content" => template.render!("site" => self, "post" => draft, "draft_preview" => true)
|
398
467
|
)
|
399
468
|
end
|
data/rakefile
CHANGED
@@ -2,6 +2,7 @@ require "rake"
|
|
2
2
|
require "time"
|
3
3
|
require "rspec/core/rake_task"
|
4
4
|
require "benchmark"
|
5
|
+
require "rdoc/task"
|
5
6
|
|
6
7
|
RSpec::Core::RakeTask.new(:test) do |t|
|
7
8
|
t.rspec_opts = "-I test --color --format nested"
|
@@ -12,6 +13,15 @@ end
|
|
12
13
|
|
13
14
|
task :default => :test
|
14
15
|
|
16
|
+
Rake::RDocTask.new(:docs) do |rd|
|
17
|
+
rd.main = "README.md"
|
18
|
+
rd.rdoc_dir = "docs"
|
19
|
+
rd.rdoc_files.include("README.md", "lib/**/*.rb")
|
20
|
+
rd.options << "--markup=markdown"
|
21
|
+
end
|
22
|
+
|
23
|
+
task :rdoc => :docs
|
24
|
+
|
15
25
|
task :stress, [:n] do |t, args|
|
16
26
|
iterations = args[:n] || 250
|
17
27
|
iterations = iterations.to_i
|
data/serif.gemspec
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "serif"
|
3
|
-
s.version = "0.
|
3
|
+
s.version = "0.5"
|
4
4
|
s.authors = ["Adam Prescott"]
|
5
5
|
s.email = ["adam@aprescott.com"]
|
6
6
|
s.homepage = "https://github.com/aprescott/serif"
|
7
|
-
s.summary = "
|
8
|
-
s.description = "Serif is a blogging system powered by markdown files and an optional admin interface complete with drag-and-drop image uploading."
|
7
|
+
s.summary = "Static site generator and markdown-based blogging with an optional admin interface complete with drag-and-drop image uploading."
|
8
|
+
s.description = "Serif is a static site generator and blogging system powered by markdown files and an optional admin interface complete with drag-and-drop image uploading."
|
9
9
|
s.files = Dir["{lib/**/*,statics/**/*,bin/*,test/**/*}"] + %w[serif.gemspec rakefile LICENSE Gemfile Gemfile.lock README.md]
|
10
10
|
s.require_path = "lib"
|
11
11
|
s.bindir = "bin"
|
@@ -30,4 +30,6 @@ Gem::Specification.new do |s|
|
|
30
30
|
s.add_development_dependency("rspec", "~> 2.5")
|
31
31
|
s.add_development_dependency("simplecov", "~> 0.7")
|
32
32
|
s.add_development_dependency("timecop", "~> 0.6.1")
|
33
|
+
s.add_development_dependency("rdoc", "~> 4.0.0")
|
34
|
+
s.add_development_dependency("coveralls")
|
33
35
|
end
|
@@ -25,17 +25,17 @@ compress me!
|
|
25
25
|
-->
|
26
26
|
{% endcomment %}
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
{
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
28
|
+
<script>
|
29
|
+
var compressed_js = 'javascript:(function(){var e,n,t,o;n=location.href,document.getSelection?(t=document.getSelection(),""==""+t?t="":(t=t.getRangeAt(0).cloneContents(),o=document.createElement("div"),o.appendChild(t),t=o.innerHTML)):t="",e=document.title,window.location.href="[BASE_URL]url="+encodeURIComponent(n)+"&content="+encodeURIComponent(t)+"&title="+encodeURIComponent(e)})();';
|
30
|
+
var base_url = window.location.protocol + "//" + window.location.host + "/admin/quick-draft?";
|
31
|
+
var js_now = compressed_js.replace('[BASE_URL]', base_url + "edit=1&");
|
32
|
+
var js_later = compressed_js.replace('[BASE_URL]', base_url);
|
33
|
+
|
34
|
+
$(function() {
|
35
|
+
$("[data-bookmark-later]").attr("href", js_later);
|
36
|
+
$("[data-bookmark-now]").attr("href", js_now);
|
37
|
+
});
|
38
|
+
</script>
|
39
39
|
|
40
40
|
<section id="bookmarks">
|
41
41
|
<h2>Bookmarks</h2>
|
@@ -44,13 +44,17 @@ compress me!
|
|
44
44
|
|
45
45
|
<p>Any selected text will go in as Markdown, ready for editing!</p>
|
46
46
|
|
47
|
+
<noscript>
|
48
|
+
<p><strong>You need to enable JavaScript!</strong></p>
|
49
|
+
</noscript>
|
50
|
+
|
47
51
|
<hr>
|
48
52
|
|
49
|
-
<p class="bookmark"><a href="
|
53
|
+
<p class="bookmark"><a href="#" data-bookmark-later>Save draft for later</a></p>
|
50
54
|
|
51
55
|
<p>Saves the current page as a draft. Takes you straight back to where you were so you can keep reading.</p>
|
52
56
|
|
53
|
-
<p class="bookmark"><a href="
|
57
|
+
<p class="bookmark"><a href="#" data-bookmark-now>Save draft and edit</a></p>
|
54
58
|
|
55
59
|
<p>Saves the current page as a draft and lets you start editing immediately.</p>
|
56
60
|
</section>
|
@@ -15,7 +15,7 @@
|
|
15
15
|
</h2>
|
16
16
|
|
17
17
|
<h2 id="private-url">
|
18
|
-
<code><span>preview:</span> <a href="{{ private_url }}">{% assign parts = private_url | split:"/" %}/{{ parts[1] }}/{{ parts[2] }}/{{ parts[3] | truncate: 10, '' }}…</a
|
18
|
+
<code><span>preview:</span> {% if private_url %}<a href="{{ private_url }}">{% assign parts = private_url | split:"/" %}/{{ parts[1] }}/{{ parts[2] }}/{{ parts[3] | truncate: 10, '' }}…</a>{% else %}(none){% endif %}</code>
|
19
19
|
</h2>
|
20
20
|
|
21
21
|
<div class="post{% if post.draft %} draft{% endif %}">
|
@@ -4,7 +4,7 @@
|
|
4
4
|
|
5
5
|
<ul>
|
6
6
|
{% for draft in drafts %}
|
7
|
-
<li><a href="/admin/edit/drafts/{{ draft.slug }}">{
|
7
|
+
<li><a href="/admin/edit/drafts/{{ draft.slug }}">{{ draft.slug }}</a></li>
|
8
8
|
{% endfor %}
|
9
9
|
</ul>
|
10
10
|
|
@@ -14,6 +14,6 @@
|
|
14
14
|
|
15
15
|
<ul>
|
16
16
|
{% for post in posts %}
|
17
|
-
<li><a href="/admin/edit/posts/{{ post.
|
17
|
+
<li><a href="/admin/edit/posts/{{ post.basename }}">{{ post.title | smarty }}</a></li>
|
18
18
|
{% endfor %}
|
19
19
|
</ul>
|
@@ -547,6 +547,22 @@ body > nav ul li:last-child {
|
|
547
547
|
</ul>
|
548
548
|
</nav>
|
549
549
|
|
550
|
+
{% if conflicts %}
|
551
|
+
<p>The following URLs have conflicts and must be resolved:</p>
|
552
|
+
<ul>
|
553
|
+
{% for conflict in conflicts %}
|
554
|
+
<li>
|
555
|
+
{{ conflict[0] | escape }}
|
556
|
+
<ul>
|
557
|
+
{% for conflicting_content in conflict[1] %}
|
558
|
+
<li>{% if conflicting_content.draft %}Draft{% else %}Post{% endif %}: {{ conflicting_content.title | escape }}</li>
|
559
|
+
{% endfor %}
|
560
|
+
</ul>
|
561
|
+
</li>
|
562
|
+
{% endfor %}
|
563
|
+
</ul>
|
564
|
+
{% endif %}
|
565
|
+
|
550
566
|
{{ content }}
|
551
567
|
|
552
568
|
<div id="shortcuts">
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class Serif::Commands
|
4
|
+
def exit(code)
|
5
|
+
"Fake exit with code #{code}"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
describe Serif::Commands do
|
10
|
+
def expect_method_call(arg, method)
|
11
|
+
c = Serif::Commands.new([arg])
|
12
|
+
c.should_receive(method)
|
13
|
+
capture_stdout { c.process }
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "#process" do
|
17
|
+
it "takes -h and --help and calls print usage" do
|
18
|
+
%w[-h --help].each do |cmd|
|
19
|
+
expect_method_call(cmd, :print_help)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
{
|
24
|
+
"admin" => :initialize_admin_server,
|
25
|
+
"dev" => :initialize_dev_server,
|
26
|
+
"new" => :produce_skeleton,
|
27
|
+
"generate" => :generate_site
|
28
|
+
}.each do |command, meth|
|
29
|
+
it "takes the command '#{command}' and runs #{meth}" do
|
30
|
+
expect_method_call(command, meth)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it "exits on help" do
|
35
|
+
expect_method_call("-h", :exit)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "#generate_site" do
|
40
|
+
it "calls Site#generate" do
|
41
|
+
Serif::Site.stub(:generation_called)
|
42
|
+
Serif::Site.any_instance.stub(:generate) { Serif::Site.generation_called }
|
43
|
+
|
44
|
+
# if this is called, it means any instance of Site had #generate called.
|
45
|
+
Serif::Site.should_receive(:generation_called)
|
46
|
+
|
47
|
+
Serif::Commands.new([]).generate_site("anything")
|
48
|
+
end
|
49
|
+
|
50
|
+
context "with a conflict" do
|
51
|
+
def conflicting_generate_command
|
52
|
+
a = b = double("")
|
53
|
+
a.stub(:url) { "/foo" }
|
54
|
+
b.stub(:url) { "/foo" }
|
55
|
+
a.stub(:path) { "/anything" }
|
56
|
+
b.stub(:path) { "/anything" }
|
57
|
+
|
58
|
+
# any non-nil value will do
|
59
|
+
Serif::Site.any_instance.stub(:conflicts) { { "/foo" => [a, b] } }
|
60
|
+
|
61
|
+
command = Serif::Commands.new([])
|
62
|
+
command.generate_site(testing_dir)
|
63
|
+
command.should_receive(:exit)
|
64
|
+
command
|
65
|
+
end
|
66
|
+
|
67
|
+
it "exits" do
|
68
|
+
capture_stdout { conflicting_generate_command.process }
|
69
|
+
end
|
70
|
+
|
71
|
+
it "prints the urls that conflict" do
|
72
|
+
output = capture_stdout { conflicting_generate_command.process }
|
73
|
+
output.should match(/Conflicts at:\n\n\/foo\n\t\/anything\n\t\/anything/)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|