serif 0.3.3 → 0.4

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.
Files changed (38) hide show
  1. data/Gemfile.lock +12 -9
  2. data/README.md +57 -32
  3. data/bin/serif +12 -27
  4. data/lib/serif/admin_server.rb +48 -0
  5. data/lib/serif/content_file.rb +23 -29
  6. data/lib/serif/draft.rb +2 -4
  7. data/lib/serif/post.rb +30 -0
  8. data/lib/serif/site.rb +34 -26
  9. data/serif.gemspec +4 -3
  10. data/statics/skeleton/_config.yml +2 -46
  11. data/statics/templates/admin/bookmarks.liquid +56 -0
  12. data/statics/templates/admin/layout.liquid +4 -2
  13. data/test/config_spec.rb +7 -0
  14. data/test/draft_spec.rb +14 -0
  15. data/test/filters_spec.rb +5 -1
  16. data/test/markup_renderer_spec.rb +4 -0
  17. data/test/post_spec.rb +89 -0
  18. data/test/site_dir/_site/drafts/another-sample-draft/{9094f3a34ce2ecfe188ad813e0d3229d2488350fb0c5bca4f8b4bcfe7b11.html → 481da12b79709bfa0547fa9b5754c9506fbed29afd0334e07a8c95e76850.html} +1 -0
  19. data/test/site_dir/_site/drafts/sample-draft/{359dca7a7237a1317c5e8ac2d3a01cd29db433f4caeb0b2209484ca09a7a.html → a986a62ad5f6edd1fcac3d08f5b461b92bcb667a2af69505230c291d405c.html} +1 -0
  20. data/test/site_dir/_site/test-archive/2012/{11/index.html → 11.html} +0 -0
  21. data/test/site_dir/_site/test-archive/2012/{12/index.html → 12.html} +0 -0
  22. data/test/site_dir/_site/test-archive/2013/{01/index.html → 01.html} +0 -0
  23. data/test/site_dir/_site/test-archive/2013/{03/index.html → 03.html} +0 -0
  24. data/test/site_dir/_site/test-archive/2399/{01/index.html → 01.html} +0 -0
  25. data/test/site_dir/_site/test-archive/2400/{01/index.html → 01.html} +0 -0
  26. data/test/site_dir/_site/test-blog/final-post.html +1 -0
  27. data/test/site_dir/_site/test-blog/penultimate-post.html +1 -0
  28. data/test/site_dir/_site/test-blog/post-to-be-published-on-generate.html +1 -0
  29. data/test/site_dir/_site/test-blog/post-with-custom-layout.html +1 -0
  30. data/test/site_dir/_site/test-blog/sample-post.html +1 -0
  31. data/test/site_dir/_site/test-blog/second-post.html +1 -0
  32. data/test/site_dir/_trash/1363633154-autopublish-draft +5 -0
  33. data/test/site_dir/_trash/{1363284991-test-draft → 1363633154-test-draft} +1 -1
  34. data/test/site_generation_spec.rb +39 -11
  35. data/test/site_spec.rb +28 -0
  36. data/test/test_helper.rb +33 -1
  37. metadata +34 -17
  38. data/test/site_dir/_trash/1363284991-autopublish-draft +0 -5
@@ -1,25 +1,9 @@
1
- class StandardFilterCheck
2
- include Liquid::StandardFilters
3
-
4
- def date_supports_now?
5
- begin
6
- date("now", "%Y") == Time.now.year
7
- rescue
8
- false
9
- end
10
- end
11
- end
12
1
 
13
- if StandardFilterCheck.new.date_supports_now?
14
- puts "NOTICE! 'now' is supported by 'date' filter. Remove the patch"
15
- sleep 5 # incur a penalty
16
- else
17
- module Liquid::StandardFilters
18
- alias_method :date_orig, :date
2
+ module Liquid::StandardFilters
3
+ alias_method :date_orig, :date
19
4
 
20
- def date(input, format)
21
- input == "now" ? date_orig(Time.now, format) : date_orig(input, format)
22
- end
5
+ def date(input, format)
6
+ input == "now" ? date_orig(Time.now, format) : date_orig(input, format)
23
7
  end
24
8
  end
25
9
 
@@ -40,7 +24,19 @@ module Filters
40
24
  end
