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
@@ -3,12 +3,13 @@ PATH
3
3
  specs:
4
4
  serif (0.3.3)
5
5
  liquid (~> 2.4)
6
+ nokogiri (~> 1.5)
6
7
  rack (~> 1.0)
7
8
  redcarpet (~> 2.2)
8
9
  redhead (~> 0.0.8)
10
+ reverse_markdown (~> 0.4.3)
9
11
  rouge (~> 0.3.2)
10
12
  sinatra (~> 1.3)
11
- slop (~> 3.3)
12
13
  timeout_cache
13
14
 
14
15
  GEM
@@ -17,12 +18,15 @@ GEM
17
18
  diff-lcs (1.1.3)
18
19
  liquid (2.5.0)
19
20
  multi_json (1.6.1)
21
+ nokogiri (1.5.6)
20
22
  rack (1.5.2)
21
23
  rack-protection (1.5.0)
22
24
  rack
23
25
  rake (0.9.6)
24
26
  redcarpet (2.2.2)
25
27
  redhead (0.0.8)
28
+ reverse_markdown (0.4.4)
29
+ nokogiri
26
30
  rouge (0.3.2)
27
31
  thor
28
32
  rspec (2.12.0)
@@ -37,14 +41,13 @@ GEM
37
41
  multi_json (~> 1.0)
38
42
  simplecov-html (~> 0.7.1)
39
43
  simplecov-html (0.7.1)
40
- sinatra (1.3.5)
41
- rack (~> 1.4)
42
- rack-protection (~> 1.3)
43
- tilt (~> 1.3, >= 1.3.3)
44
- slop (3.4.4)
44
+ sinatra (1.4.1)
45
+ rack (~> 1.5, >= 1.5.2)
46
+ rack-protection (~> 1.4)
47
+ tilt (~> 1.3, >= 1.3.4)
45
48
  thor (0.17.0)
46
- tilt (1.3.5)
47
- timecop (0.5.9.2)
49
+ tilt (1.3.6)
50
+ timecop (0.6.1)
48
51
  timeout_cache (0.0.2)
49
52
 
50
53
  PLATFORMS
@@ -55,4 +58,4 @@ DEPENDENCIES
55
58
  rspec (~> 2.5)
56
59
  serif!
57
60
  simplecov (~> 0.7)
58
- timecop (~> 0.5.5)
61
+ timecop (~> 0.6.1)
data/README.md CHANGED
@@ -6,24 +6,22 @@ Serif is a file-based blogging engine intended for simple sites. It compiles Mar
6
6
 
