serif 0.3.3 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +12 -9
- data/README.md +57 -32
- data/bin/serif +12 -27
- data/lib/serif/admin_server.rb +48 -0
- data/lib/serif/content_file.rb +23 -29
- data/lib/serif/draft.rb +2 -4
- data/lib/serif/post.rb +30 -0
- data/lib/serif/site.rb +34 -26
- data/serif.gemspec +4 -3
- data/statics/skeleton/_config.yml +2 -46
- data/statics/templates/admin/bookmarks.liquid +56 -0
- data/statics/templates/admin/layout.liquid +4 -2
- data/test/config_spec.rb +7 -0
- data/test/draft_spec.rb +14 -0
- data/test/filters_spec.rb +5 -1
- data/test/markup_renderer_spec.rb +4 -0
- data/test/post_spec.rb +89 -0
- data/test/site_dir/_site/drafts/another-sample-draft/{9094f3a34ce2ecfe188ad813e0d3229d2488350fb0c5bca4f8b4bcfe7b11.html → 481da12b79709bfa0547fa9b5754c9506fbed29afd0334e07a8c95e76850.html} +1 -0
- data/test/site_dir/_site/drafts/sample-draft/{359dca7a7237a1317c5e8ac2d3a01cd29db433f4caeb0b2209484ca09a7a.html → a986a62ad5f6edd1fcac3d08f5b461b92bcb667a2af69505230c291d405c.html} +1 -0
- data/test/site_dir/_site/test-archive/2012/{11/index.html → 11.html} +0 -0
- data/test/site_dir/_site/test-archive/2012/{12/index.html → 12.html} +0 -0
- data/test/site_dir/_site/test-archive/2013/{01/index.html → 01.html} +0 -0
- data/test/site_dir/_site/test-archive/2013/{03/index.html → 03.html} +0 -0
- data/test/site_dir/_site/test-archive/2399/{01/index.html → 01.html} +0 -0
- data/test/site_dir/_site/test-archive/2400/{01/index.html → 01.html} +0 -0
- data/test/site_dir/_site/test-blog/final-post.html +1 -0
- data/test/site_dir/_site/test-blog/penultimate-post.html +1 -0
- data/test/site_dir/_site/test-blog/post-to-be-published-on-generate.html +1 -0
- data/test/site_dir/_site/test-blog/post-with-custom-layout.html +1 -0
- data/test/site_dir/_site/test-blog/sample-post.html +1 -0
- data/test/site_dir/_site/test-blog/second-post.html +1 -0
- data/test/site_dir/_trash/1363633154-autopublish-draft +5 -0
- data/test/site_dir/_trash/{1363284991-test-draft → 1363633154-test-draft} +1 -1
- data/test/site_generation_spec.rb +39 -11
- data/test/site_spec.rb +28 -0
- data/test/test_helper.rb +33 -1
- metadata +34 -17
- data/test/site_dir/_trash/1363284991-autopublish-draft +0 -5
data/Gemfile.lock
CHANGED
@@ -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.
|
41
|
-
rack (~> 1.
|
42
|
-
rack-protection (~> 1.
|
43
|
-
tilt (~> 1.3, >= 1.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.
|
47
|
-
timecop (0.
|
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.
|
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
|
-
|
9
|
+
## First time use
|
10
10
|
|
11
|
-
|
11
|
+
To get started with Serif based on a site skeleton:
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
18
|
+
# ... edit your files how you want them ...
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
data/lib/serif/admin_server.rb
CHANGED
@@ -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"
|
data/lib/serif/content_file.rb
CHANGED
@@ -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
|
-
|
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
|
36
|
+
return nil if !@source
|
43
37
|
headers[:title]
|
44
38
|
end
|
45
39
|
|
46
40
|
def title=(new_title)
|
47
|
-
if
|
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 ? "#{
|
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
|
63
|
+
return nil if !@source
|
78
64
|
headers[:created].utc
|
79
65
|
end
|
80
66
|
|
81
67
|
def updated
|
82
|
-
return nil if
|
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
|
94
|
+
markdown ||= content if @source
|
109
95
|
|
110
96
|
save_path = path || "#{self.class.dirname}/#{@slug}"
|
111
97
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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{#{
|
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
|
-
|
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
|
|
data/lib/serif/draft.rb
CHANGED
@@ -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
|
data/lib/serif/post.rb
CHANGED
@@ -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) }
|