serif 0.3.3 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
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) }