41
25
 
42
26
  def markdown(body)
43
- Redcarpet::Markdown.new(Serif::MarkupRenderer, fenced_code_blocks: true).render(body).strip
27
+ renderer = Redcarpet::Markdown.new(Serif::MarkupRenderer, fenced_code_blocks: true)
28
+ html = renderer.render(body).strip
29
+
30
+ # make sure we aren't overriding unless we need to.
31
+ # causes the workaround to automatically turn off.
32
+ if !(renderer.render("a 'quoted' word").include?("’"))
33
+ # fix the broken single curly quotes by putting them back
34
+ # as unescaped characters and then re-running the renderer.
35
+ html.gsub!("'", "'")
36
+ html = renderer.render(html)
37
+ end
38
+
39
+ html
44
40
  end
45
41
 
46
42
  def xmlschema(input)
@@ -194,7 +190,7 @@ class Site
194
190
  year_groups.map! do |year_start_date, posts_by_year|
195
191
  {
196
192
  :date => year_start_date,
197
- :posts => posts_by_year.sort_by { |post| post.created }
193
+ :posts => posts_by_year.sort_by { |post| post.created }.reverse
198
194
  }
199
195
  end
200
196
 
@@ -211,7 +207,7 @@ class Site
211
207
  month_groups.map! do |month_start_date, posts_by_month|
212
208
  {
213
209
  :date => month_start_date,
214
- :posts => posts_by_month.sort_by { |post| post.created },
210
+ :posts => posts_by_month.sort_by { |post| post.created }.reverse,
215
211
  :archive_url => archive_url_for_date(month_start_date)
216
212
  }
217
213
  end
@@ -257,6 +253,9 @@ class Site
257
253
  # to operate on.
258
254
  preprocess_autopublish_drafts
259
255
 
256
+ # preprocess any posts that might have had an update flag set in the header
257
+ preprocess_autoupdate_posts
258
+
260
259
  posts = self.posts
261
260
 
262
261
  files.each do |path|
@@ -341,7 +340,7 @@ class Site
341
340
  generate_archives(default_layout)
342
341
 
343
342
  if Dir.exist?("_site")
344
- FileUtils.mv("_site", "/tmp/_site.#{Time.now.strftime("%Y-%m-%d-%H-%M-%S")}")
343
+ FileUtils.mv("_site", "/tmp/_site.#{Time.now.strftime("%Y-%m-%d-%H-%M-%S.%6N")}")
345
344
  end
346
345
 
347
346
  FileUtils.mv("tmp/_site", ".") && FileUtils.rm_rf("tmp/_site")
@@ -350,6 +349,15 @@ class Site
350
349
 
351
350
  private
352
351
 
352
+ def preprocess_autoupdate_posts
353
+ posts.each do |p|
354
+ if p.autoupdate?
355
+ puts "Auto-updating timestamp for: #{p.title} / #{p.slug}"
356
+ p.update!
357
+ end
358
+ end
359
+ end
360
+
353
361
  # generates draft preview files for any unpublished drafts.
354
362
  #
355
363
  # uses the same template as live posts.
@@ -418,8 +426,8 @@ class Site
418
426
 
419
427
  FileUtils.mkdir_p(archive_path)
420
428
 
421
- File.open(File.join(archive_path, "index.html"), "w") do |f|
422
- f.puts layout.render!("site" => self, "content" => template.render!("site" => self, "month" => month, "posts" => posts))
429
+ File.open(File.join(archive_path + ".html"), "w") do |f|
430
+ f.puts layout.render!("archive_page" => true, "month" => month, "site" => self, "content" => template.render!("archive_page" => true, "site" => self, "month" => month, "posts" => posts))
423
431
  end
424
432
  end
425
433
  end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "serif"
3
- s.version = "0.3.3"
3
+ s.version = "0.4"
4
4
  s.authors = ["Adam Prescott"]
5
5
  s.email = ["adam@aprescott.com"]
6
6
  s.homepage = "https://github.com/aprescott/serif"
@@ -19,7 +19,8 @@ Gem::Specification.new do |s|
19
19
  "sinatra", "~> 1.3",
20
20
  "redhead", "~> 0.0.8",
21
21
  "liquid", "~> 2.4",