7
7
  Having problems with Serif? [Open an issue on GitHub](https://github.com/aprescott/serif/issues), or use the [Serif Google Group](https://groups.google.com/forum/#!forum/serif-rb)
8
8
 
9
- # Changes and what's new
9
+ ## First time use
10
10
 
11
- # Latest release (v0.3.3)
11
+ To get started with Serif based on a site skeleton:
12
12
 
13
- * Allow drag-and-drop to work on posts as well as drafts. (9ea3bebf)
14
- * `serif new` no longer creates a sample published post (#37) and generates immediately. (#39)
15
- * Pygments.rb is replaced with Rouge for code highlighting. (#34)
13
+ ```bash
14
+ gem install serif # install serif
15
+ cd path/to/some/place # go to where you'll be creating your site directory
16
+ serif new # create an initial site skeleton
16
17
 
17
- ## v0.3.1 and v0.3.2
18
+ # ... edit your files how you want them ...
18
19
 
19
- * Be kinder about the space used by the private URL characters. (#32)
20
- * The keyup event on any input or textarea now marks the page as having changed. Previously only on blur events. (e0df1375dd)
21
- * Order the list of drafts by most-recently-modified first, clarify draft and post ordering above each list. (#33)
22
- * Support custom layouts for posts as well as non-post files. (#35)
23
- * Drag-and-drop image uploads no longer use exclusively `rw-------` permissions, now rely on umask. (605487d98)
24
- * (v0.3.2) Fix caching problems caused by #30, allowing the most recently published to appear in files that use `site.posts`. (#36)
20
+ serif generate # generate the site based on the source files
21
+ serif dev # serve up the site for local testing purposes
22
+ ```
25
23
 
26
- See `CHANGELOG` for more.
24
+ Now visit <http://localhost:8000/> to view the site.
27
25
 
28
26
  # Contents of this README
29
27
 
@@ -32,13 +30,15 @@ See `CHANGELOG` for more.
32
30
  * [Basic usage](#basics)
33
31
  * [Content and site structure](#content-and-site-structure)
34
32
  * [Publishing drafts](#publishing-drafts)
33
+ * [Updating posts](#updating-posts)
35
34
  * [Archive pages](#archive-pages)
36
35
  * [Configuration](#configuration)
37
36
  * [Deploying](#deploying)
38
37
  * [Customising the admin interface](#customising-the-admin-interface)
39
- * [Custom tags](#custom-tags)
38
+ * [Custom tags and filters](#custom-tags-and-filters)
40
39
  * [Template variables](#template-variables)
41
40
  * [Developing Serif](#developing-serif)
41
+ * [Changes and what's new](#changes-and-whats-new)
42
42
  * [Planned features](#planned-features)
43
43
 
44
44
  # Intro
@@ -67,23 +67,6 @@ For more info on development, see the section at the bottom of this README.
67
67
 
68
68
  # Basics
69
69
 
70
- ## First time use
71
-
72
- To get started with Serif based on a site skeleton:
73
-
74
- ```bash
75
- gem install serif # install serif
76
- cd path/to/some/place # go to where you'll be creating your site directory
77
- serif new # create an initial site skeleton
78
-
79
- # ... edit your files how you want them ...
80
-
81
- serif generate # generate the site based on the source files
82
- serif dev # serve up the site for local testing purposes
83
- ```
84
-
85
- Now visit <http://localhost:8000/> to view the site.
86
-
87
70
  ## Installing
88
71
 
89
72
  Installation is via [RubyGems](https://rubygems.org/). If you don't have Ruby installed, I recommend using [RVM](https://rvm.io/).
@@ -298,6 +281,26 @@ This is a draft that will be published now.
298
281
 
299
282
  On the next site generation (`serif generate`) this draft will be automatically published, using the current time as the creation timestamp.
300
283
 
284
+ # Updating posts
285
+
286
+ When you update a draft, you need to remember to change the updated time. As luck would have it, Serif takes care of timestamps for you! Just use a header of `update: now` at the top of your post:
287
+
288
+ ```
289
+ title: My blog post
290
+ Created: 2013-01-01T12:01:30+00:00
291
+ update: now
292
+ ```
293
+
294
+ Now the next time the site is generated, the timestamp will be updated:
295
+
296
+ ```
297
+ title: My blog post
298
+ Created: 2013-01-01T12:01:30+00:00
299
+ Updated: 2013-03-18T19:03:30+00:00
300
+ ```
301
+
302
+ Admin users: this is all done for you.
303
+
301
304
  # Archive pages
302
305
 
303
306
  By default, archive pages are made available at `/archive/:year/month`, e.g., `/archive/2012/11`. Individual archive pages can be customised by editing the `_templates/archive_page.html` file, which is used for each month.
@@ -491,6 +494,16 @@ Variable | Value
491
494
  `prev_post` | The post published chronologically before `post`.
492
495
  `next_post` | The post published chronologically after `post`.
493
496
 
497
+ ## Archive page variables
498
+
499
+ These are set when processing archive pages.
500
+
501
+ Variable | Value
502
+ -------------- |:-----
503
+ `month` | The month for the archive page being rendered. This is a Ruby `Date` instance.
504
+ `posts` | The list of posts for the month. Ordered by most-recently-published-first.
505
+ `archive_page` | A flag set to `true`.
506
+
494
507
  # Developing Serif
495
508
 
496
509
  ## Broad outline
@@ -504,6 +517,18 @@ Variable | Value
504
517
  * `lib/serif/` is generally where files go.
505
518
  * `test/` contains the test files. Any new files should have `require "test_helper"` at the top of the, which pulls in `test/test_helper.rb`.
506
519
 
520
+ # Changes and what's new
521
+
522
+ ## Latest release (v0.4)
523
+
524
+ ## v0.3.3
525
+
526
+ * Allow drag-and-drop to work on posts as well as drafts. (9ea3bebf)
527
+ * `serif new` no longer creates a sample published post (#37) and generates immediately. (#39)
528
+ * Pygments.rb is replaced with Rouge for code highlighting. (#34)
529
+
530
+ See `CHANGELOG` for more.
531
+
507
532
  # Planned features
508
533
 
509
534
  Some things I'm hoping to implement one day:
@@ -511,4 +536,4 @@ Some things I'm hoping to implement one day:
511
536
  1. Custom hooks to fire after particular events, such as minifying CSS after publish, or committing changes and pushing to a git repository.
512
537
  2. Simple Markdown pages instead of plain HTML for non-post content.
513
538
  3. Automatically detecting file changes and regenerating the site.
514
- 4. Adding custom Liquid filters and tags.
539
+ 4. Adding custom Liquid filters and tags.
data/bin/serif CHANGED
@@ -3,7 +3,6 @@
3
3
  $:.unshift File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
4
4
 
5
5
  require "fileutils"
6
- require "slop"
7
6
 
8
7
  def initialize_admin_server(source_dir)
9
8
  # need to cd to the directory before requiring the admin
@@ -60,30 +59,16 @@ def produce_skeleton(dir)
60
59
  puts
61
60
  end
62
61
 
63
- commands = Slop::Commands.new do
64
- on :admin do
65
- add_callback :empty do
66
- initialize_admin_server(Dir.pwd)
67
- end
68
- end
69
-
70
- on :generate do
71
- add_callback :empty do
72
- generate_site(Dir.pwd)
73
- end
74
- end
75
-
76
- on :dev do
77
- add_callback :empty do
78
- initialize_dev_server(Dir.pwd)
79
- end
80
- end
81
-
82
- on :new do
83
- add_callback :empty do
84
- produce_skeleton(Dir.pwd)
85
- end
86
- end
62
+ command = ARGV.shift
63
+ args = ARGV
64
+
65
+ case command
66
+ when "admin"
67
+ initialize_admin_server(Dir.pwd)
68
+ when "generate"
69
+ generate_site(Dir.pwd)
70
+ when "dev"
71
+ initialize_dev_server(Dir.pwd)
72
+ when "new"
73
+ produce_skeleton(Dir.pwd)
87
74
  end
88
-
89
- commands.parse
@@ -1,5 +1,7 @@
1
1
  require "sinatra/base"
2
2
  require "fileutils"
3
+ require "nokogiri"
4
+ require "reverse_markdown"
3
5
 
4
6
  module Serif
5
7
  class AdminServer
@@ -39,6 +41,52 @@ class AdminServer
39
41
  redirect to("/admin"), 301
40
42
  end
41
43
 
44
+ get "/admin/bookmarks" do
45
+ liquid :bookmarks, locals: { base_url: request.base_url }
46
+ end
47
+
48
+ get "/admin/quick-draft" do
49
+ url = params[:url]
50
+ html_content = params[:content].strip
51
+
52
+ title = params[:title]
53
+
54
+ # delete anything nonprintable
55
+ title = title.gsub(/[^\x20-\x7E]/, "")
56
+
57
+ # sanitise the HTML title into something we can use as a temporary slug
58
+ slug = title.split(" ").first(5).join(" ").gsub(/[^\w-]/, "-").gsub(/--+/, '-').gsub(/^-|-$/, '')
59
+ slug.downcase!
60
+
61
+ if html_content.empty?
62
+ markdown = "[#{title}](#{url.gsub(")", "\\)")})"
63
+ else
64
+ # parse the document fragment and remove any empty nodes.
65
+ document = Nokogiri::HTML::DocumentFragment.parse(html_content)
66
+ document.traverse { |p| p.remove if p.text && p.text.strip.empty? }
67
+ html_content = document.to_html
68
+
69
+ html_content = "<blockquote>#{html_content}</blockquote>"
70
+ markdown = ReverseMarkdown.parse(html_content, github_style_code_blocks: true)
71
+
72
+ # markdown URLs need to have any )s escaped
73
+ markdown = "[#{title}](#{url.gsub(")", "\\)")}):\n\n#{markdown}"
74
+ end
75
+
76
+ draft = Draft.new(site)
77
+ draft.title = title
78
+ draft.slug = slug
79
+ draft.save(markdown)
80
+
81
+ site.generate
82
+
83
+ if params[:edit] == "1"
84
+ redirect to("/admin/edit/drafts/#{slug}")
85
+ else
86
+ redirect to(url)
87
+ end
88
+ end
89
+
42
90
  get "/admin/new/draft" do
43
91
  content = Draft.new(site)
44
92
  autofocus = "slug"
@@ -3,10 +3,6 @@
3
3
  # which contains the contents of a post, be it in
4
4
  # draft form or otherwise.
5
5
  #
6
- # A ContentFile can determine its type based on
7
- # the presence or absence of a "published"
8
- # timestamp value.
9
- #
10
6
 
11
7
  module Serif
12
8
  class ContentFile
@@ -33,18 +29,16 @@ class ContentFile
33
29
  # if we're adding a slug and there's no path yet, then create the path.
34
30
  # this will run for new drafts
35
31
 
36
- if !@path
37
- @path = File.expand_path("#{site.directory}/#{self.class.dirname}/#{@slug}")
38
- end
32
+ @path ||= File.expand_path("#{site.directory}/#{self.class.dirname}/#{@slug}")
39
33
  end
40
34
 
41
35
  def title
42
- return nil if new?
36
+ return nil if !@source
43
37
  headers[:title]
44
38
  end
45
39
 
46
40
  def title=(new_title)
47
- if new?
41
+ if !@source
48
42
  @source = Redhead::String["title: #{new_title}\n\n"]
49
43
  else
50
44
  @source.headers[:title] = new_title
@@ -62,24 +56,16 @@ class ContentFile
62
56
  end
63
57
 
64
58
  def content(include_headers = false)
65
- include_headers ? "#{raw_headers}\n\n#{@source.to_s}" : @source.to_s
66
- end
67
-
68
- def new?
69
- !@source
70
- end
71
-
72
- def raw_headers
73
- @source.headers.to_s
59
+ include_headers ? "#{@source.headers.to_s}\n\n#{@source.to_s}" : @source.to_s
74
60
  end
75
61
 
76
62
  def created
77
- return nil if new?
63
+ return nil if !@source
78
64
  headers[:created].utc
79
65
  end
80
66
 
81
67
  def updated
82
- return nil if new?
68
+ return nil if !@source
83
69
  (headers[:updated] || created).utc
84
70
  end
85
71
 
@@ -105,18 +91,19 @@ class ContentFile
105
91
  end
106
92
 
107
93
  def save(markdown = nil)
108
- markdown ||= content if !new?
94
+ markdown ||= content if @source
109
95
 
110
96
  save_path = path || "#{self.class.dirname}/#{@slug}"
111
97
 
112
- if new?
113
- set_publish_time(Time.now)
114
- else
115
- set_updated_time(Time.now)
116
- end
117
-
98
+ # TODO: when a draft is being saved, it will call set_publish_time
99
+ # and then the #save call will execute this line, which will mean
100
+ # there is a very, very slight difference (fraction of a second)
101
+ # between the update time of a brand new published post and the
102
+ # creation time.
103
+ set_updated_time(Time.now)
104
+
118
105
  File.open(save_path, "w") do |f|
119
- f.puts %Q{#{raw_headers}
106
+ f.puts %Q{#{@source.headers.to_s}
120
107
 
121
108
  #{markdown}}.strip
122
109
  end
@@ -135,11 +122,18 @@ class ContentFile
135
122
 
136
123
  def set_publish_time(time)
137
124
  @source.headers[:created] = time.xmlschema
138
- @cached_headers = nil
125
+ headers_changed!
139
126
  end
140
127
 
141
128
  def set_updated_time(time)
142
129
  @source.headers[:updated] = time.xmlschema
130
+ headers_changed!
131
+ end
132
+
133
+ # Invalidates the cached headers entirely.
134
+ #
135
+ # Any methods which alter headers should call this.
136
+ def headers_changed!
143
137
  @cached_headers = nil
144
138
  end
145
139
 
@@ -36,18 +36,16 @@ class Draft < ContentFile
36
36
  @path = Post.from_slug(site, slug).path
37
37
  end
38
38
 
39
- # sets the autopublish flag to the given value.
40
- #
41
39
  # if the assigned value is truthy, the "publish" header
42
40
  # is set to "now", otherwise the header is removed.
43
41
  def autopublish=(value)
44
- @autopublish = value
45
-
46
42
  if value
47
43
  @source.headers[:publish] = "now"
48
44
  else
49
45
  @source.headers.delete(:publish)
50
46
  end
47
+
48
+ headers_changed!
51
49
  end
52
50
 
53
51
  # Checks the value of the "publish" header, and returns
@@ -27,6 +27,36 @@ class Post < ContentFile
27
27
  output
28
28
  end
29
29
 
30
+ # if the assigned value is truthy, the "update" header
31
+ # is set to "now", otherwise the header is removed.
32
+ def autoupdate=(value)
33
+ if value
34
+ @source.headers[:update] = "now"
35
+ else
36
+ @source.headers.delete(:update)
37
+ end
38
+
39
+ headers_changed!
40
+ end
41
+
42
+ # returns true if the post has been marked as needing a
43
+ # new updated timestamp header.
44
+ #
45
+ # this is based on the presence of an "update: now" header.
46
+ def autoupdate?
47
+ update_header = headers[:update]
48
+ update_header && update_header.strip == "now"
49
+ end
50
+
51
+ # Updates the updated timestamp and saves the contents.
52
+ #
53
+ # If there is an "update" header (see autoupdate?), it is deleted.
54
+ def update!
55
+ @source.headers.delete(:update)
56
+ set_updated_time(Time.now)
57
+ save
58
+ end
59
+
30
60
  def self.all(site)
31
61
  files = Dir[File.join(site.directory, dirname, "*")].select { |f| File.file?(f) }.map { |f| File.expand_path(f) }
32
62
  files.map { |f| new(site, f) }