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 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>