22
- "slop", "~> 3.3",
22
+ "reverse_markdown", "~> 0.4.3",
23
+ "nokogiri", "~> 1.5",
23
24
  "timeout_cache"
24
25
  ].each_slice(2) do |name, version|
25
26
  s.add_runtime_dependency(name, version)
@@ -28,5 +29,5 @@ Gem::Specification.new do |s|
28
29
  s.add_development_dependency("rake", "~> 0.9")
29
30
  s.add_development_dependency("rspec", "~> 2.5")
30
31
  s.add_development_dependency("simplecov", "~> 0.7")
31
- s.add_development_dependency("timecop", "~> 0.5.5")
32
+ s.add_development_dependency("timecop", "~> 0.6.1")
32
33
  end
@@ -1,52 +1,8 @@
1
- # This is the Serif config file. It must be a valid YAML document.
2
- #
3
- # Some configuration options:
4
- #
5
- # admin:
6
- # username: [a username for the admin web interface]
7
- # password: [a password for the admin web interface]
8
- # permalink: [permalink format for generated posts]
9
- #
10
- # See the README for information on permalink formats.
11
- #
12
- # Image upload path configuration, used for any images added
13
- # through the admin drag-and-drop interface:
14
- #
15
- # image_upload_path: [/some/absolute/path/:with/:placeholders]
16
- #
17
- # This should be an ABSOLUTE PATH and will be relative to your
18
- # base site (e.g., mysite.com). The value used will have the
19
- # relevant extension appended AUTOMATICALLY.
20
- #
21
- # The default value is: /images/:timestamp_:name
22
1
  #
23
- # Possible placeholders for the image upload path:
24
- #
25
- # :year
26
- # current full year, e.g., "2013"
27
- #
28
- # :month
29
- # current month as a two digit value, e.g., "02"
30
- #
31
- # :day
32
- # current day as a two digit value, e.g., "01"
33
- #
34
- # :timestamp
35
- # unix timestamp in seconds, e.g., "1361057832685"
36
- #
37
- # :name
38
- # filename of the image being uploaded
39
- #
40
- # :slug
41
- # the slug of the post at the time the image is added
42
- #
43
- # Any slashes in this string will become directories relative to the
44
- # base site directory.
2
+ # This is the Serif config file. It must be a valid YAML document.
45
3
  #
46
- # Example: image_upload_path: /images/:slug/:year_:month_:day_:timestamp
4
+ # For information on which values are available here, see the README.
47
5
  #
48
- # This would lead to an example path of
49
- # /images/sample-post/2013_02_16_1361057832685.png
50
6
  admin:
51
7
  username: changethisusername
52
8
  password: changethispassword
@@ -0,0 +1,56 @@
1
+ {% comment %}
2
+ <!--
3
+ compress me!
4
+
5
+ (function () {
6
+ var title, url, selection, container;
7
+ url = location.href;
8
+ if (document.getSelection) {
9
+ selection = document.getSelection();
10
+
11
+ if (selection.toString() == '') {
12
+ selection = '';
13
+ } else {
14
+ selection = selection.getRangeAt(0).cloneContents();
15
+ container = document.createElement('div');
16
+ container.appendChild(selection);
17
+ selection = container.innerHTML;
18
+ }
19
+ } else {
20
+ selection = '';
21
+ };
22
+ title = document.title;
23
+ window.location.href = '[BASE_URL]url=' + encodeURIComponent(url) + '&content=' + encodeURIComponent(selection) + '&title=' + encodeURIComponent(title);
24
+ })();
25
+ -->
26
+ {% endcomment %}
27
+
28
+ {% capture compressed_js %}
29
+
30
+ (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)})();
31
+
32
+ {% endcapture %}
33
+
34
+ {% assign compressed_js = compressed_js | strip | replace:'"',"'" | prepend: "javascript:" %}
35
+ {% assign non_edit_url = base_url | append:"/admin/quick-draft?" %}
36
+ {% assign edit_now_url = non_edit_url | append:"edit=1&" %}
37
+ {% assign js_later = compressed_js | replace:"[BASE_URL]",non_edit_url %}
38
+ {% assign js_now = compressed_js | replace:"[BASE_URL]",edit_now_url %}
39
+
40
+ <section id="bookmarks">
41
+ <h2>Bookmarks</h2>
42
+
43
+ <p>Drag these links to your bookmarks and use them to create new drafts.</p>
44
+
45
+ <p>Any selected text will go in as Markdown, ready for editing!</p>
46
+
47
+ <hr>
48
+
49
+ <p class="bookmark"><a href="{{ js_later }}">Save draft for later</a></p>
50
+
51
+ <p>Saves the current page as a draft. Takes you straight back to where you were so you can keep reading.</p>
52
+
53
+ <p class="bookmark"><a href="{{ js_now }}">Save draft and edit</a></p>
54
+
55
+ <p>Saves the current page as a draft and lets you start editing immediately.</p>
56
+ </section>
@@ -32,10 +32,11 @@ body > nav ul {
32
32
 
33
33
  body > nav ul li {
34
34
  display: inline;
35
+ margin-right: 1em;
35
36
  }
36
37
 
37
38
  body > nav ul li:last-child {
38
- margin-left: 1em;
39
+ margin-right: 0;
39
40
  }
40
41
 
41
42
  #edit input[disabled] {
@@ -537,11 +538,12 @@ body > nav ul li:last-child {
537
538
  });
