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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c4223a5c99949293121434806a6e208892610b2e
4
- data.tar.gz: 11ddc6a101e1d2da88cc950fc5b22addbeed5662
3
+ metadata.gz: 0f258b5e260b6477100127d77278a8b97ea9ce8c
4
+ data.tar.gz: aa4775b05d306ef5788e22f430ff89ba6d998019
5
5
  SHA512:
6
- metadata.gz: 9c96fe40d449a78473f476f3810f84908c27f7258428c1fd5df7891427be9f54e92674fefd745703c1e33bed69763ea61cf7e784c6a2bb2672e170bbfccffe59
7
- data.tar.gz: 69ae7f14ae1bad2d91c380243f1c5fd4e475c165907989e82eb7b94604b98257c64043cf0e0030a0474091d75dcf1c1f95f58ac5ffdbb5ef9a9f3abde162d7fc
6
+ metadata.gz: e33b8e4920fd2e9fbd4f2962bfba99992b42c05daf605bfc62117334cf2c4010ded613a24ebe547e3f0779fddb4e4fd278089feadcf5f1993ccdbee27cceae63
7
+ data.tar.gz: 9f3ea4463fcf75272be1b5aff24d34434f8139b5260bba931635dcbe16449b9cab8ce6edd3ee082975de5015972883a40f9929fa15b03f26c2239894b0970881
data/README.md CHANGED
@@ -1,17 +1,33 @@
1
1
  # About
2
2
 
3
- Few years back we used to use all the fancy static site generators for building a blog, so the blog could have layouts, tags, pagination and other features. With the arrival of frontend frameworks such as AngularJS, this is no longer necessary.
3
+ [![Build Status](https://travis-ci.org/botanicus/blog-generator.svg?branch=master)](https://travis-ci.org/botanicus/blog-generator)
4
4
 
5
- We can generate a static JSON API and let AngularJS handle the rest.
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 botanicus.me
12
- cd botanicus.me
13
- mkdir posts
14
- blog-generator.rb posts api
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
- <div id="excerpt">
39
+ <p id="excerpt">
24
40
  Excerpt
25
- </div>
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.
@@ -1,92 +1,23 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- # [Usage]
4
- #
5
- # blog-generator.rb [posts dir] [output base path]
6
-
7
- require 'ostruct'
8
- require 'fileutils'
9
- require 'blog-generator'
10
-
11
- POSTS_DIR, OUTPUT_BASE_PATH = ARGV.map { |i| i.chomp('/') }
12
-
13
- unless ARGV.length == 2
14
- abort "Usage: #{$0} [posts dir] [output base path]"
15
- end
16
-
17
- unless File.directory?(POSTS_DIR)
18
- abort "Posts directory #{POSTS_DIR} doesn't exist."
19
- end
20
-
21
- if Dir.exists?("#{OUTPUT_BASE_PATH}_prev") # If it does, the last build crashed.
22
- puts "~ Last build crashed. Reusing #{OUTPUT_BASE_PATH}_prev."
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.
@@ -1,37 +1,28 @@
1
- require 'json'
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, old_posts)
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
- published_posts = posts.select { |post| ! post.metadata[:draft] }
16
-
17
- published_posts.sort! do |a, b|
18
- b.published_at <=> a.published_at
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
- published_posts.each do |post|
22
- if old_post = old_posts[post.slug]
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, published_posts))
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
@@ -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
- attr_reader :site, :metadata, :format, :published_on, :updated_at
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
- @metadata = YAML.load_file(path).reduce(Hash.new) do |buffer, (slug, value)|
16
- buffer.merge(slug.to_sym => value)
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.merge!(slug: slug, published_at: self.published_at)
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
- feed = "#{site.base_url}/#{slug}.atom"
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
- # slug cannot be updated
48
- # => It has to be in Git now.
49
- def update_post_with_previous_values(old_post)
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
- # TODO: get rid off the variable and proxy it to metadata[:published_at].
61
- def published_at
62
- # When it was actually generated. It is then sourced from the last generated file, so it doesn't keep updating.
63
- @published_at ||= Time.now.utc
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 id
91
- digest = Digest::MD5.hexdigest(self.metadata[:slug])
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 ||= File.read(@path).match(/\n---\n(.+)$/m)[1].strip
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
- if self.format == :html
102
- @excerpt ||= Nokogiri::HTML(Nokogiri::HTML(self.body).css('#excerpt').inner_html.strip).css('p').inner_html
103
- elsif self.format == :md
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
- private
120
- def method_missing(method, *args, &block)
121
- return super if (! args.empty?) || block
122
- @metadata[method]
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 parse_path(path)
126
- match = File.basename(path).match(REGEXP)
127
- [Date.parse(match[1]).to_time.utc, match[2], match[3].to_sym]
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
- def convert_markdown(markup)
131
- require 'redcarpet'
118
+ private
119
+ def nokogiri_raw_document
120
+ @nokogiri_raw_document ||= Nokogiri::HTML(self.raw_body)
121
+ end
132
122
 
133
- renderer = Redcarpet::Render::HTML.new(no_links: true, hard_wrap: true)
134
- markdown = Redcarpet::Markdown.new(renderer, extensions = {})
135
- markdown.render(markup)
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.9
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-21 00:00:00.000000000 Z
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.6'
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.6'
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/feed.rb
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.2.2
65
+ rubygems_version: 2.6.8
61
66
  signing_key:
62
67
  specification_version: 4
63
68
  summary: Simple generator of blog APIs.
@@ -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>