fngtps-weblog 0.5.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.
data/TODO ADDED
@@ -0,0 +1,5 @@
1
+ NOTES
2
+
3
+ git config --add user.name 'Manfred Stienstra'
4
+ git config --add user.email 'manfred@fngtps.com'
5
+ gem install rdiscount erubis git json
data/bin/weblog ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ if $0 == __FILE__
4
+ require 'rubygems'
5
+ $:.unshift(File.expand_path('../../lib', __FILE__))
6
+ end
7
+
8
+ require 'weblog'
9
+ require 'option_parser'
10
+
11
+ def usage
12
+ puts "Usage: #{File.basename($0)} <command>"
13
+ puts ""
14
+ puts "Commands:"
15
+ puts " init: Initialize a directory for use with the weblog tool"
16
+ puts " generate: Convert all Markdown source to an HTML file"
17
+ puts " publish: Move a draft directory into the public directory"
18
+ puts " info: Print information about the current working directory"
19
+ puts "Options:"
20
+ puts " -v, --verbose: Print informative messages"
21
+ end
22
+
23
+ options = {}
24
+ flags, args = OptionParser.parse(ARGV)
25
+
26
+ flags.each do |key, value|
27
+ case key
28
+ when 'h', 'help'
29
+ usage
30
+ exit 0
31
+ when 'v', 'verbose'
32
+ options[:verbose] = true
33
+ end
34
+ end
35
+
36
+ case args[0]
37
+ when 'init'
38
+ Weblog.init(options)
39
+ when 'publish'
40
+ Weblog.publish(options, *args[1..-1])
41
+ when 'info'
42
+ Weblog.print_info(options)
43
+ when 'help'
44
+ usage
45
+ else
46
+ Weblog.generate(options)
47
+ end
@@ -0,0 +1,28 @@
1
+ class OptionParser
2
+ def self.parse(argv)
3
+ return [{},[]] if argv.empty?
4
+
5
+ options = {}
6
+ rest = []
7
+ switch = nil
8
+
9
+ for value in argv
10
+ # values is a switch
11
+ if value[0] == 45
12
+ switch = value.slice((value[1] == 45 ? 2 : 1)..-1)
13
+ options[switch] = nil
14
+ else
15
+ if switch
16
+ # we encountered a switch so this
17
+ # value belongs to that switch
18
+ options[switch] = value
19
+ switch = nil
20
+ else
21
+ rest << value
22
+ end
23
+ end
24
+ end
25
+
26
+ [options, rest]
27
+ end
28
+ end
data/lib/weblog.rb ADDED
@@ -0,0 +1,155 @@
1
+ require 'set'
2
+ require 'time'
3
+ require 'fileutils'
4
+
5
+ require 'rdiscount'
6
+ require 'erubis'
7
+ require 'json'
8
+ require 'git'
9
+
10
+ class Weblog
11
+ autoload :Helpers, "weblog/helpers"
12
+ autoload :Index, "weblog/index"
13
+ autoload :Month, "weblog/month"
14
+ autoload :Snippet, "weblog/snippet"
15
+ autoload :Post, "weblog/post"
16
+
17
+ DIRECTORIES = %w(
18
+ public/javascripts
19
+ public/stylesheets
20
+ draft
21
+ )
22
+
23
+ # --- Utilities
24
+
25
+ def self.source_root
26
+ File.expand_path('../../', __FILE__)
27
+ end
28
+
29
+ def self.template_path
30
+ File.join(source_root, 'templates')
31
+ end
32
+
33
+ def self.find_working_directory(cwd)
34
+ candidate = cwd
35
+ while(candidate)
36
+ directory = File.join(candidate, 'public')
37
+ if File.exist?(directory)
38
+ return candidate
39
+ else
40
+ parts = candidate.split('/')
41
+ candidate = parts.empty? ? false : parts[0..-2].join('/')
42
+ end
43
+ end; nil
44
+ end
45
+
46
+ def initialize(cwd, options={})
47
+ @options = options
48
+ unless @path = self.class.find_working_directory(cwd)
49
+ puts "[!] Please use this tool from somewhere within the working directory"
50
+ end
51
+ end
52
+
53
+ def git
54
+ @git ||= Git.open(@path)
55
+ end
56
+
57
+ def verbose?
58
+ @options[:verbose]
59
+ end
60
+
61
+ # --- Attributes
62
+
63
+ attr_accessor :path
64
+
65
+ def title
66
+ @path.split('/').last.gsub(/[_-]/, ' ').capitalize
67
+ end
68
+
69
+ def months
70
+ Month.all(self)
71
+ end
72
+
73
+ def posts
74
+ if @posts.nil?
75
+ paths = Set.new
76
+ base_path = File.join(@path, 'public')
77
+ path_length = base_path.split('/').length
78
+ Dir.glob(File.join(base_path, '**/post.*')).each do |file|
79
+ paths << file.split('/')[path_length..-2].join('/')
80
+ end
81
+ @posts = paths.map do |path|
82
+ Weblog::Post.new(self, path)
83
+ end
84
+ end; @posts
85
+ end
86
+
87
+ def size
88
+ posts.size
89
+ end
90
+
91
+ def author
92
+ git.config('user.name')
93
+ end
94
+
95
+ # --- Commands
96
+
97
+ def index
98
+ Weblog::Index.new(self)
99
+ end
100
+
101
+ def generate
102
+ posts.each { |post| post.generate }
103
+ months.each { |month| month.generate }
104
+ index.generate
105
+ end
106
+
107
+ def publish(*directories)
108
+ year = Time.now.year.to_s
109
+ month = "%02d" % Time.now.month
110
+ directories.each do |directory|
111
+ name = directory.split('/').last
112
+ destination = File.join(@path, 'public', year, month, name)
113
+ FileUtils.mkdir_p(File.dirname(destination))
114
+ FileUtils.mv(directory, destination)
115
+ end
116
+ end
117
+
118
+ def print_info
119
+ months.each do |month|
120
+ puts "#{month.path}: #{month.size} #{month.size == 1 ? 'post' : 'posts'}"
121
+ end
122
+ puts [
123
+ "Directory: #{@path}",
124
+ "Title: #{title}",
125
+ "Author: #{author}",
126
+ "Total posts: #{size} #{size == 1 ? 'post' : 'posts'}"
127
+ ].join("\n")
128
+ end
129
+
130
+ def self.init(options)
131
+ DIRECTORIES.each { |path| FileUtils.mkdir_p(path) }
132
+ unless File.exist?('templates')
133
+ FileUtils.cp_r(template_path, 'templates')
134
+ FileUtils.rm_rf('templates/.svn')
135
+ end
136
+ end
137
+
138
+ def self.generate(options)
139
+ weblog = new(Dir.pwd, options)
140
+ weblog.generate
141
+ weblog
142
+ end
143
+
144
+ def self.publish(options, *directories)
145
+ weblog = new(Dir.pwd, options)
146
+ weblog.publish(*directories)
147
+ weblog
148
+ end
149
+
150
+ def self.print_info(options)
151
+ weblog = new(Dir.pwd, options)
152
+ weblog.print_info
153
+ weblog
154
+ end
155
+ end
@@ -0,0 +1,13 @@
1
+ class Weblog
2
+ module Helpers
3
+ def format_date(date)
4
+ return '' if date.nil?
5
+ date.strftime("%d-%m-%Y")
6
+ end
7
+
8
+ def format_date_and_time(time)
9
+ return '' if time.nil?
10
+ time.strftime("#{format_date(time)}, %H:%M")
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,56 @@
1
+ class Weblog
2
+ class Index
3
+ SIZE = 5
4
+
5
+ def initialize(weblog)
6
+ @weblog = weblog
7
+ end
8
+
9
+ def posts
10
+ if @posts.nil?
11
+ @posts = []
12
+ @weblog.months.reverse.each do |month|
13
+ month.posts.each do |post|
14
+ @posts << post
15
+ return @posts if @posts.length >= SIZE
16
+ end
17
+ end
18
+ end; @posts
19
+ end
20
+
21
+ # --- Rendering system
22
+
23
+ include Weblog::Helpers
24
+
25
+ def html_template_filename
26
+ File.join(@weblog.path, "templates/index.html.erb")
27
+ end
28
+
29
+ def feed_template_filename
30
+ File.join(@weblog.path, "templates/index.rss.erb")
31
+ end
32
+
33
+ def render(template_filename)
34
+ Erubis::EscapedEruby.new(
35
+ File.read(template_filename)
36
+ ).result(binding)
37
+ end
38
+
39
+ def html_filename
40
+ File.join(@weblog.path, 'public', 'weblog.html')
41
+ end
42
+
43
+ def feed_filename
44
+ File.join(@weblog.path, 'public', 'weblog.rss')
45
+ end
46
+
47
+ def generate
48
+ FileUtils.mkdir_p(File.dirname(html_filename))
49
+ File.open(html_filename, 'w') { |file| file.write(render(html_template_filename)) }
50
+ puts "Generated weblog index" if @weblog.verbose?
51
+ FileUtils.mkdir_p(File.dirname(feed_filename))
52
+ File.open(feed_filename, 'w') { |file| file.write(render(feed_template_filename)) }
53
+ puts "Generated weblog feed" if @weblog.verbose?
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,67 @@
1
+ class Weblog
2
+ class Month
3
+ attr_accessor :year, :month
4
+
5
+ def initialize(weblog, year, month)
6
+ @weblog = weblog
7
+ @year = year.to_i
8
+ @month = month.to_i
9
+ @path = File.join(@year.to_s, ("%02d" % @month))
10
+ end
11
+
12
+ attr_reader :path
13
+
14
+ # Returns a list of posts for this month in reverse chronological order
15
+ def posts
16
+ @weblog.posts.select do |post|
17
+ post.path.start_with?(@path)
18
+ end.sort_by { |post| -post.published_at.to_i }
19
+ end
20
+
21
+ def size
22
+ posts.size
23
+ end
24
+
25
+ def self.all(weblog)
26
+ months = []
27
+ Dir.entries(File.join(weblog.path, 'public')).each do |year|
28
+ if year =~ /^\d{4}$/
29
+ Dir.entries(File.join(weblog.path, 'public', year)).each do |month|
30
+ if month =~ /^\d{2}$/
31
+ months << new(weblog, year, month)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ months.sort_by do |month|
37
+ [month.year, month.month]
38
+ end
39
+ end
40
+
41
+ # --- Rendering system
42
+
43
+ include Weblog::Helpers
44
+
45
+ def template_filename
46
+ File.join(@weblog.path, "templates/month.html.erb")
47
+ end
48
+
49
+ def render
50
+ Erubis::EscapedEruby.new(
51
+ File.read(template_filename)
52
+ ).result(binding)
53
+ end
54
+
55
+ def index_filename
56
+ File.join(@weblog.path, 'public', @path, 'index.html')
57
+ end
58
+
59
+ def generate
60
+ FileUtils.mkdir_p(File.dirname(index_filename))
61
+ File.open(index_filename, 'w') do |file|
62
+ file.write(render)
63
+ end
64
+ puts "Generated #{@year}/#{"%02d" % @month}" if @weblog.verbose?
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,154 @@
1
+ class Weblog
2
+ class Post
3
+ attr_accessor :path
4
+
5
+ def initialize(weblog, path)
6
+ @weblog = weblog
7
+ @path = path
8
+ @post = self
9
+ _load_metadata
10
+ end
11
+
12
+ # --- Metadata
13
+
14
+ def metadata_filename
15
+ File.join(@weblog.path, 'public', @path, 'post.json')
16
+ end
17
+
18
+ attr_writer :author
19
+
20
+ def published_at=(published_at)
21
+ @published_at = Time.parse(published_at)
22
+ end
23
+
24
+ def updated_at=(updated_at)
25
+ @updated_at = Time.parse(updated_at)
26
+ end
27
+
28
+ def _load_metadata
29
+ JSON.parse(File.read(metadata_filename)).each do |key, value|
30
+ self.send("#{key}=", value)
31
+ end if File.exist?(metadata_filename)
32
+ end
33
+
34
+ # --- Attributes
35
+
36
+ def blob
37
+ if File.exist?(index_filename)
38
+ @blob ||= @weblog.git.gblob(index_filename)
39
+ end
40
+ end
41
+
42
+ def last_commit
43
+ @last_commit ||= blob.log(1).first if blob
44
+ end
45
+
46
+ def first_commit
47
+ @first_commit ||= blob.log(10_000).map{|l|l}.last if blob
48
+ end
49
+
50
+ def modified?
51
+ @modified ||= @weblog.git.status.changed.any? do |filename, status|
52
+ snippet.filename.end_with?(filename)
53
+ end
54
+ end
55
+
56
+ # * Author:
57
+ # - When it's in the git repository: first commit author
58
+ # - Otherwise: configured author
59
+ def _author
60
+ if commit = first_commit
61
+ first_commit.author.name
62
+ else
63
+ @weblog.author
64
+ end
65
+ end
66
+
67
+ def author
68
+ @author ||= _author
69
+ end
70
+
71
+ # * The created date:
72
+ # - When it's not in the git repository: current date and time
73
+ # - When it's in the git repository: first commit date
74
+ def _published_at
75
+ if commit = first_commit
76
+ commit.date
77
+ else
78
+ Time.now
79
+ end
80
+ end
81
+
82
+ def published_at
83
+ @published_at ||= _published_at
84
+ end
85
+
86
+ # * The updated date:
87
+ # - When it's not in the git repository: nil
88
+ # - When it has modifications: current date and time
89
+ # - When it has no modifications: last commit date
90
+ def _updated_at
91
+ if modified?
92
+ Time.now
93
+ elsif commit = last_commit
94
+ commit.date
95
+ else
96
+ nil
97
+ end
98
+ end
99
+
100
+ def updated_at
101
+ @updated_at ||= _updated_at
102
+ end
103
+
104
+ def stylesheets
105
+ Dir.glob(File.join(@weblog.path, 'public', path, '*.css')).map do |path|
106
+ File.basename(path)
107
+ end
108
+ end
109
+
110
+ def javascripts
111
+ Dir.glob(File.join(@weblog.path, 'public', path, '*.js')).map do |path|
112
+ File.basename(path)
113
+ end
114
+ end
115
+
116
+ def snippet
117
+ @snippet ||= Snippet.new(@weblog, self, File.join('public', path))
118
+ end
119
+
120
+ def title
121
+ snippet.title.strip
122
+ end
123
+
124
+ def content
125
+ snippet.content
126
+ end
127
+
128
+ # --- Rendering system
129
+
130
+ include Weblog::Helpers
131
+
132
+ def template_filename
133
+ File.join(@weblog.path, "templates/post.html.erb")
134
+ end
135
+
136
+ def render
137
+ Erubis::EscapedEruby.new(
138
+ File.read(template_filename)
139
+ ).result(binding)
140
+ end
141
+
142
+ def index_filename
143
+ File.join(@weblog.path, 'public', @path, 'index.html')
144
+ end
145
+
146
+ def generate
147
+ FileUtils.mkdir_p(File.dirname(index_filename))
148
+ File.open(index_filename, 'w') do |file|
149
+ file.write(render)
150
+ end
151
+ puts "Generated #{path}" if @weblog.verbose?
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,51 @@
1
+ class Weblog
2
+ class Snippet
3
+ attr_accessor :path, :filename
4
+
5
+ def initialize(weblog, object, path)
6
+ @weblog = weblog
7
+ @path = path
8
+ find_snippet_file
9
+ end
10
+
11
+ def candidates
12
+ %w(post.md post.html).map do |name|
13
+ File.join(@weblog.path, @path, name)
14
+ end
15
+ end
16
+
17
+ def find_snippet_file
18
+ @filename = candidates.detect do |filename|
19
+ File.exist?(filename)
20
+ end
21
+ end
22
+
23
+ def source_type
24
+ @filename.end_with?('.md') ? :markdown : :html
25
+ end
26
+
27
+ def read
28
+ @read ||= File.read(filename).split("\n")
29
+ end
30
+
31
+ def title
32
+ return '' if read.empty?
33
+ case source_type
34
+ when :markdown
35
+ /^#(.*)$/.match(read[0])[1]
36
+ when :html
37
+ /<h1>(.*)<\/h1>/.match(read[0])[1]
38
+ end
39
+ end
40
+
41
+ def content
42
+ return '' if read.empty?
43
+ case source_type
44
+ when :markdown
45
+ RDiscount.new(read[1..-1].join("\n")).to_html
46
+ when :html
47
+ read[1..-1].join("\n")
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= @weblog.title %></title>
5
+ <meta charset="utf-8" />
6
+ </head>
7
+ <body>
8
+ <% posts.each do |post| %>
9
+ <h1><a href="<%= post.path %>"><%= post.title %></a></h1>
10
+ <%== post.content %>
11
+ <% end %>
12
+ </body>
13
+ </html>
@@ -0,0 +1,23 @@
1
+ <?xml version="1.0"?>
2
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
3
+ <channel>
4
+ <title><%= @weblog.title %></title>
5
+ <atom:id>http://www.fngtps.com/index.rss</atom:id>
6
+ <atom:link rel="self" type="application/rss+xml" href="http://www.fngtps.com/index.rss"/>
7
+ <link>http://www.fngtps.com/index.rss</link>
8
+ <description>Recent posts on ‘Fingertips’</description>
9
+ <% unless posts.empty? %>
10
+ <lastBuildDate><%= (posts.first.updated_at || posts.first.published_at).rfc822 %></lastBuildDate>
11
+ <% end %>
12
+ <% posts.each do |post| %><% if post.published_at %>
13
+ <item>
14
+ <title><%= post.title %></title>
15
+ <link>http://www.fngtps.com/<%= post.path %></link>
16
+ <description><%= post.content %></description>
17
+ <pubDate><%= post.published_at.rfc822 %></pubDate>
18
+ <atom:updated><%= (post.updated_at || post.published_at).rfc822 %></atom:updated>
19
+ <guid>http://www.fngtps.com/<%= post.path %></guid>
20
+ </item>
21
+ <% end %><% end %>
22
+ </channel>
23
+ </rss>
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= @weblog.title %></title>
5
+ <meta charset="utf-8" />
6
+ </head>
7
+ <body>
8
+ <% posts.each do |post| %>
9
+ <h1><a href="<%= post.path.split('/').last %>"><%= post.title %></a></h1>
10
+ <%== post.content %>
11
+ <% end %>
12
+ </body>
13
+ </html>
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= @weblog.title %></title>
5
+ <meta charset="utf-8" />
6
+ <% @post.stylesheets.each do |path| %>
7
+ <link rel="stylesheet" href="<%= path %>" />
8
+ <% end %>
9
+ </head>
10
+ <body>
11
+ <h1><%= @post.title %></h1>
12
+ <%== @post.content %>
13
+ <p><%= @post.author %>, <%= format_date(@post.published_at) %><% if @post.updated_at %>, updated at <%= format_date_and_time(@post.updated_at) %><% end %></p>
14
+ <% @post.javascripts.each do |path| %>
15
+ <script src="<%= path %>" type="text/javascript"></script>
16
+ <% end %>
17
+ </body>
18
+ </html>
metadata ADDED
@@ -0,0 +1,162 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fngtps-weblog
3
+ version: !ruby/object:Gem::Version
4
+ hash: 11
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 5
9
+ - 0
10
+ version: 0.5.0
11
+ platform: ruby
12
+ authors:
13
+ - Manfred Stienstra
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-11-18 00:00:00 +01:00
19
+ default_executable: weblog
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rdiscount
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: erubis
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: git
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ type: :runtime
62
+ version_requirements: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ name: json
65
+ prerelease: false
66
+ requirement: &id004 !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ type: :runtime
76
+ version_requirements: *id004
77
+ - !ruby/object:Gem::Dependency
78
+ name: mocha
79
+ prerelease: false
80
+ requirement: &id005 !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ hash: 3
86
+ segments:
87
+ - 0
88
+ version: "0"
89
+ type: :development
90
+ version_requirements: *id005
91
+ - !ruby/object:Gem::Dependency
92
+ name: test-spec
93
+ prerelease: false
94
+ requirement: &id006 !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ hash: 3
100
+ segments:
101
+ - 0
102
+ version: "0"
103
+ type: :development
104
+ version_requirements: *id006
105
+ description: Weblog tools.
106
+ email: manfred@fngtps.com
107
+ executables:
108
+ - weblog
109
+ extensions: []
110
+
111
+ extra_rdoc_files:
112
+ - TODO
113
+ files:
114
+ - bin/weblog
115
+ - lib/option_parser.rb
116
+ - lib/weblog.rb
117
+ - lib/weblog/helpers.rb
118
+ - lib/weblog/index.rb
119
+ - lib/weblog/month.rb
120
+ - lib/weblog/post.rb
121
+ - lib/weblog/snippet.rb
122
+ - templates/index.html.erb
123
+ - templates/index.rss.erb
124
+ - templates/month.html.erb
125
+ - templates/post.html.erb
126
+ - TODO
127
+ has_rdoc: true
128
+ homepage:
129
+ licenses: []
130
+
131
+ post_install_message:
132
+ rdoc_options:
133
+ - --charset=UTF-8
134
+ require_paths:
135
+ - lib
136
+ required_ruby_version: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ hash: 3
142
+ segments:
143
+ - 0
144
+ version: "0"
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ none: false
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ hash: 3
151
+ segments:
152
+ - 0
153
+ version: "0"
154
+ requirements: []
155
+
156
+ rubyforge_project:
157
+ rubygems_version: 1.3.7
158
+ signing_key:
159
+ specification_version: 3
160
+ summary: Weblog tools.
161
+ test_files: []
162
+