mr_poole 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 30428d35eafd9d58388483a2a69f299e7014f89a
4
+ data.tar.gz: b1f25c5ba6677ddc1568f3d62310a751f0a6b68a
5
+ SHA512:
6
+ metadata.gz: 83cbc63e84de3ac30014a48b4ba1d376ea12add90644c3c320d9456737d26931a03050f09b38cbd1b1f642b20da5f654740e2b286d0e0cf24e423c8e4c0b30b1
7
+ data.tar.gz: 0bc63bef197d7b5d5d5e4f9a8840e9faaf42aab26f0fbdd2b2ff334380d50ac1b6b5b51dd0debdf0c161fcc330606165e2d62d6a7a88634f3a757581ba6a36a0
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in foo.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 Michael McClimon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # Mr. Poole
2
+
3
+ A butler for Jekyll. Provides a command-line interface (called `poole`) for
4
+ creating and publishing posts and drafts for [Jekyll](http://jekyllrb.com)
5
+ blogs.
6
+
7
+ The literary Mr. Poole is Jekyll's butler, who "serves Jekyll faithfully, and
8
+ attempts to do a good job and be loyal to his master"
9
+ [Wikipedia](http://en.wikipedia.org/wiki/Jekyll_and_hyde#Mr._Poole), and the
10
+ Mr. Poole gem looks to be the same thing.
11
+
12
+ ## Usage
13
+
14
+ Mr. Poole is primarily a command-line application: the gem installs an
15
+ executable called `poole` in your path. It has four subcommands: post, draft,
16
+ publish, and unpublish.
17
+
18
+ ### Post
19
+
20
+ poole post [OPTIONS] TITLE
21
+
22
+ Generates a timestamped post in your `_posts` directory, with the format
23
+ `YYYY-MM-DD-slug.md` (other formats to be suppored in the future). With no
24
+ options, will generate a slug based on your title by replacing spaces with
25
+ underscores, downcasing, and removing any special character. With option
26
+ `--slug` (or `-s`), you can provide a custom slug.
27
+
28
+ Poole generates a simple file (in the future, this will be customizable) that
29
+ looks like this:
30
+
31
+ ```yaml
32
+ ---
33
+ title: (your title automatically inserted here)
34
+ layout: post
35
+ date: (current date automatically inserted here)
36
+ ---
37
+ ```
38
+
39
+ ### Draft
40
+
41
+ poole draft [OPTIONS] TITLE
42
+
43
+ Just like `poole post`, except that it creates an untimestamped post in your
44
+ `_drafts` directory (creating it if it doesn't exist yet). Also takes
45
+ `--slug`/`-s` as an option. In the generated file, no date is inserted.
46
+
47
+ ### Publish
48
+
49
+ poole publish DRAFT_PATH
50
+
51
+ Publishes a draft from your _drafts folder to your _posts folder, renaming the
52
+ file and updating the date in the header.
53
+
54
+ Given this file (called `_drafts/test_draft.md`):
55
+
56
+ ```
57
+ ---
58
+ title: My awesome blog post
59
+ layout: post
60
+ date:
61
+ ---
62
+
63
+ The life, universe, and everything.
64
+ ```
65
+
66
+ A call to `poole publish` will generate a file named
67
+ `_posts/yyyy-mm-dd-test_draft.md` and delete the draft. (TODO: add flags for
68
+ no-delete drafts, and no-update timstamp.) Also updates the date filed in the
69
+ header with a date, and HH:MM, producing this file:
70
+
71
+ ```
72
+ ---
73
+ title: My awesome blog post
74
+ layout: post
75
+ date: 2010-01-02 16:00
76
+ ---
77
+
78
+ The life, universe, and everything.
79
+ ```
80
+
81
+ ### Unpublish
82
+
83
+ poole unpublish POST_PATH
84
+
85
+ The reverse of publish: moves a file from your _posts folder to the _drafts
86
+ folder, renaming the file and removing the date in the header. This will
87
+ rename a file called `_posts/yyyy-mm-dd-test_post.md` to
88
+ `_drafts/test_post.md`. (TODO: add flags for no-delete post, no-update
89
+ timestamp, and custom slug for unpublished draft (?))
90
+
91
+
92
+ ### Script usage
93
+
94
+ The actual work is done in `MrPoole::Commands`: calls into that class return
95
+ the path name for newly created files, so you can do something useful with
96
+ them if you want to. This should get better in the future.
97
+
98
+
99
+ ## To do
100
+
101
+ - Configuration: custom templates, hooking into jekyll's `_config.yml`
102
+ - Support for multiple output formats (right now, only markdown is supported)
103
+ - Better option handling (allow custom templates, more flexible date
104
+ substitution)
105
+ - Better documentation (this is an open source project, after all)
106
+
107
+ ## Installation
108
+
109
+ Add this line to your application's Gemfile:
110
+
111
+ gem 'mr_poole'
112
+
113
+ And then execute:
114
+
115
+ $ bundle
116
+
117
+ Or install it yourself as:
118
+
119
+ $ gem install mr_poole
120
+
121
+ ## Contact
122
+
123
+ Contact me on Github, at michael@mcclimon.org, or on twitter, @mmcclimon.
124
+
125
+ ## Contributing
126
+
127
+ 1. Fork it
128
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
129
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
130
+ 4. Push to the branch (`git push origin my-new-feature`)
131
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec) do |t|
5
+ t.rspec_opts = "--color --format doc"
6
+ end
data/bin/poole ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'mr_poole'
4
+
5
+ action = ARGV.shift
6
+
7
+ cli = MrPoole::CLI.new(ARGV)
8
+ cli.execute(action)
@@ -0,0 +1,85 @@
1
+ require 'optparse'
2
+ require 'ostruct'
3
+
4
+ module MrPoole
5
+ class CLI
6
+
7
+ def initialize(args)
8
+ @helper = Helper.new
9
+ @helper.ensure_jekyll_dir
10
+
11
+ @params = args
12
+ @commands = Commands.new
13
+ end
14
+
15
+ def execute(action)
16
+ case action
17
+ when 'post' then handle_post
18
+ when 'draft' then handle_draft
19
+ when 'publish' then handle_publish
20
+ when 'unpublish' then handle_unpublish
21
+ else @helper.gen_usage
22
+ end
23
+ end
24
+
25
+ def handle_post
26
+ options = do_creation_options
27
+ options.title ||= @params.first
28
+
29
+ @helper.post_usage unless options.title
30
+ @commands.post(options.title, options.slug)
31
+ end
32
+
33
+ def handle_draft
34
+ options = do_creation_options
35
+ options.title ||= @params.first
36
+
37
+ @helper.draft_usage unless options.title
38
+ @commands.draft(options.title, options.slug)
39
+ end
40
+
41
+ def handle_publish
42
+ options = OpenStruct.new
43
+ opt_parser = OptionParser.new do |opts|
44
+ # eventually there will be options...not yet
45
+ end
46
+ opt_parser.parse! @params
47
+
48
+ path = @params.first
49
+ @helper.publish_usage unless path
50
+ @commands.publish(path)
51
+ end
52
+
53
+ def handle_unpublish
54
+ options = OpenStruct.new
55
+ opt_parser = OptionParser.new do |opts|
56
+ # eventually there will be options...not yet
57
+ end
58
+ opt_parser.parse! @params
59
+
60
+ path = @params.first
61
+ @helper.unpublish_usage unless path
62
+ @commands.unpublish(path)
63
+ end
64
+
65
+ def do_creation_options
66
+ options = OpenStruct.new
67
+ options.slug = nil
68
+ options.title = nil
69
+
70
+ opt_parser = OptionParser.new do |opts|
71
+ opts.on('-s', '--slug [SLUG]', "Use custom slug") do |s|
72
+ options.slug = s
73
+ end
74
+
75
+ opts.on('-t', '--title [TITLE]', "Specifiy title") do |t|
76
+ options.title = t
77
+ end
78
+ end
79
+
80
+ opt_parser.parse! @params
81
+ options
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1,110 @@
1
+ require 'fileutils'
2
+ require 'shellwords'
3
+
4
+ module MrPoole
5
+ class Commands
6
+
7
+ POSTS_FOLDER = '_posts'
8
+ DRAFTS_FOLDER = '_drafts'
9
+
10
+ def initialize
11
+ @helper = Helper.new
12
+ @default_layout = @helper.get_default_layout
13
+ end
14
+
15
+ # Generate a timestamped post
16
+ def post(title, slug='')
17
+ date = @helper.get_date_stamp
18
+
19
+ # still want to escape any garbage in the slug
20
+ slug = title if slug.nil? || slug.empty?
21
+ slug = @helper.get_slug_for(slug)
22
+
23
+ # put the metadata into the layout header
24
+ head = @default_layout
25
+ head.sub!(/^title:\s*$/, "title: #{title}")
26
+ head.sub!(/^date:\s*$/, "date: #{date}")
27
+
28
+ path = File.join(POSTS_FOLDER, "#{date}-#{slug}.md")
29
+ f = File.open(path, "w")
30
+ f.write(head)
31
+ f.close
32
+
33
+ path # return the path, in case we want to do anything useful
34
+ end
35
+
36
+ # Generate a non-timestamped draft
37
+ def draft(title, slug='')
38
+ # the drafts folder might not exist yet...create it just in case
39
+ FileUtils.mkdir_p(DRAFTS_FOLDER)
40
+
41
+ slug = title if slug.nil? || slug.empty?
42
+ slug = @helper.get_slug_for(slug)
43
+
44
+ head = @default_layout
45
+ head.sub!(/^title:\s*$/, "title: #{title}")
46
+
47
+ path = File.join(DRAFTS_FOLDER, "#{slug}.md")
48
+ f = File.open(path, "w")
49
+ f.write(head)
50
+ f.close
51
+
52
+ path # return the path, in case we want to do anything useful
53
+ end
54
+
55
+ # Todo make this take a path instead?
56
+ def publish(draftpath)
57
+ slug = File.basename(draftpath, '.md')
58
+
59
+ begin
60
+ infile = File.open(draftpath, "r")
61
+ rescue Errno::ENOENT
62
+ @helper.bad_path(draftpath)
63
+ end
64
+
65
+ date = @helper.get_date_stamp
66
+ time = @helper.get_time_stamp
67
+
68
+ outpath = File.join(POSTS_FOLDER, "#{date}-#{slug}.md")
69
+ outfile = File.open(outpath, "w")
70
+
71
+ infile.each_line do |line|
72
+ l = line.sub(/^date:\s*$/, "date: #{date} #{time}\n")
73
+ outfile.write(l)
74
+ end
75
+
76
+ infile.close
77
+ outfile.close
78
+ FileUtils.rm(draftpath)
79
+
80
+ outpath
81
+ end
82
+
83
+ def unpublish(inpath)
84
+ # the drafts folder might not exist yet...create it just in case
85
+ FileUtils.mkdir_p(DRAFTS_FOLDER)
86
+
87
+ begin
88
+ infile = File.open(inpath, "r")
89
+ rescue Errno::ENOENT
90
+ @helper.bad_path(inpath)
91
+ end
92
+
93
+ slug = inpath.sub(/.*?\d{4}-\d{2}-\d{2}-(.*)/, '\1')
94
+ outpath = File.join(DRAFTS_FOLDER, slug)
95
+ outfile = File.open(outpath, "w")
96
+
97
+ infile.each_line do |line|
98
+ l = line.sub(/^date:\s*.*$/, "date:")
99
+ outfile.write(l)
100
+ end
101
+
102
+ infile.close
103
+ outfile.close
104
+ FileUtils.rm(inpath)
105
+
106
+ outpath
107
+ end
108
+
109
+ end
110
+ end
@@ -0,0 +1,113 @@
1
+ module MrPoole
2
+ class Helper
3
+
4
+ def initialize
5
+ # nothing to do here
6
+ end
7
+
8
+ # Check for a _posts directory in current directory
9
+ # If we don't find one, puke an error message and die
10
+ def ensure_jekyll_dir
11
+ unless Dir.exists?('./_posts')
12
+ puts 'ERROR: Cannot locate _posts directory. Double check to make sure'
13
+ puts ' that you are in a jekyll directory.'
14
+ exit
15
+ end
16
+ end
17
+
18
+ # Configure the default layout.
19
+ #
20
+ # If a user has $HOME/.poole_default_layout, will use the contents of
21
+ # that file, otherwise will use a simple template
22
+ def get_default_layout
23
+ config_path = File.join(Dir.home, '.poole_default_layout')
24
+
25
+ if File.exists?(config_path)
26
+ return File.open(config_path, 'r').read
27
+ else
28
+ s = "---\n"
29
+ s << "title:\n"
30
+ s << "layout: post\n"
31
+ s << "date:\n"
32
+ s << "---\n"
33
+ end
34
+ end
35
+
36
+ # Given a post title (mixed case, spaces, etc.), generates a slug for
37
+ # This clobbers any non-ASCII text (TODO don't do that)
38
+ def get_slug_for(title)
39
+ title.downcase.gsub(/[^a-z0-9_\s-]/, '').gsub(/\s+/, '_')
40
+ end
41
+
42
+ def get_date_stamp
43
+ Time.now.strftime("%Y-%m-%d")
44
+ end
45
+
46
+ def get_time_stamp
47
+ Time.now.strftime("%H:%M")
48
+ end
49
+
50
+ def bad_path(path)
51
+ puts "Error: could not open #{path}"
52
+ exit
53
+ end
54
+
55
+ # Print a usage message and exit
56
+ def gen_usage
57
+ puts 'Usage:'
58
+ puts ' poole [ACTION] [ARG]'
59
+ puts ''
60
+ puts 'Actions:'
61
+ puts ' draft Create a new draft in _drafts with title SLUG'
62
+ puts ' post Create a new timestamped post in _posts with title SLUG'
63
+ puts ' publish Publish the draft with SLUG, timestamping appropriately'
64
+ puts ' unpublish Move a post to _drafts, untimestamping appropriately'
65
+ exit
66
+ end
67
+
68
+ def post_usage
69
+ puts 'Usage:'
70
+ puts ' poole post [OPTION] [ARG] TITLE'
71
+ puts ''
72
+ puts 'Options:'
73
+ puts ' --slug Define a custom slug for post, used for generated file name'
74
+ puts ' (also available with -s)'
75
+ puts ' --title Define a title for post (also available with -t)'
76
+ puts ' This option may be omitted provided that TITLE is given as'
77
+ puts ' the last argument to poole'
78
+ exit
79
+ end
80
+
81
+ def draft_usage
82
+ puts 'Usage:'
83
+ puts ' poole draft [OPTION] [ARG] TITLE'
84
+ puts ''
85
+ puts 'Options:'
86
+ puts ' --slug Define a custom slug for post, used for generated file name'
87
+ puts ' (also available with -s)'
88
+ puts ' --title Define a title for post (also available with -t)'
89
+ puts ' This option may be omitted provided that TITLE is given as'
90
+ puts ' the last argument to poole'
91
+ exit
92
+ end
93
+
94
+ def publish_usage
95
+ puts 'Usage:'
96
+ puts ' poole publish PATH_TO_DRAFT'
97
+ puts ''
98
+ puts 'Options:'
99
+ puts ' (coming soon)'
100
+ exit
101
+ end
102
+
103
+ def unpublish_usage
104
+ puts 'Usage:'
105
+ puts ' poole unpublish PATH_TO_POST'
106
+ puts ''
107
+ puts 'Options:'
108
+ puts ' (coming soon)'
109
+ exit
110
+ end
111
+
112
+ end
113
+ end
@@ -0,0 +1,3 @@
1
+ module MrPoole
2
+ VERSION = '0.1.0'
3
+ end
data/lib/mr_poole.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'fileutils'
2
+
3
+ require 'mr_poole/commands'
4
+ require 'mr_poole/helper'
5
+ require 'mr_poole/cli'
6
+
7
+ module MrPoole
8
+ end
data/mr_poole.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mr_poole/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mr_poole"
8
+ spec.version = MrPoole::VERSION
9
+ spec.authors = ["Michael McClimon"]
10
+ spec.email = ["michael@mcclimon.org"]
11
+ spec.description = %q{A butler for Jekyll, provides interface for creating posts/drafts}
12
+ spec.summary = <<-EOF
13
+ A butler for Jekyll. Provides a command-line interface (called `poole`) for
14
+ creating and publishing posts and drafts for Jekyll (http://jekyllrb.com)
15
+ blogs.
16
+
17
+ The literary Mr. Poole is Jekyll's butler, who "serves Jekyll faithfully, and
18
+ attempts to do a good job and be loyal to his master"
19
+ (http://en.wikipedia.org/wiki/Jekyll_and_hyde#Mr._Poole), and the
20
+ Mr. Poole gem looks to be the same thing.
21
+ EOF
22
+ spec.homepage = "http://github.com/mmcclimon/mr_poole"
23
+ spec.license = "MIT"
24
+
25
+ spec.files = `git ls-files`.split($/)
26
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
27
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_development_dependency "bundler", "~> 1.3"
31
+ spec.add_development_dependency "rspec"
32
+ spec.add_development_dependency "rake"
33
+ end
data/spec/cli_spec.rb ADDED
@@ -0,0 +1,328 @@
1
+ require 'spec_helper'
2
+
3
+ require 'mr_poole'
4
+ require 'fileutils'
5
+ require 'stringio'
6
+
7
+ module MrPoole
8
+ describe CLI do
9
+
10
+ context 'should determine jekyll dir correctly' do
11
+
12
+ it 'should exit with no _posts directory' do
13
+ olddir, tmpdir = make_no_jekyll_dir
14
+
15
+ argv = []
16
+ output = capture_stdout do
17
+ begin
18
+ cli = CLI.new(argv)
19
+ rescue SystemExit => e
20
+ e.should be_instance_of(SystemExit)
21
+ end
22
+ end
23
+
24
+ clean_tmp_files(tmpdir, olddir)
25
+ end
26
+
27
+ it 'should not exit with _posts directory' do
28
+ olddir, tmpdir = make_jekyll_dir
29
+
30
+ argv = []
31
+ lambda { cli = CLI.new(argv) }.should_not raise_error
32
+
33
+ clean_tmp_files(tmpdir, olddir)
34
+ end
35
+
36
+ end # end context determine jekyll dir
37
+
38
+ describe "action 'post'" do
39
+
40
+ before :each do
41
+ @olddir, @tmpdir = make_jekyll_dir
42
+ end
43
+
44
+ after :each do
45
+ clean_tmp_files(@tmpdir, @olddir)
46
+ end
47
+
48
+ context 'error handling' do
49
+
50
+ it 'should fail with no arguments' do
51
+ argv = ['post']
52
+
53
+ expect {
54
+ poole_with_args_no_stdout(argv).call
55
+ }.to raise_error(SystemExit)
56
+ end
57
+
58
+ it 'should fail with no title (with slug)' do
59
+ argv = ['post', '-s', 'post_slug']
60
+
61
+ expect {
62
+ poole_with_args_no_stdout(argv).call
63
+ }.to raise_error(SystemExit)
64
+ end
65
+
66
+ it 'should not fail with a title (no switch)' do
67
+ argv = ['post', 'Here is a title']
68
+
69
+ expect {
70
+ poole_with_args_no_stdout(argv).call
71
+ }.not_to raise_error
72
+ end
73
+
74
+ it 'should not fail with a title (long switch)' do
75
+ argv = ['post', '--title', 'Here is a title']
76
+
77
+ expect {
78
+ poole_with_args_no_stdout(argv).call
79
+ }.not_to raise_error
80
+ end
81
+
82
+ it 'should not fail with a title (short switch)' do
83
+ argv = ['post', '-t', 'Here is a title']
84
+
85
+ expect {
86
+ poole_with_args_no_stdout(argv).call
87
+ }.not_to raise_error
88
+ end
89
+
90
+ end # context error handling
91
+
92
+ context 'exit message' do
93
+
94
+ it 'should exit with a usage message' do
95
+ argv = ['post']
96
+
97
+ output = capture_stdout do
98
+ begin
99
+ poole_with_args(argv).call
100
+ rescue SystemExit => e
101
+ # this will fail, but we want the exit message
102
+ end
103
+ end
104
+
105
+ output.should match(/Usage:\s+poole post/)
106
+ end
107
+
108
+ end # context exit message
109
+
110
+ end # end describe post
111
+
112
+ describe "action 'draft'" do
113
+
114
+ before :each do
115
+ @olddir, @tmpdir = make_jekyll_dir
116
+ end
117
+
118
+ after :each do
119
+ clean_tmp_files(@tmpdir, @olddir)
120
+ end
121
+
122
+ context 'error handling' do
123
+
124
+ it 'should fail with no arguments' do
125
+ argv = ['draft']
126
+
127
+ expect {
128
+ poole_with_args_no_stdout(argv).call
129
+ }.to raise_error(SystemExit)
130
+ end
131
+
132
+ it 'should fail with no title (with slug)' do
133
+ argv = ['draft', '-s', 'draft_slug']
134
+
135
+ expect {
136
+ poole_with_args_no_stdout(argv).call
137
+ }.to raise_error(SystemExit)
138
+ end
139
+
140
+ it 'should not fail with a title (no switch)' do
141
+ argv = ['draft', 'Here is a title']
142
+
143
+ expect {
144
+ poole_with_args_no_stdout(argv).call
145
+ }.not_to raise_error
146
+ end
147
+
148
+ it 'should not fail with a title (long switch)' do
149
+ argv = ['draft', '--title', 'Here is a title']
150
+
151
+ expect {
152
+ poole_with_args_no_stdout(argv).call
153
+ }.not_to raise_error
154
+ end
155
+
156
+ it 'should not fail with a title (short switch)' do
157
+ argv = ['draft', '-t', 'Here is a title']
158
+
159
+ expect {
160
+ poole_with_args_no_stdout(argv).call
161
+ }.not_to raise_error
162
+ end
163
+
164
+ end # context error handling
165
+
166
+ context 'exit message' do
167
+
168
+ it 'should exit with a usage message' do
169
+ argv = ['draft']
170
+
171
+ output = capture_stdout do
172
+ begin
173
+ poole_with_args(argv).call
174
+ rescue SystemExit
175
+ # this will fail, but we want the exit message
176
+ end
177
+ end
178
+
179
+ output.should match(/Usage:\s+poole draft/)
180
+ end
181
+
182
+ end # context exit message
183
+
184
+ end # end describe draft
185
+
186
+ describe "action 'publish'" do
187
+ before :each do
188
+ @olddir, @tmpdir = make_jekyll_dir
189
+ @c = Commands.new
190
+ @d_path = @c.draft('test_draft')
191
+ end
192
+
193
+ after :each do
194
+ clean_tmp_files(@tmpdir, @olddir)
195
+ end
196
+
197
+ context 'error handling' do
198
+
199
+ it 'should fail with no arguments' do
200
+ argv = ['publish']
201
+
202
+ expect {
203
+ poole_with_args_no_stdout(argv).call
204
+ }.to raise_error(SystemExit)
205
+ end
206
+
207
+ it 'should fail with a bad path' do
208
+ argv = ['publish', '_drafts/does_not_exist.md']
209
+
210
+ expect {
211
+ poole_with_args_no_stdout(argv).call
212
+ }.to raise_error(SystemExit)
213
+ end
214
+
215
+ it 'should not fail with a good path' do
216
+ argv = ['publish', @d_path]
217
+
218
+ expect {
219
+ poole_with_args_no_stdout(argv).call
220
+ }.not_to raise_error
221
+ end
222
+
223
+ end # context error handling
224
+
225
+ context 'exit message' do
226
+
227
+ it 'should exit with usage with no arguments' do
228
+ argv = ['publish']
229
+
230
+ output = capture_stdout do
231
+ begin
232
+ poole_with_args(argv).call
233
+ rescue SystemExit
234
+ end
235
+ end
236
+
237
+ output.should match(/Usage:\s+poole publish/)
238
+ end
239
+
240
+ it 'should exit with a description of bad path' do
241
+ argv = ['publish', '_drafts/does_not_exist.md']
242
+
243
+ output = capture_stdout do
244
+ begin
245
+ poole_with_args(argv).call
246
+ rescue SystemExit
247
+ end
248
+ end
249
+
250
+ output.should match(/Error:\s+could not open/)
251
+ end
252
+ end # context exit message
253
+
254
+ end
255
+
256
+ describe "action 'unpublish'" do
257
+ before :each do
258
+ @olddir, @tmpdir = make_jekyll_dir
259
+ @c = Commands.new
260
+ @p_path = @c.post('test_post')
261
+ end
262
+
263
+ after :each do
264
+ clean_tmp_files(@tmpdir, @olddir)
265
+ end
266
+
267
+ context 'error handling' do
268
+
269
+ it 'should fail with no arguments' do
270
+ argv = ['unpublish']
271
+
272
+ expect {
273
+ poole_with_args_no_stdout(argv).call
274
+ }.to raise_error(SystemExit)
275
+ end
276
+
277
+ it 'should fail with a bad path' do
278
+ argv = ['unpublish', '_posts/does_not_exist.md']
279
+
280
+ expect {
281
+ poole_with_args_no_stdout(argv).call
282
+ }.to raise_error(SystemExit)
283
+ end
284
+
285
+ it 'should not fail with a good path' do
286
+ argv = ['unpublish', @p_path]
287
+
288
+ expect {
289
+ poole_with_args_no_stdout(argv).call
290
+ }.not_to raise_error
291
+ end
292
+
293
+ end # context error handling
294
+
295
+ context 'exit message' do
296
+
297
+ it 'should exit with usage with no arguments' do
298
+ argv = ['unpublish']
299
+
300
+ output = capture_stdout do
301
+ begin
302
+ poole_with_args(argv).call
303
+ rescue SystemExit
304
+ end
305
+ end
306
+
307
+ output.should match(/Usage:\s+poole unpublish/)
308
+ end
309
+
310
+ it 'should exit with a description of bad path' do
311
+ argv = ['unpublish', '_posts/does_not_exist.md']
312
+
313
+ output = capture_stdout do
314
+ begin
315
+ poole_with_args(argv).call
316
+ rescue SystemExit
317
+ end
318
+ end
319
+
320
+ output.should match(/Error:\s+could not open/)
321
+ end
322
+ end # context exit message
323
+
324
+
325
+ end # action unpublish
326
+
327
+ end
328
+ end
@@ -0,0 +1,290 @@
1
+ require 'spec_helper'
2
+
3
+ require 'mr_poole'
4
+
5
+ module MrPoole
6
+ describe Commands do
7
+
8
+ before :all do
9
+ @date_regex = %r{\d{4}-\d{2}-\d{2}}
10
+ end
11
+
12
+ before :each do
13
+ @c = Commands.new
14
+ @olddir, @tmpdir = make_jekyll_dir
15
+ end
16
+
17
+ after :each do
18
+ clean_tmp_files(@tmpdir, @olddir)
19
+ end
20
+
21
+ describe "#post" do
22
+ context 'title only' do
23
+
24
+ it "should create a new post in the _posts directory" do
25
+ @c.post("test_post")
26
+ Dir.glob("_posts/*.md").length.should == 1
27
+ end
28
+
29
+ it "should create a timestamped post in the _posts directory" do
30
+ @c.post("test_post")
31
+ fn = Dir.glob("_posts/*.md").first
32
+ fn.should match(/#{@date_regex}-test_post[.]md$/)
33
+ end
34
+
35
+ it "should return path to the newly created post" do
36
+ returned = @c.post("test_post")
37
+ determined = Dir.glob("_posts/*.md").first
38
+ returned.should == determined
39
+ end
40
+
41
+ it "should downcase a title" do
42
+ @c.post("Test_Post_With_Uppercase")
43
+ fn = Dir.glob("_posts/*.md").first
44
+ fn.should match(/#{@date_regex}-test_post_with_uppercase[.]md/)
45
+ end
46
+
47
+ it "should sub underscores for spaces in title" do
48
+ @c.post("Test Post with Spaces")
49
+ fn = Dir.glob("_posts/*.md").first
50
+ fn.should match(/#{@date_regex}-test_post_with_spaces[.]md/)
51
+ end
52
+
53
+ it "should remove non-word characters for slug" do
54
+ @c.post("On (function() {}()) in JavaScript")
55
+ fn = Dir.glob("_posts/*.md").first
56
+ fn.should match(/#{@date_regex}-on_function_in_javascript[.]md/)
57
+ end
58
+
59
+ it "should update the title in the file itself" do
60
+ @c.post("Testing Post {}")
61
+ fn = Dir.glob("_posts/*.md").first
62
+ content = File.open(fn, 'r').read
63
+ content.should match(/title: Testing Post {}/)
64
+ end
65
+
66
+ it "should update the date in the file itself" do
67
+ @c.post("Date test post")
68
+ fn = Dir.glob("_posts/*.md").first
69
+
70
+ # date in filename should match date in file itself
71
+ date = fn.match(/(#{@date_regex})-date_test_post[.]md/)[1]
72
+ content = File.open(fn, 'r').read
73
+ content.should match(/date: #{date}/)
74
+ end
75
+
76
+ end # end context title only
77
+
78
+ context 'title and slug' do
79
+
80
+ it "should create a post named for slug" do
81
+ @c.post("Test Post", 'unique_slug')
82
+ fn = Dir.glob("_posts/*.md").first
83
+ fn.should match(/#{@date_regex}-unique_slug[.]md$/)
84
+ end
85
+
86
+ it "should sub any weird characters in slug" do
87
+ @c.post("Test Post with Spaces", "(stupid] {slüg/")
88
+ fn = Dir.glob("_posts/*.md").first
89
+ fn.should match(/#{@date_regex}-stupid_slg[.]md/)
90
+ end
91
+
92
+ it "should update the title in the file itself" do
93
+ @c.post("Testing Post {}", 'shouldnt_be_in_title')
94
+ fn = Dir.glob("_posts/*.md").first
95
+ content = File.open(fn, 'r').read
96
+ content.should match(/title: Testing Post {}/)
97
+ end
98
+
99
+ end # end context title & slug
100
+
101
+ end # end describe post
102
+
103
+ describe "#draft" do
104
+ context 'title only' do
105
+
106
+ it "should create a _drafts directory" do
107
+ @c.draft('draft post')
108
+ Dir.exists?('_drafts').should be_true
109
+ end
110
+
111
+ it "should create a new draft in the _drafts directory" do
112
+ @c.draft('draft post')
113
+ Dir.glob("_drafts/*.md").length.should == 1
114
+ end
115
+
116
+ it "should return path to the newly created draft" do
117
+ returned = @c.draft("test_draft")
118
+ determined = Dir.glob("_drafts/*.md").first
119
+ returned.should == determined
120
+ end
121
+
122
+ it "should create a non-timestamped draft" do
123
+ @c.draft('draft post')
124
+ fn = Dir.glob("_drafts/*.md").first
125
+ fn.should_not match(/#{@date_regex}/)
126
+ end
127
+
128
+ it "should downcase and underscore title for slug" do
129
+ @c.draft("Test Post with Spaces")
130
+ fn = Dir.glob("_drafts/*.md").first
131
+ fn.should match(/test_post_with_spaces[.]md/)
132
+ end
133
+
134
+ it "should remove non-word characters for slug" do
135
+ @c.draft("On (function() {}()) in JavaScript")
136
+ fn = Dir.glob("_drafts/*.md").first
137
+ fn.should match(/on_function_in_javascript[.]md/)
138
+ end
139
+
140
+ it "should update the title in the file itself" do
141
+ @c.draft("Testing Draft {}")
142
+ fn = Dir.glob("_drafts/*.md").first
143
+ content = File.open(fn, 'r').read
144
+ content.should match(/title: Testing Draft {}/)
145
+ end
146
+
147
+ it "should not update the date in the file itself" do
148
+ @c.draft("Date test post")
149
+ fn = Dir.glob("_drafts/*.md").first
150
+
151
+ # date in filename should match date in file itself
152
+ content = File.open(fn, 'r').read
153
+ content.should match(/date:\s*\n/)
154
+ end
155
+
156
+ end # end context title only
157
+
158
+ context 'title and slug' do
159
+
160
+ it "should create a draft named for slug" do
161
+ @c.draft("Test Draft", 'unique_slug')
162
+ fn = Dir.glob("_drafts/*.md").first
163
+ fn.should match(/unique_slug[.]md$/)
164
+ end
165
+
166
+ it "should sub any weird characters in slug" do
167
+ @c.draft("Test Post with Spaces", "(stupid] {slüg/")
168
+ fn = Dir.glob("_drafts/*.md").first
169
+ fn.should match(/stupid_slg[.]md/)
170
+ end
171
+
172
+ it "should update the title in the file itself" do
173
+ @c.draft("Testing Post {}", 'shouldnt_be_in_title')
174
+ fn = Dir.glob("_drafts/*.md").first
175
+ content = File.open(fn, 'r').read
176
+ content.should match(/title: Testing Post {}/)
177
+ end
178
+
179
+ end # end context title & slug
180
+
181
+ end # end describe draft
182
+
183
+ describe "#publish" do
184
+
185
+ before :each do
186
+ @d_path = @c.draft('test_draft')
187
+ end
188
+
189
+ it 'should create a timestamped post in the _posts folder' do
190
+ @c.publish(@d_path)
191
+ fn = Dir.glob("_posts/*.md").first
192
+ fn.should match(/#{@date_regex}-test_draft[.]md$/)
193
+ end
194
+
195
+ it 'should remove file in the _drafts folder' do
196
+ @c.publish(@d_path)
197
+ File.exist?(@d_path).should be_false
198
+ end
199
+
200
+ it 'should return path to newly created post' do
201
+ returned = @c.publish(@d_path)
202
+ determined = Dir.glob("_posts/*.md").first
203
+ returned.should == determined
204
+ end
205
+
206
+ it 'should create post with matching slug' do
207
+ post = @c.publish(@d_path)
208
+
209
+ draft_slug = File.basename(@d_path, '.md')
210
+ post_slug = post.match(/#{@date_regex}-(.*)[.]md/)[1]
211
+
212
+ post_slug.should == draft_slug
213
+ end
214
+
215
+ it 'should update timestamp in actual file' do
216
+ post = @c.publish(@d_path)
217
+ content = File.open(post, 'r').read
218
+ content.should match(/date: #{@date_regex} \d{2}:\d{2}\n/)
219
+ end
220
+
221
+ it 'should copy contents of draft into post' do
222
+ # first add some content to the draft
223
+ f = File.open(@d_path, 'a')
224
+ f.write("Some new content for my blog\n")
225
+ f.close
226
+
227
+ post = @c.publish(@d_path)
228
+ content = File.open(post, 'r').read
229
+ content.should match(/Some new content for my blog/)
230
+ end
231
+
232
+ end # end describe publish
233
+
234
+ describe "#unpublish" do
235
+
236
+ before :each do
237
+ @p_path = @c.post('test_post')
238
+ end
239
+
240
+ it 'should create a _drafts directory' do
241
+ @c.unpublish(@p_path)
242
+ Dir.exists?('_drafts').should be_true
243
+ end
244
+
245
+ it 'should create an untimestamped draft in the _drafts folder' do
246
+ @c.unpublish(@p_path)
247
+ fn = Dir.glob("_drafts/*.md").first
248
+ fn.should_not match(/#{@date_regex}/)
249
+ end
250
+
251
+ it 'should remove file in the _posts folder' do
252
+ @c.unpublish(@p_path)
253
+ File.exist?(@p_path).should be_false
254
+ end
255
+
256
+ it 'should return path to newly created draft' do
257
+ returned = @c.unpublish(@p_path)
258
+ determined = Dir.glob("_drafts/*.md").first
259
+ returned.should == determined
260
+ end
261
+
262
+ it 'should create draft with matching slug' do
263
+ draft = @c.unpublish(@p_path)
264
+
265
+ post_slug = @p_path.match(/#{@date_regex}-(.*)[.]md$/)[1]
266
+ draft_slug = File.basename(draft, '.md')
267
+
268
+ draft_slug.should == post_slug
269
+ end
270
+
271
+ it 'should delete timestamp in actual file' do
272
+ draft = @c.unpublish(@p_path)
273
+ content = File.open(draft, 'r').read
274
+ content.should match(/date:\s*\n/)
275
+ end
276
+
277
+ it 'should copy contents of post into draft' do
278
+ # first add some content to the draft
279
+ f = File.open(@p_path, 'a')
280
+ f.write("Some new content for my blog\n")
281
+ f.close
282
+
283
+ draft = @c.unpublish(@p_path)
284
+ content = File.open(draft, 'r').read
285
+ content.should match(/Some new content for my blog/)
286
+ end
287
+
288
+ end # end describe unpublish
289
+ end
290
+ end
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'stringio'
4
+ require 'tmpdir'
5
+ require 'fileutils'
6
+
7
+ def capture_stdout(&block)
8
+ stdout = $stdout
9
+ fake_out = StringIO.new
10
+ $stdout = fake_out
11
+ begin
12
+ yield
13
+ ensure
14
+ $stdout = stdout
15
+ end
16
+ fake_out.string
17
+ end
18
+
19
+ def make_no_jekyll_dir
20
+ olddir = Dir.pwd()
21
+ newdir = Dir.mktmpdir('nojekyll')
22
+ Dir.chdir(newdir)
23
+ return olddir, newdir
24
+ end
25
+
26
+ def make_jekyll_dir
27
+ olddir = Dir.pwd()
28
+ newdir = Dir.mktmpdir('jekyll')
29
+ posts = File.join(newdir, '_posts')
30
+ Dir.mkdir(posts)
31
+ Dir.chdir(newdir)
32
+ return olddir, newdir
33
+
34
+ end
35
+
36
+ def clean_tmp_files(tmpdir, restoredir)
37
+ Dir.chdir(restoredir)
38
+ FileUtils.rm_rf(tmpdir)
39
+ end
40
+
41
+ def poole_with_args(argv)
42
+ return Proc.new do
43
+ action = argv.shift
44
+ cli = MrPoole::CLI.new(argv)
45
+ cli.execute(action)
46
+ end
47
+ end
48
+
49
+ def poole_with_args_no_stdout(argv)
50
+ return Proc.new do
51
+ capture_stdout do
52
+ action = argv.shift
53
+ cli = MrPoole::CLI.new(argv)
54
+ cli.execute(action)
55
+ end
56
+ end
57
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mr_poole
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael McClimon
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-09-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: A butler for Jekyll, provides interface for creating posts/drafts
56
+ email:
57
+ - michael@mcclimon.org
58
+ executables:
59
+ - poole
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - .gitignore
64
+ - Gemfile
65
+ - LICENSE
66
+ - README.md
67
+ - Rakefile
68
+ - bin/poole
69
+ - lib/mr_poole.rb
70
+ - lib/mr_poole/cli.rb
71
+ - lib/mr_poole/commands.rb
72
+ - lib/mr_poole/helper.rb
73
+ - lib/mr_poole/version.rb
74
+ - mr_poole.gemspec
75
+ - spec/cli_spec.rb
76
+ - spec/command_spec.rb
77
+ - spec/spec_helper.rb
78
+ homepage: http://github.com/mmcclimon/mr_poole
79
+ licenses:
80
+ - MIT
81
+ metadata: {}
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubyforge_project:
98
+ rubygems_version: 2.1.4
99
+ signing_key:
100
+ specification_version: 4
101
+ summary: A butler for Jekyll. Provides a command-line interface (called `poole`) for
102
+ creating and publishing posts and drafts for Jekyll (http://jekyllrb.com) blogs. The
103
+ literary Mr. Poole is Jekyll's butler, who "serves Jekyll faithfully, and attempts
104
+ to do a good job and be loyal to his master" (http://en.wikipedia.org/wiki/Jekyll_and_hyde#Mr._Poole),
105
+ and the Mr. Poole gem looks to be the same thing.
106
+ test_files:
107
+ - spec/cli_spec.rb
108
+ - spec/command_spec.rb
109
+ - spec/spec_helper.rb