hawkins 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.
@@ -0,0 +1,268 @@
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
@@ -0,0 +1,116 @@
1
+ require 'pathname'
2
+ require 'rack'
3
+ require 'safe_yaml/load'
4
+ require 'set'
5
+
6
+ module Hawkins
7
+ class IsolationInjector
8
+ attr_reader :site_root
9
+ attr_reader :isolation_file
10
+
11
+ def initialize(options={})
12
+ @options = options
13
+ @site_root = @options[:site_root] || Jekyll.configuration({})['destination']
14
+ @isolation_file = @options[:isolation_file] || Hawkins::ISOLATION_FILE
15
+ SafeYAML::OPTIONS[:default_mode] = :safe
16
+ end
17
+
18
+ def call(env)
19
+ req = Rack::Request.new(env)
20
+ path = Pathname.new(req.path_info).relative_path_from(Pathname.new('/'))
21
+ path = File.join(site_root, path.to_s)
22
+
23
+ path = File.join(path, "index.html") if File.directory?(path)
24
+ path = Pathname.new(path).cleanpath.to_s
25
+
26
+ files = Dir[File.join(site_root, "**/*")]
27
+ if files.include?(path)
28
+ mime = mime(path)
29
+ file = file_info(path)
30
+ body = file[:body]
31
+ time = file[:time]
32
+ hdrs = {'Last-Modified' => time}
33
+
34
+ if time == req.env['HTTP_IF_MODIFIED_SINCE']
35
+ [304, hdrs, []]
36
+ else
37
+ hdrs.update(
38
+ 'Content-length' => body.bytesize.to_s,
39
+ 'Content-Type' => mime
40
+ )
41
+ [200, hdrs, [body]]
42
+ end
43
+ else
44
+ handle_404(req, path)
45
+ end
46
+ end
47
+
48
+ def handle_404(req, true_path)
49
+ if File.exist?(isolation_file)
50
+ file = true_path
51
+ # Use a wildcard since the origin file could be anything
52
+ file = "#{File.basename(file, File.extname(file))}.*".force_encoding('utf-8')
53
+
54
+ config = SafeYAML.load_file(isolation_file)
55
+
56
+ file_set = Set.new(config['include'])
57
+
58
+ # Prevent loops. If it's already in 'include'
59
+ # then we've gone through here before.
60
+ return static_error if file_set.include?(file)
61
+
62
+ config['include'] = file_set.add(file).to_a
63
+
64
+ File.open(isolation_file, 'w') do |f|
65
+ YAML.dump(config, f)
66
+ end
67
+
68
+ response = <<-PAGE.gsub(/^\s*/, '')
69
+ <!DOCTYPE HTML>
70
+ <html lang="en-US">
71
+ <head>
72
+ <meta charset="UTF-8">
73
+ <title>Rendering #{req.path_info}</title>
74
+ </head>
75
+ <body>
76
+ <h1>Hold on while I render that page for you!</h1>
77
+ </body>
78
+ PAGE
79
+
80
+ headers = {}
81
+ headers['Content-Length'] = response.bytesize.to_s
82
+ headers['Content-Type'] = 'text/html'
83
+ headers['Connection'] = 'keep-alive'
84
+ [200, headers, [response]]
85
+ else
86
+ static_error
87
+ end
88
+ end
89
+
90
+ def static_error
91
+ error_page = File.join(site_root, "404.html")
92
+ if File.exist?(error_page)
93
+ body = file_info(error_page)[:body]
94
+ mime = mime(error_page)
95
+ else
96
+ body = "Not found"
97
+ mime = "text/plain"
98
+ end
99
+ [404, {"Content-Type" => mime, "Content-length" => body.bytesize.to_s}, [body]]
100
+ end
101
+
102
+ def mime(path_info)
103
+ Rack::Mime.mime_type(File.extname(path_info))
104
+ end
105
+
106
+ def file_info(path)
107
+ File.open(path, 'r') do |f|
108
+ {
109
+ :body => f.read,
110
+ :time => f.mtime.httpdate,
111
+ :expand_path => path
112
+ }
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,3 @@
1
+ module Hawkins
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,21 @@
1
+ # Do not use tabs in this file!
2
+ # Required for running on Ruby 1.9
3
+ encoding: utf-8
4
+
5
+ # Change this if we ever need the site to reside in a sub-directory
6
+ baseurl: ''
7
+
8
+ permalink: /:year/:title.html
9
+ paginate: 8
10
+ paginate_path: /news/page:num
11
+
12
+ markdown: kramdown
13
+ kramdown:
14
+ # Use GitHub flavor markdown
15
+ input: GFM
16
+ hard_wrap: false
17
+
18
+ # See http://jekyllrb.com/docs/configuration/#frontmatter-defaults
19
+ defaults:
20
+ - scope: { type: "posts" }
21
+ values: { layout: "default" }
@@ -0,0 +1,5 @@
1
+ <footer>
2
+ <div>
3
+ Some footer content
4
+ </div>
5
+ </footer>
@@ -0,0 +1,8 @@
1
+ <header>
2
+ <nav role="navigation">
3
+ <ul>
4
+ <li>Nav Item 1</li>
5
+ <li>Nav Item 2</li>
6
+ </ul>
7
+ </nav>
8
+ </header>
@@ -0,0 +1,6 @@
1
+ <!DOCTYPE HTML>
2
+ <html lang="en-US">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Hawkins Test - {{ page.title }}</title>
6
+ <link rel="stylesheet" href="{{ site.baseurl }}/assets/hawkins.css" />
@@ -0,0 +1,10 @@
1
+ {% include top.html %}
2
+ </head>
3
+ <body>
4
+ {% include header.html %}
5
+ <div id="page-content">
6
+ {{ content }}
7
+ </div>
8
+ {% include footer.html %}
9
+ </body>
10
+ </html>