mr_poole 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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