blog-generator 0.0.9 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/botanicus/blog-generator.svg?branch=master)](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>
|