hawkins 0.1.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/hawkins/cli.rb DELETED
@@ -1,190 +0,0 @@
1
- require 'jekyll'
2
- require 'guard'
3
- require 'guard/runner'
4
- require 'listen'
5
-
6
- module Hawkins
7
- class Cli < Thor
8
- include Thor::Actions
9
-
10
- attr_accessor :jekyll_config
11
-
12
- def self.exit_on_failure?
13
- true
14
- end
15
-
16
- def initialize(*args)
17
- super
18
- Jekyll.logger.log_level = :warn
19
- @jekyll_config = Jekyll::Configuration[Jekyll::Configuration::DEFAULTS]
20
- @jekyll_config.read_config_files(@jekyll_config.config_files({}))
21
- end
22
-
23
- desc "post TITLE", "create a post"
24
- option :editor, :default => ENV['VISUAL'] || ENV['EDITOR'] || 'vi'
25
- option :date, :default => Time.now.to_s
26
- def post(title)
27
- begin
28
- date = Date.parse(options[:date])
29
- rescue
30
- # If an exception is not descended from the Thor::Error class, Thor
31
- # prints the stacktrace. We don't want a stacktrace in this instance,
32
- # so we lie and use one of Thor's error classes in the call to fail().
33
- fail(Thor::InvocationError, "ERROR: Could not parse '#{options[:date]}' as a date")
34
- end
35
- slug = title.to_url
36
- dest = path_to(:_posts)
37
-
38
- shell.mute { empty_directory(dest) }
39
-
40
- filename = "#{date.strftime('%Y-%m-%d')}-#{slug}.md"
41
- content = <<-CONTENT.gsub(/^\s*/, '')
42
- ---
43
- title: #{title}
44
- ---
45
- CONTENT
46
-
47
- create_file(path_to(dest, filename), content)
48
-
49
- case options[:editor]
50
- when /g?vim/
51
- editor_args = "+"
52
- when /x?emacs/
53
- editor_args = "+#{content.lines.count}"
54
- else
55
- editor_args = nil
56
- end
57
-
58
- exec(*[options[:editor], editor_args, path_to(dest, filename)].compact)
59
- end
60
-
61
- desc "isolate FILE ...", "work on a file or files in isolation (globs are allowed)"
62
- long_desc <<-LONGDESC
63
- Jekyll's regeneration capability is limited to regenerating the
64
- entire site. This option will ignore all files except the ones you
65
- specify in order to speed regeneration.
66
-
67
- Keep in mind that Jekyll's inclusion mechanism is not aware of
68
- subdirectories so this command operates on the basename of all files
69
- that match the file or glob you provide.
70
- LONGDESC
71
- option :future, :type => :boolean, :default => false, :desc => "publish future dated posts"
72
- option :drafts, :type => :boolean, :default => false, :desc => "publish draft posts"
73
- def isolate(*files)
74
- SafeYAML::OPTIONS[:default_mode] = :safe
75
-
76
- pages = []
77
- pages << Hawkins::DEFAULT_EXTENSIONS.map do |ext|
78
- Dir.glob("**/*.#{ext}")
79
- end
80
- pages.flatten!.reject! { |f| File.fnmatch?('_site/*', f) }
81
-
82
- pages.select! do |p|
83
- content = File.read(p)
84
- # Jekyll only renders pages with YAML frontmatter. See Jekyll's
85
- # convertible.rb read_yaml method.
86
- content =~ /\A(---\s*\n.*?\n?)^(---\s*$\n?)/m
87
- end
88
-
89
- isolation_config = {}
90
- isolation_config['exclude'] = pages.concat(jekyll_config['exclude'])
91
- isolation_config['include'] = Hawkins::DEFAULT_INCLUDES
92
-
93
- files.each do |glob|
94
- matches = Dir.glob(glob)
95
- matches.map! do |f|
96
- ext = File.extname(f)
97
- # If we have to add to this list from Rack, then Rack will have no
98
- # idea what the original file extension was. Use a wildcard to
99
- # avoid this issue.
100
- "#{File.basename(f, ext)}.*"
101
- end
102
- if matches.empty?
103
- raise Thor::Error.new("Could not find any matches for #{glob}.")
104
- end
105
- isolation_config['include'] += matches
106
- end
107
-
108
- isolation_config['include'].uniq!
109
-
110
- # We have to write the isolation config out to a file so that the Guard
111
- # can detect when the configuration changes.
112
- create_file(Hawkins::ISOLATION_FILE, YAML.dump(isolation_config))
113
-
114
- begin
115
- invoke(:serve, [], options)
116
- ensure
117
- remove_file(Hawkins::ISOLATION_FILE)
118
- end
119
- end
120
-
121
- desc "serve", "render and serve the site"
122
- option :future, :type => :boolean, :default => false, :desc => "publish future dated posts"
123
- option :drafts, :type => :boolean, :default => false, :desc => "publish draft posts"
124
- def serve
125
- config_files = jekyll_config.config_files({}) || []
126
- if File.exist?(Hawkins::ISOLATION_FILE)
127
- config_files << Hawkins::ISOLATION_FILE
128
- end
129
-
130
- # By default Jekyll uses the absolute path and Guard doesn't so we need to fix that.
131
- dest = Pathname.new(jekyll_config['destination']).relative_path_from(Pathname.new(Dir.pwd))
132
-
133
- contents = <<-GUARDFILE.gsub(/^\s*/, '')
134
- interactor :off
135
- notification :off
136
- guard 'hawkins',
137
- :config => #{config_files},
138
- :drafts => #{options[:drafts]},
139
- :future => #{options[:future]} do
140
- watch %r{.*}
141
- ignore %r{^#{dest}}
142
- end
143
-
144
- guard 'livereload',
145
- :grace_period => 5 do
146
- watch %r{.*}
147
- end
148
- GUARDFILE
149
- guard_start(contents)
150
- end
151
-
152
- # Methods in the no_tasks block are not exposed to users
153
- no_tasks do
154
- def guard_start(guardfile_contents)
155
- Guard.setup(:guardfile_contents => guardfile_contents)
156
- Guard::Runner.new.run(:start)
157
- Guard.listener.start
158
-
159
- exitcode = 0
160
- begin
161
- while Guard.interactor.foreground != :exit
162
- Guard.queue.process while Guard.queue.pending?
163
- end
164
- rescue Interrupt
165
- rescue SystemExit => e
166
- exitcode = e.status
167
- end
168
-
169
- guard_stop
170
- exitcode
171
- end
172
-
173
- def guard_stop
174
- Guard.listener.stop
175
- Guard.interactor.background
176
- Guard::Runner.new.run(:stop)
177
- end
178
-
179
- def self.source_root
180
- File.expand_path("..", File.dirname(__FILE__))
181
- end
182
-
183
- # Builds a path within the source_root
184
- def path_to(*args)
185
- args = args.map(&:to_s)
186
- File.join(*args)
187
- end
188
- end
189
- end
190
- end
data/lib/hawkins/guard.rb DELETED
@@ -1,268 +0,0 @@
1
- # encoding: UTF-8
2
-
3
- require 'benchmark'
4
- require 'guard/plugin'
5
- require 'hawkins'
6
- require 'jekyll'
7
- require 'thin'
8
-
9
- # Most of this is courtesy of the guard-jekyll-plus gem at
10
- # https://github.com/imathis/guard-jekyll-plus
11
-
12
- module Guard
13
- class Hawkins < Plugin
14
- def initialize(options={})
15
- super
16
-
17
- default_extensions = ::Hawkins::DEFAULT_EXTENSIONS
18
-
19
- @options = {
20
- :extensions => [],
21
- :config => Jekyll::Configuration.new.config_files({}),
22
- :drafts => false,
23
- :future => false,
24
- :config_hash => nil,
25
- :silent => false,
26
- :msg_prefix => 'Jekyll'
27
- }.merge(options)
28
-
29
- @config = load_config(@options)
30
- @source = local_path(@config['source'])
31
- @destination = local_path(@config['destination'])
32
- @msg_prefix = @options[:msg_prefix]
33
- @app_prefix = 'Hawkins'
34
-
35
- # Convert array of extensions into a regex for matching file extensions
36
- # E.g. /\.md$|\.markdown$|\.html$/i
37
- extensions = @options[:extensions].concat(default_extensions).flatten.uniq
38
- @extensions = Regexp.new(
39
- extensions.map { |e| (e << '$').gsub('\.', '\\.') }.join('|'),
40
- true
41
- )
42
-
43
- # set Jekyll server thread to nil
44
- @server_thread = nil
45
-
46
- # Create a Jekyll site
47
- @site = Jekyll::Site.new(@config)
48
- end
49
-
50
- def load_config(options)
51
- config = jekyll_config(options)
52
-
53
- # Override configuration with option values
54
- config['show_drafts'] ||= options[:drafts]
55
- config['future'] ||= options[:future]
56
- config
57
- end
58
-
59
- def reload_config!
60
- UI.info "Reloading Jekyll configuration!"
61
- @config = load_config(@options)
62
- end
63
-
64
- def start
65
- build
66
- start_server
67
- return if @config[:silent]
68
- msg = "#{@app_prefix} "
69
- msg += "watching and serving at #{@config['host']}:#{@config['port']}#{@config['baseurl']}"
70
- UI.info(msg)
71
- end
72
-
73
- def reload
74
- stop if !@server_thread.nil? && @server_thread.alive?
75
- reload_config!
76
- start
77
- end
78
-
79
- def reload_server
80
- stop_server
81
- start_server
82
- end
83
-
84
- def stop
85
- stop_server
86
- end
87
-
88
- def run_on_modifications(paths)
89
- # At this point we know @options[:config] is going to be an Array
90
- # thanks to the call to jekyll_config earlier.
91
- reload_config! if @options[:config].map { |f| paths.include?(f) }.any?
92
- matched = jekyll_matches paths
93
- unmatched = non_jekyll_matches paths
94
-
95
- if matched.size > 0
96
- build(matched, "Files changed: ", " ~ ".yellow)
97
- elsif unmatched.size > 0
98
- copy(unmatched)
99
- end
100
- end
101
-
102
- def run_on_additions(paths)
103
- matched = jekyll_matches paths
104
- unmatched = non_jekyll_matches paths
105
-
106
- if matched.size > 0
107
- build(matched, "Files added: ", " + ".green)
108
- elsif unmatched.size > 0
109
- copy(unmatched)
110
- end
111
- end
112
-
113
- def run_on_removals(paths)
114
- matched = jekyll_matches paths
115
- unmatched = non_jekyll_matches paths
116
-
117
- if matched.size > 0
118
- build(matched, "Files removed: ", " x ".red)
119
- elsif unmatched.size > 0
120
- remove(unmatched)
121
- end
122
- end
123
-
124
- private
125
-
126
- def build(files=nil, message='', mark=nil)
127
- UI.info "#{@msg_prefix} #{message}" + "building...".yellow unless @config[:silent]
128
- if files
129
- puts '| '
130
- files.each { |file| puts '|' + mark + file }
131
- puts '| '
132
- end
133
- elapsed = Benchmark.realtime { Jekyll::Site.new(@config).process }
134
- unless @config[:silent]
135
- msg = "#{@msg_prefix} " + "build completed in #{elapsed.round(2)}s ".green
136
- msg += "#{@source} → #{@destination}"
137
- UI.info(msg)
138
- end
139
- rescue
140
- UI.error("#{@msg_prefix} build has failed") unless @config[:silent]
141
- stop_server
142
- throw :task_has_failed
143
- end
144
-
145
- # Copy static files to destination directory
146
- #
147
- def copy(files=[])
148
- files = ignore_stitch_sources files
149
- return false unless files.size > 0
150
- begin
151
- message = 'copied file'
152
- message += 's' if files.size > 1
153
- UI.info "#{@msg_prefix} #{message.green}" unless @config[:silent]
154
- puts '| '
155
- files.each do |file|
156
- path = destination_path file
157
- FileUtils.mkdir_p(File.dirname(path))
158
- FileUtils.cp(file, path)
159
- puts '|' + " → ".green + path
160
- end
161
- puts '| '
162
-
163
- rescue
164
- UI.error "#{@msg_prefix} copy has failed" unless @config[:silent]
165
- UI.error e
166
- stop_server
167
- throw :task_has_failed
168
- end
169
- true
170
- end
171
-
172
- # Remove deleted source file/directories from destination
173
- def remove(files=[])
174
- # Ensure at least one file still exists (other scripts may clean up too)
175
- return false unless files.select { |f| File.exist? f }.size > 0
176
- begin
177
- message = 'removed file'
178
- message += 's' if files.size > 1
179
- UI.info "#{@msg_prefix} #{message.red}" unless @config[:silent]
180
- puts '| '
181
-
182
- files.each do |file|
183
- path = destination_path file
184
- if File.exist?(path)
185
- FileUtils.rm(path)
186
- puts '|' + " x ".red + path
187
- end
188
-
189
- dir = File.dirname(path)
190
- next unless Dir[File.join(dir, '*')].empty?
191
- FileUtils.rm_r(dir)
192
- puts '|' + " x ".red + dir
193
- end
194
- puts '| '
195
-
196
- rescue
197
- UI.error "#{@msg_prefix} remove has failed" unless @config[:silent]
198
- UI.error e
199
- stop_server
200
- throw :task_has_failed
201
- end
202
- true
203
- end
204
-
205
- def jekyll_matches(paths)
206
- paths.select { |file| file =~ @extensions }
207
- end
208
-
209
- def non_jekyll_matches(paths)
210
- paths.select { |file| !file.match(/^_/) && !file.match(@extensions) }
211
- end
212
-
213
- def jekyll_config(options)
214
- if options[:config_hash]
215
- config = options[:config_hash]
216
- elsif options[:config]
217
- options[:config] = [options[:config]] unless options[:config].is_a? Array
218
- config = options
219
- end
220
- Jekyll.configuration(config)
221
- end
222
-
223
- # TODO Use Pathname.relative_path_from or similar here
224
- def local_path(path)
225
- Dir.chdir('.')
226
- current = Dir.pwd
227
- path = path.sub current, ''
228
- if path == ''
229
- './'
230
- else
231
- path.sub(/^\//, '')
232
- end
233
- end
234
-
235
- def destination_path(file)
236
- if @source =~ /^\./
237
- File.join(@destination, file)
238
- else
239
- file.sub(/^#{@source}/, "#{@destination}")
240
- end
241
- end
242
-
243
- def start_server
244
- if @server_thread.nil?
245
- @server_thread = Thread.new do
246
- Thin::Server.start(@config['host'], @config['port'], :signals => false) do
247
- require 'rack/livereload'
248
- require 'hawkins/isolation'
249
- use Rack::LiveReload,
250
- :min_delay => 500,
251
- :max_delay => 2000,
252
- :no_swf => true,
253
- :source => :vendored
254
- run ::Hawkins::IsolationInjector.new
255
- end
256
- end
257
- UI.info "#{@app_prefix} running Rack" unless @config[:silent]
258
- else
259
- UI.warning "#{@app_prefix} using an old server thread!"
260
- end
261
- end
262
-
263
- def stop_server
264
- @server_thread.kill unless @server_thread.nil?
265
- @server_thread = nil
266
- end
267
- end
268
- end