538
539
  </script>
539
540
  </head>
540
- <body>
541
+ <body id="admin">
541
542
  <nav>
542
543
  <ul>
543
544
  <li><a href="/admin">Admin</a></li>
544
545
  <li><a href="/admin/new/draft">New draft</a></li>
546
+ <li><a href="/admin/bookmarks">Bookmarks</a></li>
545
547
  </ul>
546
548
  </nav>
547
549
 
@@ -17,6 +17,13 @@ describe Serif::Config do
17
17
  end
18
18
  end
19
19
 
20
+ describe "#image_upload_path" do
21
+ it "defaults to /images/:timestamp/_name" do
22
+ subject.stub(:yaml) { {} }
23
+ subject.image_upload_path.should == "/images/:timestamp_:name"
24
+ end
25
+ end
26
+
20
27
  describe "#permalink" do
21
28
  it "is the permalink format defined in the config file" do
22
29
  subject.permalink.should == "/test-blog/:title"
@@ -152,6 +152,20 @@ describe Serif::Draft do
152
152
 
153
153
  draft.delete!
154
154
  end
155
+
156
+ it "carries its value through to #autopublish?" do
157
+ draft = D.new(@site)
158
+ draft.slug = "test-draft"
159
+ draft.title = "Some draft title"
160
+ draft.autopublish = false
161
+ draft.autopublish?.should be_false
162
+
163
+ draft.autopublish = true
164
+ draft.autopublish?.should be_true
165
+
166
+ draft.autopublish = false
167
+ draft.autopublish?.should be_false
168
+ end
155
169
  end
156
170
 
157
171
  describe "#autopublish?" do
@@ -80,7 +80,11 @@ describe Serif::Filters do
80
80
  describe "#markdown" do
81
81
  it "processes its input as markdown" do
82
82
  # bit of a stub test
83
- subject.markdown("# Hi!").should == "<h1>Hi!</h1>"
83
+ subject.markdown("# Hi!").should == "<h1>Hi!</h1>\n"
84
+ end
85
+
86
+ it "uses curly single quotes properly" do
87
+ subject.markdown("# something's test").should include("something&rsquo;s")
84
88
  end
85
89
  end
86
90
  end
@@ -33,6 +33,10 @@ END_SOURCE
33
33
  END_OUTPUT
34
34
  end
35
35
 
36
+ # NOTE: The output here is not the desired output.
37
+ #
38
+ # See vmg/redcarpet#57 and note that any filters that use this renderer
39
+ # are tested elsewhere.
36
40
  it "renders quote marks properly" do
37
41
  subject.render(<<END_SOURCE).should == <<END_OUTPUT
38
42
  This "very" sentence's structure "isn't" necessary.
@@ -9,6 +9,21 @@ describe Serif::Post do
9
9
  @posts = subject.posts
10
10
  end
11
11
 
