blog-generator 0.0.9 → 0.1.0
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.
- checksums.yaml +4 -4
- data/README.md +24 -28
- data/bin/blog-generator.rb +20 -89
- data/lib/blog-generator.rb +40 -21
- data/lib/blog-generator/adapters/markdown.rb +31 -0
- data/lib/blog-generator/cli/draft.rb +46 -0
- data/lib/blog-generator/cli/generate.rb +71 -0
- data/lib/blog-generator/cli/ignore_update.rb +34 -0
- data/lib/blog-generator/cli/publish.rb +41 -0
- data/lib/blog-generator/cli/update.rb +31 -0
- data/lib/blog-generator/post.rb +59 -68
- metadata +16 -11
- data/lib/blog-generator/feed.rb +0 -78
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f258b5e260b6477100127d77278a8b97ea9ce8c
|
4
|
+
data.tar.gz: aa4775b05d306ef5788e22f430ff89ba6d998019
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e33b8e4920fd2e9fbd4f2962bfba99992b42c05daf605bfc62117334cf2c4010ded613a24ebe547e3f0779fddb4e4fd278089feadcf5f1993ccdbee27cceae63
|
7
|
+
data.tar.gz: 9f3ea4463fcf75272be1b5aff24d34434f8139b5260bba931635dcbe16449b9cab8ce6edd3ee082975de5015972883a40f9929fa15b03f26c2239894b0970881
|
data/README.md
CHANGED
@@ -1,17 +1,33 @@
|
|
1
1
|
# About
|
2
2
|
|
3
|
-
|
3
|
+
[](https://travis-ci.org/botanicus/blog-generator)
|
4
4
|
|
5
|
-
|
5
|
+
Blog-generator is a static site generator that generates only JSON files that can
|
6
|
+
be consumed from a framework such as React.js. Think Nanoc but for fake APIs.
|
6
7
|
|
7
8
|
# Usage
|
8
9
|
|
9
10
|
gem install blog-generator
|
10
11
|
gem install redcarpet # If you are going to use markdown.
|
11
|
-
mkdir
|
12
|
-
cd
|
13
|
-
|
14
|
-
blog-generator.rb
|
12
|
+
mkdir myblog.com
|
13
|
+
cd myblog.com
|
14
|
+
blog-generator.rb draft hello-world # Optionally hello-world.md, otherwise defaults to .html.
|
15
|
+
blog-generator.rb publish hello-world
|
16
|
+
blog-generator.rb generate api.myblog.com
|
17
|
+
|
18
|
+
## Development
|
19
|
+
|
20
|
+
To include drafts in your output JSON:
|
21
|
+
|
22
|
+
```
|
23
|
+
blog-generator.rb draft my-draft
|
24
|
+
blog-generator.rb generate api.myblog.com --include-drafts
|
25
|
+
```
|
26
|
+
|
27
|
+
## Updating posts
|
28
|
+
|
29
|
+
If you updated either excerpt or body of a post, the digest will no longer match
|
30
|
+
and you will get a warning upon running generate. You can either run `blog-generator.rb update my-post` to add `updated_at` timestamp and update the digest or `blog-generator.rb ignore_update my-post` to dismiss the update and only regenerate the digest.
|
15
31
|
|
16
32
|
# Post structure
|
17
33
|
|
@@ -20,10 +36,9 @@ title: 'Hello world!'
|
|
20
36
|
tags: ['Hello world', 'Test']
|
21
37
|
---
|
22
38
|
|
23
|
-
<
|
39
|
+
<p id="excerpt">
|
24
40
|
Excerpt
|
25
|
-
</
|
26
|
-
|
41
|
+
</p>
|
27
42
|
|
28
43
|
<h1>Hello world!</h1>
|
29
44
|
<p>
|
@@ -47,22 +62,3 @@ tags: ['Hello world', 'Test']
|
|
47
62
|
- `/posts/:slug.json`
|
48
63
|
- `/tags.json`
|
49
64
|
- `/tags/:slug.json`
|
50
|
-
|
51
|
-
# Feeds
|
52
|
-
|
53
|
-
```html
|
54
|
-
<!-- Global feed. -->
|
55
|
-
<link href="{{blog.feed_url}}" type="application/atom+xml" rel="alternate" title="{{blog.title}}" />
|
56
|
-
|
57
|
-
<!-- Per-tag feeds. -->
|
58
|
-
<link href="{{tag.feed_url}}" type="application/atom+xml" rel="alternate" title="{{tag.title}}" />
|
59
|
-
```
|
60
|
-
|
61
|
-
# Status
|
62
|
-
|
63
|
-
It works, but it needs polishing.
|
64
|
-
|
65
|
-
# TODO
|
66
|
-
|
67
|
-
- Generate assets by parsing the document, no explicit spec.
|
68
|
-
- GH markdown including source code support.
|
data/bin/blog-generator.rb
CHANGED
@@ -1,92 +1,23 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
elsif (! Dir.exists?("#{OUTPUT_BASE_PATH}_prev")) && Dir.exists?(OUTPUT_BASE_PATH) # otherwise it's first run.
|
24
|
-
FileUtils.mv(OUTPUT_BASE_PATH, "#{OUTPUT_BASE_PATH}_prev") # So we don't get any artifacts.
|
25
|
-
end
|
26
|
-
|
27
|
-
unless Dir.exists?(OUTPUT_BASE_PATH)
|
28
|
-
puts "~ #{OUTPUT_BASE_PATH} doesn't exist, creating."
|
29
|
-
Dir.mkdir(OUTPUT_BASE_PATH)
|
30
|
-
end
|
31
|
-
|
32
|
-
OLD_POSTS = Dir.glob("#{OUTPUT_BASE_PATH}_prev/posts/*.json").reduce(Hash.new) do |posts, path|
|
33
|
-
post = JSON.parse(File.read(path))
|
34
|
-
posts.merge(post['slug'] => post)
|
35
|
-
end
|
36
|
-
|
37
|
-
path = File.expand_path(File.join(POSTS_DIR, '..', 'defaults.yml'))
|
38
|
-
unless File.exist?(path)
|
39
|
-
puts "~ Feed configuration file #{path} not found."
|
3
|
+
command = ARGV.shift
|
4
|
+
commands = %w{draft generate ignore_update publish update}
|
5
|
+
if commands.include?(command)
|
6
|
+
require "blog-generator/cli/#{command}"
|
7
|
+
else
|
8
|
+
abort <<-EOF
|
9
|
+
[Usage]
|
10
|
+
|
11
|
+
#{$0} generate [output base path]
|
12
|
+
#{$0} generate [output base path] --include-drafts
|
13
|
+
#{$0} draft [slug].[extension]
|
14
|
+
#{$0} publish [slug]
|
15
|
+
#{$0} update [slug]
|
16
|
+
#{$0} ignore_update [slug]
|
17
|
+
|
18
|
+
[Defaults]
|
19
|
+
|
20
|
+
Posts dir is assumed to be posts/ in the same directory as the output base path.
|
21
|
+
Drafts dir is assumed to be drafts/ in the same directory as the output base path.
|
22
|
+
EOF
|
40
23
|
end
|
41
|
-
|
42
|
-
# Parse the posts.
|
43
|
-
site = OpenStruct.new(File.exist?(path) ? YAML.load_file(path) : Hash.new)
|
44
|
-
site.feed = [site.base_url, 'posts.atom'].join('/')
|
45
|
-
generator = BlogGenerator::Generator.parse(site, POSTS_DIR, OLD_POSTS)
|
46
|
-
|
47
|
-
# Generate.
|
48
|
-
|
49
|
-
def file(path, content)
|
50
|
-
puts "~ #{path}"
|
51
|
-
File.open(path, 'w') do |file|
|
52
|
-
file.puts(content)
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
Dir.chdir(OUTPUT_BASE_PATH) do
|
57
|
-
# GET /metadata.json
|
58
|
-
# TODO: Refactor this, it's evil.
|
59
|
-
file 'metadata.json', JSON.pretty_generate(site.instance_variable_get(:@table))
|
60
|
-
|
61
|
-
# GET /posts.atom
|
62
|
-
if generator.posts.any?
|
63
|
-
feed = BlogGenerator::Feed.new(site, generator.posts, 'posts.atom')
|
64
|
-
file 'posts.atom', feed.render
|
65
|
-
end
|
66
|
-
|
67
|
-
# GET /api/posts.json
|
68
|
-
# This calls PostList#to_json
|
69
|
-
file 'posts.json', JSON.pretty_generate(generator.posts)
|
70
|
-
|
71
|
-
# GET /api/posts/hello-world.json
|
72
|
-
Dir.mkdir('posts') unless Dir.exist?('posts')
|
73
|
-
generator.posts.each do |post|
|
74
|
-
file "posts/#{post.slug}.json", JSON.pretty_generate(post)
|
75
|
-
end
|
76
|
-
|
77
|
-
# GET /api/tags.json
|
78
|
-
file 'tags.json', JSON.pretty_generate(generator.tags.keys)
|
79
|
-
|
80
|
-
Dir.mkdir('tags') unless Dir.exist?('tags')
|
81
|
-
generator.tags.each do |tag, posts|
|
82
|
-
# GET /api/tags/doxxu.json
|
83
|
-
body = {tag: tag, posts: posts}
|
84
|
-
file "tags/#{tag[:slug]}.json", JSON.pretty_generate(body)
|
85
|
-
|
86
|
-
# GET /api/tags/doxxu.atom
|
87
|
-
feed = BlogGenerator::Feed.new(site, posts, "#{tag[:slug]}.atom")
|
88
|
-
file "tags/#{tag[:slug]}.atom", feed.render
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
FileUtils.rm_rf("#{OUTPUT_BASE_PATH}_prev") # Has to be forced, otherwise fails on the first run.
|
data/lib/blog-generator.rb
CHANGED
@@ -1,37 +1,28 @@
|
|
1
|
-
require '
|
2
|
-
|
1
|
+
require 'digest'
|
3
2
|
require 'blog-generator/post'
|
4
3
|
require 'blog-generator/post_list'
|
5
4
|
|
6
|
-
require 'blog-generator/feed'
|
7
|
-
|
8
5
|
module BlogGenerator
|
9
6
|
class Generator
|
10
|
-
def self.parse(site, posts_dir,
|
7
|
+
def self.parse(site, posts_dir, drafts_dir = false)
|
11
8
|
posts = Dir.glob("#{posts_dir}/*.{html,md}").reduce(Array.new) do |posts, path|
|
12
9
|
posts.push(Post.new(site, path))
|
13
10
|
end
|
14
11
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
12
|
+
if drafts_dir
|
13
|
+
Dir.glob("#{drafts_dir}/*.{html,md}").each do |path|
|
14
|
+
draft = Post.new(site, path)
|
15
|
+
draft.metadata[:draft] = true
|
16
|
+
draft.metadata[:published_at] = Time.now.utc.strftime('%d/%m/%Y %H:%M') # Let's emulate it so we get expected attributes in development.
|
17
|
+
posts.push(draft)
|
18
|
+
end
|
19
19
|
end
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
post.update_post_with_previous_values(OpenStruct.new(old_post))
|
24
|
-
p [post.published_at, post.published_on]
|
25
|
-
if post.published_at && post.published_on.to_date != post.published_at.to_date
|
26
|
-
abort "~ Published_at doesn't match published_on from the date part of #{post.slug} filename: #{[post.published_on.to_date, post.published_at.to_date].inspect}"
|
27
|
-
end
|
28
|
-
|
29
|
-
else
|
30
|
-
puts "~ New post: #{post.slug}"
|
31
|
-
end
|
21
|
+
posts.sort! do |a, b|
|
22
|
+
DateTime.parse(b.published_at) <=> DateTime.parse(a.published_at)
|
32
23
|
end
|
33
24
|
|
34
|
-
self.new(site, PostList.new(site,
|
25
|
+
self.new(site, PostList.new(site, posts))
|
35
26
|
end
|
36
27
|
|
37
28
|
attr_reader :site, :posts
|
@@ -49,5 +40,33 @@ module BlogGenerator
|
|
49
40
|
buffer
|
50
41
|
end
|
51
42
|
end
|
43
|
+
|
44
|
+
def validate!
|
45
|
+
self.validate_digest!
|
46
|
+
self.validate_linked_posts!
|
47
|
+
end
|
48
|
+
|
49
|
+
def validate_digest!
|
50
|
+
@posts.each do |post|
|
51
|
+
next if post.metadata[:draft]
|
52
|
+
|
53
|
+
# copied from cli/update.rb
|
54
|
+
body_digest = Digest::MD5.hexdigest(post.raw_body) # Raw body, so it's with the excerpt as well.
|
55
|
+
|
56
|
+
if body_digest != post.metadata[:digest]
|
57
|
+
warn "WARNING: The MD5 digest of the body of #{post.slug} changed. You should either acknowledge so by running the update command or dismiss it by running the ignore_update command."
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def validate_linked_posts!
|
63
|
+
@posts.each do |post|
|
64
|
+
post.links.each do |link|
|
65
|
+
unless @posts.any? { |post| post.metadata[:path] == link }
|
66
|
+
raise "Post #{post.slug} links #{link}, but there is no such post."
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
52
71
|
end
|
53
72
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
def convert_markdown(markup)
|
2
|
+
require 'redcarpet'
|
3
|
+
|
4
|
+
renderer = Redcarpet::Render::HTML.new(no_links: true, hard_wrap: true)
|
5
|
+
markdown = Redcarpet::Markdown.new(renderer, extensions = {})
|
6
|
+
markdown.render(markup)
|
7
|
+
end
|
8
|
+
|
9
|
+
def excerpt
|
10
|
+
if self.format == :html
|
11
|
+
@excerpt ||= Nokogiri::HTML(Nokogiri::HTML(self.body).css('#excerpt').inner_html.strip).css('p').inner_html
|
12
|
+
elsif self.format == :md
|
13
|
+
# We're converting it to MD, apparently it's necessary even though we
|
14
|
+
# converted the whole text initially, but it seems like MD ignores whatever
|
15
|
+
# is in <div id="excerpt">...</div>.
|
16
|
+
@excerpt ||= Nokogiri::HTML(convert_markdown(Nokogiri::HTML(self.body).css('#excerpt').inner_html.strip)).css('p').inner_html
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Maybe rename body -> raw_body and to_html -> body.
|
21
|
+
# This is being rewritten from initialize!
|
22
|
+
def body(format = @format)
|
23
|
+
case format
|
24
|
+
when :md
|
25
|
+
@body ||= convert_markdown(self.body(:html)) # I don't think this would work with ||= (which we're using so we can rewrite @body = in initialize.)
|
26
|
+
when :html
|
27
|
+
@body ||= File.read(@path).match(/\n---\n(.+)$/m)[1].strip
|
28
|
+
else
|
29
|
+
raise TypeError.new("Format #{@format} isn't supported.")
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# Variables.
|
2
|
+
drafts_dir = 'drafts'
|
3
|
+
posts_dir = 'posts'
|
4
|
+
|
5
|
+
# Main.
|
6
|
+
unless ARGV.length == 1
|
7
|
+
abort 'The draft command needs only slug or slug.extension as an argument.'
|
8
|
+
end
|
9
|
+
|
10
|
+
if ARGV.first.split('.').length == 2
|
11
|
+
slug, format = ARGV.shift.split('.')
|
12
|
+
else
|
13
|
+
slug, format = ARGV.shift, 'html'
|
14
|
+
end
|
15
|
+
|
16
|
+
unless %w{html md}.include?(format)
|
17
|
+
abort("BlogGenerator doesn't support #{format}.")
|
18
|
+
end
|
19
|
+
|
20
|
+
draft_path = "#{drafts_dir}/#{slug}.#{format}"
|
21
|
+
|
22
|
+
if File.exist?(draft_path)
|
23
|
+
abort "ERROR: Draft #{draft_path} already exists."
|
24
|
+
elsif post = Dir.glob("#{posts_dir}/*-#{slug}.*").first
|
25
|
+
abort "ERROR: Slug #{slug} is already being used by #{post}."
|
26
|
+
end
|
27
|
+
|
28
|
+
# The template is actually the same for both HTML and MD.
|
29
|
+
template = <<-EOF
|
30
|
+
title: #{slug.tr('-', ' ').capitalize}
|
31
|
+
tags: []
|
32
|
+
---
|
33
|
+
|
34
|
+
<p id="excerpt">
|
35
|
+
</p>
|
36
|
+
|
37
|
+
<!-- Headings start from h2, since h1 is the title of the article. -->
|
38
|
+
EOF
|
39
|
+
|
40
|
+
Dir.mkdir(drafts_dir) unless Dir.exists?(drafts_dir)
|
41
|
+
|
42
|
+
File.open(draft_path, 'w') do |file|
|
43
|
+
file.puts(template)
|
44
|
+
end
|
45
|
+
|
46
|
+
puts "~ Draft #{draft_path} has been created."
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'blog-generator'
|
4
|
+
|
5
|
+
# Variables.
|
6
|
+
drafts_dir = 'drafts'
|
7
|
+
posts_dir = 'posts'
|
8
|
+
|
9
|
+
# Main.
|
10
|
+
unless (1..2).include?(ARGV.length)
|
11
|
+
abort 'The generate command needs only output directory as an argument with --include-drafts being optional.'
|
12
|
+
end
|
13
|
+
|
14
|
+
output_dir = ARGV.shift.chomp('/')
|
15
|
+
include_drafts = true if ARGV.shift == '--include-drafts'
|
16
|
+
|
17
|
+
unless File.directory?(posts_dir)
|
18
|
+
abort "Posts directory #{posts_dir} doesn't exist."
|
19
|
+
end
|
20
|
+
|
21
|
+
unless Dir.exists?(output_dir)
|
22
|
+
puts "~ #{output_dir} doesn't exist, creating."
|
23
|
+
Dir.mkdir(output_dir)
|
24
|
+
else
|
25
|
+
FileUtils.rm_rf("#{output_dir}/*")
|
26
|
+
end
|
27
|
+
|
28
|
+
site_defaults_path = File.expand_path(File.join(posts_dir, '..', 'defaults.yml'))
|
29
|
+
unless File.exist?(site_defaults_path)
|
30
|
+
puts "~ Feed configuration file #{site_defaults_path} not found." # Do we still need it?
|
31
|
+
end
|
32
|
+
|
33
|
+
# Parse the posts.
|
34
|
+
site = OpenStruct.new(File.exist?(site_defaults_path) ? YAML.load_file(site_defaults_path) : Hash.new)
|
35
|
+
generator = BlogGenerator::Generator.parse(site, posts_dir, include_drafts ? drafts_dir : false)
|
36
|
+
|
37
|
+
# Generate.
|
38
|
+
generator.validate!
|
39
|
+
|
40
|
+
def file(path, content)
|
41
|
+
puts "~ #{path}"
|
42
|
+
File.open(path, 'w') do |file|
|
43
|
+
file.puts(content)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
Dir.chdir(output_dir) do
|
48
|
+
# GET /metadata.json
|
49
|
+
# TODO: Refactor this, it's evil.
|
50
|
+
file 'metadata.json', JSON.pretty_generate(site.instance_variable_get(:@table))
|
51
|
+
|
52
|
+
# GET /api/posts.json
|
53
|
+
# This calls PostList#to_json
|
54
|
+
file 'posts.json', JSON.pretty_generate(generator.posts)
|
55
|
+
|
56
|
+
# GET /api/posts/hello-world.json
|
57
|
+
Dir.mkdir('posts') unless Dir.exist?('posts')
|
58
|
+
generator.posts.each do |post|
|
59
|
+
file "posts/#{post.slug}.json", JSON.pretty_generate(post)
|
60
|
+
end
|
61
|
+
|
62
|
+
# GET /api/tags.json
|
63
|
+
file 'tags.json', JSON.pretty_generate(generator.tags.keys)
|
64
|
+
|
65
|
+
Dir.mkdir('tags') unless Dir.exist?('tags')
|
66
|
+
generator.tags.each do |tag, posts|
|
67
|
+
# GET /api/tags/doxxu.json
|
68
|
+
body = {tag: tag, posts: posts}
|
69
|
+
file "tags/#{tag[:slug]}.json", JSON.pretty_generate(body)
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# This is exactly the same as update.rb, except we don't write updated_at,
|
2
|
+
# we only update the digest.
|
3
|
+
|
4
|
+
require 'digest'
|
5
|
+
require 'blog-generator/post'
|
6
|
+
|
7
|
+
# Variables.
|
8
|
+
posts_dir = 'posts'
|
9
|
+
|
10
|
+
# Main.
|
11
|
+
unless ARGV.length == 1
|
12
|
+
abort 'ERROR: The update command needs only slug as an argument.'
|
13
|
+
end
|
14
|
+
|
15
|
+
slug = ARGV.shift
|
16
|
+
post_path = Dir.glob("#{posts_dir}/*-#{slug}.{html,md}").first
|
17
|
+
|
18
|
+
unless post_path
|
19
|
+
abort "ERROR: There is no #{slug} with extension html or md in #{posts_dir}."
|
20
|
+
end
|
21
|
+
|
22
|
+
site = OpenStruct.new # mock
|
23
|
+
post = BlogGenerator::Post.new(site, post_path)
|
24
|
+
|
25
|
+
updated_at = Time.now.utc.strftime('%d/%m/%Y %H:%M')
|
26
|
+
body_digest = Digest::MD5.hexdigest(post.raw_body) # Raw body, so it's with the excerpt as well.
|
27
|
+
|
28
|
+
if body_digest == post.metadata[:digest]
|
29
|
+
abort 'ERROR: The MD5 digest of the body is the same, which means the post has not been updated.'
|
30
|
+
end
|
31
|
+
|
32
|
+
File.open(post_path, 'w') do |file|
|
33
|
+
file.puts(post.save(digest: body_digest))
|
34
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'digest'
|
3
|
+
require 'blog-generator/post'
|
4
|
+
|
5
|
+
# Variables.
|
6
|
+
drafts_dir = 'drafts'
|
7
|
+
posts_dir = 'posts'
|
8
|
+
|
9
|
+
# Main.
|
10
|
+
unless ARGV.length == 1
|
11
|
+
abort 'ERROR: The draft command needs only slug as an argument.'
|
12
|
+
end
|
13
|
+
|
14
|
+
slug = ARGV.shift
|
15
|
+
draft_path = Dir.glob("#{drafts_dir}/#{slug}.{html,md}").first
|
16
|
+
|
17
|
+
if post = Dir.glob("#{posts_dir}/*-#{slug}.{html,md}").first
|
18
|
+
abort "ERROR: Post #{post} has already been published."
|
19
|
+
end
|
20
|
+
|
21
|
+
unless draft_path
|
22
|
+
abort "ERROR: There is no #{slug} with extension html or md in #{drafts_dir}."
|
23
|
+
end
|
24
|
+
|
25
|
+
Dir.mkdir(posts_dir) unless Dir.exists?(posts_dir)
|
26
|
+
|
27
|
+
date_slug = Time.now.strftime('%Y-%m-%d')
|
28
|
+
format = File.extname(draft_path)[1..-1]
|
29
|
+
post_path = File.join(posts_dir, "#{date_slug}-#{slug}.#{format}")
|
30
|
+
|
31
|
+
site = OpenStruct.new # mock
|
32
|
+
post = BlogGenerator::Post.new(site, draft_path)
|
33
|
+
|
34
|
+
published_at = Time.now.utc.strftime('%d/%m/%Y %H:%M')
|
35
|
+
body_digest = Digest::MD5.hexdigest(post.raw_body) # Raw body, so it's with the excerpt as well.
|
36
|
+
|
37
|
+
File.open(post_path, 'w') do |file|
|
38
|
+
file.puts(post.save(digest: body_digest, published_at: published_at))
|
39
|
+
end
|
40
|
+
|
41
|
+
File.unlink(draft_path)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'blog-generator/post'
|
3
|
+
|
4
|
+
# Variables.
|
5
|
+
posts_dir = 'posts'
|
6
|
+
|
7
|
+
# Main.
|
8
|
+
unless ARGV.length == 1
|
9
|
+
abort 'ERROR: The update command needs only slug as an argument.'
|
10
|
+
end
|
11
|
+
|
12
|
+
slug = ARGV.shift
|
13
|
+
post_path = Dir.glob("#{posts_dir}/*-#{slug}.{html,md}").first
|
14
|
+
|
15
|
+
unless post_path
|
16
|
+
abort "ERROR: There is no #{slug} with extension html or md in #{posts_dir}."
|
17
|
+
end
|
18
|
+
|
19
|
+
site = OpenStruct.new # mock
|
20
|
+
post = BlogGenerator::Post.new(site, post_path)
|
21
|
+
|
22
|
+
updated_at = Time.now.utc.strftime('%d/%m/%Y %H:%M')
|
23
|
+
body_digest = Digest::MD5.hexdigest(post.raw_body) # Raw body, so it's with the excerpt as well.
|
24
|
+
|
25
|
+
if body_digest == post.metadata[:digest]
|
26
|
+
abort 'ERROR: The MD5 digest of the body is the same, which means the post has not been updated.'
|
27
|
+
end
|
28
|
+
|
29
|
+
File.open(post_path, 'w') do |file|
|
30
|
+
file.puts(post.save(digest: body_digest, updated_at: updated_at))
|
31
|
+
end
|
data/lib/blog-generator/post.rb
CHANGED
@@ -5,66 +5,44 @@ require 'nokogiri'
|
|
5
5
|
|
6
6
|
module BlogGenerator
|
7
7
|
class Post
|
8
|
-
REGEXP = /^(\d{4}-\d{2}-\d{2})-(.+)\.(html|md)$/
|
8
|
+
REGEXP = /^((\d{4}-\d{2}-\d{2})-)?(.+)\.(html|md)$/
|
9
9
|
|
10
|
-
|
10
|
+
[:slug, :tags, :published_at, :updated_at].each do |attribute|
|
11
|
+
define_method(attribute) do
|
12
|
+
@metadata[attribute]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :site, :metadata, :format
|
11
17
|
def initialize(site, path)
|
12
18
|
# TODO: metadata so we can construct url (base_url + relative) AND merge author
|
13
19
|
@site, @path = site, File.expand_path(path)
|
14
20
|
|
15
|
-
|
16
|
-
|
17
|
-
end
|
18
|
-
|
19
|
-
@published_on, slug, format = parse_path(path)
|
20
|
-
|
21
|
-
@format = format # So we can access it from excerpt without having to pass it as an argument and break everything.
|
22
|
-
|
23
|
-
@body = convert_markdown(self.body) if format == :md
|
24
|
-
self.body # cache if it wasn't called yet
|
21
|
+
# TODO: Bring back .md from adapters/markdown.rb
|
22
|
+
published_on, slug, format = parse_path(path)
|
25
23
|
|
26
|
-
@metadata
|
24
|
+
@metadata = self.load_metadata
|
25
|
+
@metadata.merge!(slug: slug)
|
27
26
|
@metadata.merge!(excerpt: excerpt)
|
28
27
|
@metadata.merge!(path: "/posts/#{slug}") ### TODO: some routing config.
|
28
|
+
@metadata.merge!(links: self.links)
|
29
29
|
|
30
30
|
@metadata[:tags].map! do |tag|
|
31
31
|
slug = generate_slug(tag)
|
32
|
-
|
33
|
-
{title: tag, slug: slug, path: "/tags/#{slug}", feed: feed}
|
32
|
+
{title: tag, slug: slug, path: "/tags/#{slug}"}
|
34
33
|
end
|
35
|
-
|
36
|
-
tag_feeds = @metadata[:tags].map do |tag|
|
37
|
-
tag[:feed]
|
38
|
-
end
|
39
|
-
|
40
|
-
# @metadata.merge!(feeds: atom.feeds + tag_feeds) ### TODO: some routing config.
|
41
|
-
|
42
|
-
document = Nokogiri::HTML(self.body)
|
43
|
-
document.css('#excerpt').remove
|
44
|
-
@body = document.css('body').inner_html.strip
|
45
34
|
end
|
46
35
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
@published_at = DateTime.parse(old_post.published_at).to_time.utc
|
51
|
-
@updated_at = DateTime.parse(old_post.updated_at).to_time.utc if old_post.updated_at
|
52
|
-
@metadata.merge!(published_at: self.published_at)
|
53
|
-
@metadata.merge!(updated_at: self.updated_at) if @updated_at
|
54
|
-
if old_post.body != self.body # TODO: some more intelligent analysis, if more than 10% changed.
|
55
|
-
puts "~ Post #{self.slug} has been updated."
|
56
|
-
self.update!
|
36
|
+
def raw_metadata
|
37
|
+
@raw_metadata ||= begin
|
38
|
+
YAML.load_file(@path) || raise("Metadata in #{@path} are not valid YAML.")
|
57
39
|
end
|
58
40
|
end
|
59
41
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
end
|
65
|
-
|
66
|
-
def update!
|
67
|
-
@updated_at = Time.now.utc
|
42
|
+
def load_metadata
|
43
|
+
self.raw_metadata.reduce(Hash.new) do |buffer, (slug, value)|
|
44
|
+
buffer.merge(slug.to_sym => value.dup)
|
45
|
+
end
|
68
46
|
end
|
69
47
|
|
70
48
|
def author
|
@@ -75,7 +53,7 @@ module BlogGenerator
|
|
75
53
|
self.metadata[:email] || site.email
|
76
54
|
end
|
77
55
|
|
78
|
-
def generate_slug(name)
|
56
|
+
def generate_slug(name) # for tags, should go to utils or something.
|
79
57
|
name.downcase.tr(' /', '-').delete('!?')
|
80
58
|
end
|
81
59
|
|
@@ -87,24 +65,22 @@ module BlogGenerator
|
|
87
65
|
[site.base_url, self.relative_url].join('')
|
88
66
|
end
|
89
67
|
|
90
|
-
def
|
91
|
-
|
92
|
-
"urn:uuid:#{digest}"
|
68
|
+
def raw_body
|
69
|
+
File.read(@path).match(/\n---\n(.+)$/m)[1].strip
|
93
70
|
end
|
94
71
|
|
95
|
-
# Maybe rename body -> raw_body and to_html -> body.
|
96
72
|
def body
|
97
|
-
@body ||=
|
73
|
+
@body ||= begin
|
74
|
+
document = nokogiri_raw_document.dup
|
75
|
+
document.css('#excerpt').remove
|
76
|
+
document.css('body').inner_html.strip
|
77
|
+
end
|
98
78
|
end
|
99
79
|
|
100
80
|
def excerpt
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
# We're converting it to MD, apparently it's necessary even though we
|
105
|
-
# converted the whole text initially, but it seems like MD ignores whatever
|
106
|
-
# is in <div id="excerpt">...</div>.
|
107
|
-
@excerpt ||= Nokogiri::HTML(convert_markdown(Nokogiri::HTML(self.body).css('#excerpt').inner_html.strip)).css('p').inner_html
|
81
|
+
@excerpt ||= begin
|
82
|
+
document = nokogiri_raw_document.dup
|
83
|
+
document.css('#excerpt').inner_html.sub(/^\s*(.*)\s*$/, '\1').chomp
|
108
84
|
end
|
109
85
|
end
|
110
86
|
|
@@ -116,23 +92,38 @@ module BlogGenerator
|
|
116
92
|
self.as_json.to_json(*args)
|
117
93
|
end
|
118
94
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
95
|
+
def save(extra_metadata = Hash.new)
|
96
|
+
extra_metadata = extra_metadata.reduce(Hash.new) do |buffer, (key, value)|
|
97
|
+
buffer.merge(key.to_s => value)
|
98
|
+
end
|
99
|
+
|
100
|
+
excerpt = self.excerpt.empty? ? %Q{<p id="excerpt">\n</p>} : %Q{<p id="excerpt">\n #{self.excerpt}\n</p>}
|
101
|
+
metadata = self.raw_metadata.merge(extra_metadata)
|
102
|
+
<<-EOF
|
103
|
+
#{metadata.map { |key, value| "#{key}: #{value}"}.join("\n")}
|
104
|
+
---
|
105
|
+
|
106
|
+
#{excerpt}
|
107
|
+
|
108
|
+
#{self.body}
|
109
|
+
EOF
|
123
110
|
end
|
124
111
|
|
125
|
-
def
|
126
|
-
|
127
|
-
|
112
|
+
def links
|
113
|
+
nokogiri_raw_document.css('a').map do |anchor|
|
114
|
+
anchor.attribute('href').value
|
115
|
+
end.uniq
|
128
116
|
end
|
129
117
|
|
130
|
-
|
131
|
-
|
118
|
+
private
|
119
|
+
def nokogiri_raw_document
|
120
|
+
@nokogiri_raw_document ||= Nokogiri::HTML(self.raw_body)
|
121
|
+
end
|
132
122
|
|
133
|
-
|
134
|
-
|
135
|
-
|
123
|
+
def parse_path(path)
|
124
|
+
match = File.basename(path).match(REGEXP)
|
125
|
+
published_on = match[1] ? Date.parse(match[1]).to_time.utc : nil
|
126
|
+
[published_on, match[3], match[4].to_sym]
|
136
127
|
end
|
137
128
|
end
|
138
129
|
end
|
metadata
CHANGED
@@ -1,30 +1,30 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: blog-generator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- James C Russell
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-01-
|
11
|
+
date: 2017-01-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - ~>
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1.
|
19
|
+
version: '1.7'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - ~>
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '1.
|
27
|
-
description:
|
26
|
+
version: '1.7'
|
27
|
+
description: "..."
|
28
28
|
email: james@101ideas.cz
|
29
29
|
executables:
|
30
30
|
- blog-generator.rb
|
@@ -34,7 +34,12 @@ files:
|
|
34
34
|
- README.md
|
35
35
|
- bin/blog-generator.rb
|
36
36
|
- lib/blog-generator.rb
|
37
|
-
- lib/blog-generator/
|
37
|
+
- lib/blog-generator/adapters/markdown.rb
|
38
|
+
- lib/blog-generator/cli/draft.rb
|
39
|
+
- lib/blog-generator/cli/generate.rb
|
40
|
+
- lib/blog-generator/cli/ignore_update.rb
|
41
|
+
- lib/blog-generator/cli/publish.rb
|
42
|
+
- lib/blog-generator/cli/update.rb
|
38
43
|
- lib/blog-generator/post.rb
|
39
44
|
- lib/blog-generator/post_list.rb
|
40
45
|
homepage: http://github.com/botanicus/blog-generator
|
@@ -47,17 +52,17 @@ require_paths:
|
|
47
52
|
- lib
|
48
53
|
required_ruby_version: !ruby/object:Gem::Requirement
|
49
54
|
requirements:
|
50
|
-
- -
|
55
|
+
- - ">="
|
51
56
|
- !ruby/object:Gem::Version
|
52
57
|
version: '0'
|
53
58
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
59
|
requirements:
|
55
|
-
- -
|
60
|
+
- - ">="
|
56
61
|
- !ruby/object:Gem::Version
|
57
62
|
version: '0'
|
58
63
|
requirements: []
|
59
64
|
rubyforge_project:
|
60
|
-
rubygems_version: 2.
|
65
|
+
rubygems_version: 2.6.8
|
61
66
|
signing_key:
|
62
67
|
specification_version: 4
|
63
68
|
summary: Simple generator of blog APIs.
|
data/lib/blog-generator/feed.rb
DELETED
@@ -1,78 +0,0 @@
|
|
1
|
-
require 'erb'
|
2
|
-
require 'digest'
|
3
|
-
require 'forwardable'
|
4
|
-
require 'date'
|
5
|
-
|
6
|
-
module BlogGenerator
|
7
|
-
class Feed
|
8
|
-
extend Forwardable
|
9
|
-
def_delegators :@site, :base_url, :title, :subtitle, :author, :email
|
10
|
-
|
11
|
-
attr_reader :site, :posts
|
12
|
-
def initialize(site, posts, relative_url)
|
13
|
-
@site, @posts = site, posts
|
14
|
-
@relative_url = relative_url
|
15
|
-
end
|
16
|
-
|
17
|
-
def feed_url
|
18
|
-
[@site.base_url, @relative_url].join('/')
|
19
|
-
end
|
20
|
-
|
21
|
-
def as_json
|
22
|
-
{title: self.title, url: self.feed_url}
|
23
|
-
end
|
24
|
-
|
25
|
-
def to_json(*args)
|
26
|
-
self.as_json.to_json(*args)
|
27
|
-
end
|
28
|
-
|
29
|
-
def id
|
30
|
-
digest = Digest::MD5.hexdigest(posts.each.map(&:title).join(","))
|
31
|
-
"urn:uuid:#{digest}"
|
32
|
-
end
|
33
|
-
|
34
|
-
def updated_at
|
35
|
-
self.posts.last.updated_at || self.posts.last.published_at
|
36
|
-
end
|
37
|
-
|
38
|
-
def template
|
39
|
-
# @template ||= DATA.read # Why can't I use DATA?
|
40
|
-
@template ||= File.read(__FILE__).sub(/\A.*\n__END__\n/m, '')
|
41
|
-
end
|
42
|
-
|
43
|
-
def render
|
44
|
-
ERB.new(self.template).result(binding)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
__END__
|
50
|
-
<?xml version="1.0" encoding="utf-8"?>
|
51
|
-
|
52
|
-
<feed xmlns="http://www.w3.org/2005/Atom">
|
53
|
-
<title><%= self.title %></title>
|
54
|
-
<subtitle><%= self.subtitle %></subtitle>
|
55
|
-
<link href="<%= self.feed_url %>" rel="self" />
|
56
|
-
<link href="<%= self.base_url %>" />
|
57
|
-
<id><%= self.id %></id>
|
58
|
-
<updated><%= self.updated_at.to_date.iso8601 %></updated>
|
59
|
-
|
60
|
-
<% posts.each do |post| %>
|
61
|
-
<entry>
|
62
|
-
<title><%= post.title %></title>
|
63
|
-
<link href="<%= post.absolute_url %>" />
|
64
|
-
<link rel="alternate" type="text/html" href="<%= post.absolute_url %>"/>
|
65
|
-
<id><%= post.id %></id>
|
66
|
-
<updated><%= (post.updated_at || post.published_at).to_date.iso8601 %></updated>
|
67
|
-
<!-- TODO: strip HTML from excerpt -->
|
68
|
-
<summary><%= post.excerpt %></summary>
|
69
|
-
<!-- <content type="xhtml">
|
70
|
-
<%= post.body %>
|
71
|
-
</content> -->
|
72
|
-
<author>
|
73
|
-
<name><%= post.author %></name>
|
74
|
-
<email><%= post.email %></email>
|
75
|
-
</author>
|
76
|
-
</entry>
|
77
|
-
<% end %>
|
78
|
-
</feed>
|