hawkins 0.1.0

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