hawkins 0.1.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +31 -6
- data/LICENSE.txt +2 -0
- data/README.md +43 -14
- data/Rakefile +13 -10
- data/hawkins.gemspec +7 -11
- data/js/WebSocketMain.swf +0 -0
- data/js/livereload.js +1055 -0
- data/js/swfobject.js +4 -0
- data/js/web_socket.js +379 -0
- data/lib/hawkins.rb +6 -40
- data/lib/hawkins/liveserve.rb +250 -0
- data/lib/hawkins/post.rb +86 -0
- data/lib/hawkins/servlet.rb +154 -0
- data/lib/hawkins/version.rb +1 -1
- data/lib/hawkins/websockets.rb +160 -0
- data/lib/mime.types +800 -0
- data/test/resources/test.crt +18 -0
- data/test/resources/test.key +28 -0
- data/test/spec_helper.rb +13 -10
- data/test/test_liveserve.rb +166 -0
- data/test/test_post.rb +171 -0
- metadata +50 -94
- data/lib/hawkins/cli.rb +0 -190
- data/lib/hawkins/guard.rb +0 -268
- data/lib/hawkins/isolation.rb +0 -116
- data/test/test_hawkins.rb +0 -134
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
|