12
+ around :each do |example|
13
+ begin
14
+ d = Serif::Draft.new(subject)
15
+ d.slug = "foo-bar-bar-temp"
16
+ d.title = "Testing title"
17
+ d.save("# some content")
18
+ d.publish!
19
+ @temporary_post = Serif::Post.from_slug(subject, d.slug)
20
+
21
+ example.run
22
+ ensure
23
+ FileUtils.rm(@temporary_post.path)
24
+ end
25
+ end
26
+
12
27
  it "uses the config file's permalink value" do
13
28
  @posts.all? { |p| p.url == "/test-blog/#{p.slug}" }.should be_true
14
29
  end
@@ -18,4 +33,78 @@ describe Serif::Post do
18
33
  @posts.all? { |p| p.inspect.should include(p.headers.inspect) }
19
34
  end
20
35
  end
36
+
37
+ describe "#autoupdate=" do
38
+ it "sets the 'update' header to 'now' if truthy assigned value" do
39
+ @temporary_post.autoupdate = true
40
+ @temporary_post.headers[:update].should == "now"
41
+ end
42
+
43
+ it "removes the 'update' header entirely if falsey assigned value" do
44
+ @temporary_post.autoupdate = false
45
+ @temporary_post.headers.key?(:update).should be_false
46
+ end
47
+
48
+ it "marks the post as autoupdate? == true" do
49
+ @temporary_post.autoupdate?.should be_false
50
+ @temporary_post.autoupdate = true
51
+ @temporary_post.autoupdate?.should be_true
52
+ end
53
+ end
54
+
55
+ describe "#autoupdate?" do
56
+ it "returns true if there is an update: now header" do
57
+ @temporary_post.stub(:headers) { { :update => "foo" } }
58
+ @temporary_post.autoupdate?.should be_false
59
+ @temporary_post.stub(:headers) { { :update => "now" } }
60
+ @temporary_post.autoupdate?.should be_true
61
+ end
62
+
63
+ it "is ignorant of whitespace in the update header value" do
64
+ @temporary_post.stub(:headers) { { :update => "now" } }
65
+ @temporary_post.autoupdate?.should be_true
66
+
67
+ (1..3).each do |left|
68
+ (1..3).each do |right|
69
+ @temporary_post.stub(:headers) { { :update => "#{" " * left}now#{" " * right}"} }
70
+ @temporary_post.autoupdate?.should be_true
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ describe "#update!" do
77
+ it "sets the updated header timestamp to the current time" do
78
+ old_update_time = @temporary_post.updated
79
+ t = Time.now + 50
80
+
81
+ Timecop.freeze(t) do
82
+ @temporary_post.update!
83
+ @temporary_post.updated.should_not == old_update_time
84
+ @temporary_post.updated.to_i.should == t.to_i
85
+ @temporary_post.headers[:updated].to_i.should == t.to_i
86
+ end
87
+ end
88
+
89
+ it "calls save and writes out the new timestamp value, without a publish: now header" do
90
+ @temporary_post.should_receive(:save).once.and_call_original
91
+
92
+ t = Time.now + 50
93
+ Timecop.freeze(t) do
94
+ @temporary_post.update!
95
+
96
+ file_content = Redhead::String[File.read(@temporary_post.path)]
97
+ Time.parse(file_content.headers[:updated].value).to_i.should == t.to_i
98
+ file_content.headers[:publish].should be_nil
99
+ end
100
+ end
101
+
102
+ it "marks the post as no longer auto-updating" do
103
+ @temporary_post.autoupdate?.should be_false
104
+ @temporary_post.autoupdate = true
105
+ @temporary_post.autoupdate?.should be_true
106
+ @temporary_post.update!
107
+ @temporary_post.autoupdate?.should be_false
108
+ end
109
+ end
21
110
  end
@@ -8,6 +8,7 @@
8
8
 
9
9
  <p>another-sample-draft</p>
10
10
 
11
+
11
12
  <p><a href="http://twitter.com/share?text=another+sample+draft&amp;url=http%3A%2F%2Fwww.mysite.com">Submit this to Twitter.</p>
12
13
 
13
14
 
@@ -8,6 +8,7 @@
8
8
 
9
9
  <p>Just a sample draft.</p>
10
10
 
11
+
11
12
  <p><a href="http://twitter.com/share?text=Sample+draft&amp;url=http%3A%2F%2Fwww.mysite.com">Submit this to Twitter.</p>
12
13
 
13
14