glim 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,375 @@
1
+ require 'exception'
2
+ require 'listen'
3
+ require 'mime/types'
4
+ require 'socket'
5
+ require 'webrick'
6
+ require 'websocket'
7
+
8
+ module WebSocket
9
+ class Connection
10
+ attr_reader :socket
11
+
12
+ def self.establish(socket)
13
+ handshake = WebSocket::Handshake::Server.new
14
+ handshake << socket.gets until handshake.finished?
15
+
16
+ raise "Malformed handshake received from WebSocket client" unless handshake.valid?
17
+
18
+ socket.puts(handshake.to_s)
19
+ Connection.new(socket, handshake)
20
+ end
21
+
22
+ def initialize(socket, handshake)
23
+ @socket = socket
24
+ @handshake = handshake
25
+ end
26
+
27
+ def puts(message)
28
+ frame = WebSocket::Frame::Outgoing::Server.new(version: @handshake.version, data: message, type: :text)
29
+ @socket.puts(frame.to_s)
30
+ end
31
+
32
+ def each_message
33
+ frame = WebSocket::Frame::Incoming::Server.new(version: @handshake.version)
34
+ frame << @socket.read_nonblock(4096)
35
+ while message = frame.next
36
+ yield message
37
+ end
38
+ end
39
+ end
40
+
41
+ class Server
42
+ def initialize(host: 'localhost', port: nil)
43
+ @server, @rd_pipe, @wr_pipe = TCPServer.new(host, port), *IO.pipe
44
+ end
45
+
46
+ def broadcast(message)
47
+ @wr_pipe.puts(message)
48
+ end
49
+
50
+ def shutdown
51
+ broadcast('shutdown')
52
+
53
+ @wr_pipe.close
54
+ @wr_pipe = nil
55
+
56
+ @thread.join
57
+
58
+ @server.close
59
+ @server = nil
60
+ end
61
+
62
+ def start
63
+ @thread = Thread.new do
64
+ connections = []
65
+ running = true
66
+ while running
67
+ rs, _, _ = IO.select([ @server, @rd_pipe, *connections.map { |conn| conn.socket } ])
68
+ rs.each do |socket|
69
+ if socket == @server
70
+ socket = @server.accept
71
+ begin
72
+ connections << Connection.establish(socket)
73
+ rescue => e
74
+ $log.warn("Failed to perform handshake with new WebSocket client: #{e}", e)
75
+ socket.close
76
+ end
77
+ elsif socket == @rd_pipe
78
+ message = @rd_pipe.gets.chomp
79
+ if message == 'shutdown'
80
+ running = false
81
+ break
82
+ end
83
+ $log.debug("Send ‘#{message}’ to #{connections.count} WebSocket #{connections.count == 1 ? 'client' : 'clients'}") unless connections.empty?
84
+ connections.each do |conn|
85
+ begin
86
+ conn.puts(message)
87
+ rescue => e
88
+ $log.warn("Error writing to WebSocket client socket: #{e}")
89
+ end
90
+ end
91
+ else
92
+ if conn = connections.find { |candidate| candidate.socket == socket }
93
+ begin
94
+ conn.each_message do |frame|
95
+ $log.debug("Received #{frame.to_s.size} bytes from WebSocket client: #{frame}") unless frame.to_s.empty?
96
+ end
97
+ rescue IO::WaitReadable
98
+ $log.warn("IO::WaitReadable exception while reading from WebSocket client")
99
+ rescue EOFError
100
+ conn.socket.close
101
+ connections.delete(conn)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ @rd_pipe.close
108
+ @rd_pipe = nil
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ module Glim
115
+ module LocalServer
116
+ class Servlet < WEBrick::HTTPServlet::AbstractServlet
117
+ @@mutex = Mutex.new
118
+
119
+ def initialize(server, config)
120
+ @config = config
121
+ end
122
+
123
+ def do_GET(request, response)
124
+ @@mutex.synchronize do
125
+ do_GET_impl(request, response)
126
+ end
127
+ end
128
+
129
+ def do_GET_impl(request, response)
130
+ status, mime_type, body, file = 200, nil, nil, nil
131
+
132
+ if request.path == '/.ws/script.js'
133
+ mime_type, body = self.mime_type_for(request.path), self.websocket_script
134
+ elsif page = self.find_page(request.path)
135
+ file = page
136
+ elsif dir = self.find_directory(request.path)
137
+ if request.path.end_with?('/')
138
+ if request.path == '/' || @config['show_dir_listing']
139
+ mime_type, body = 'text/html', self.directory_index_for_path(dir)
140
+ else
141
+ $log.warn("Directory index forbidden for: #{request.path}")
142
+ status = 403
143
+ end
144
+ else
145
+ response['Location'] = "#{dir}/"
146
+ status = 302
147
+ end
148
+ else
149
+ $log.warn("No file for request: #{request.path}")
150
+ status = 404
151
+ end
152
+
153
+ if status != 200 && body.nil? && file.nil?
154
+ unless file = self.find_error_page(status, request.path)
155
+ mime_type, body = 'text/html', self.error_page_for_status(status, request.path)
156
+ end
157
+ end
158
+
159
+ mime_type ||= file ? self.mime_type_for(file.output_path('/')) : 'text/plain'
160
+ body ||= content_for_file(file)
161
+
162
+ response['Cache-Control'] = 'no-cache, no-store, must-revalidate'
163
+ response['Pragma'] = 'no-cache'
164
+ response['Expires'] = '0'
165
+ response.status = status
166
+ response.content_type = mime_type
167
+ response.body = mime_type.start_with?('text/html') ? inject_reload_script(body) : body
168
+ end
169
+
170
+ def content_for_file(file)
171
+ if file.frontmatter?
172
+ begin
173
+ file.output
174
+ rescue Glim::Error => e
175
+ content = "<pre>#{e.messages.join("\n")}</pre>"
176
+ self.create_page("Error", "Exception raised for <code>#{file}</code>", content)
177
+ rescue => e
178
+ content = "<pre>#{e.to_s}</pre>"
179
+ self.create_page("Error", "Exception raised for <code>#{file}</code>", content)
180
+ end
181
+ else
182
+ File.read(file.path)
183
+ end
184
+ end
185
+
186
+ def find_page(path)
187
+ self.files.find do |file|
188
+ candidate = file.output_path('/')
189
+ if path == candidate || path + File.extname(candidate) == candidate
190
+ true
191
+ elsif path.end_with?('/')
192
+ File.basename(candidate, '.*') == 'index' && path + File.basename(candidate) == candidate
193
+ end
194
+ end
195
+ end
196
+
197
+ def find_error_page(status, path)
198
+ candidates = self.files.select do |file|
199
+ file.basename == status.to_s && path_descends_from?(path, File.dirname(file.output_path('/')))
200
+ end
201
+ candidates.max { |lhs, rhs| lhs.output_path('/').size <=> rhs.output_path('/').size }
202
+ end
203
+
204
+ def find_directory(path)
205
+ path = path.chomp('/') unless path == '/'
206
+ self.files.map { |file| File.dirname(file.output_path('/')) }.find { |dir| path == dir }
207
+ end
208
+
209
+ def directory_index_for_path(path)
210
+ candidates = self.files.map { |file| file.output_path('/') }
211
+ candidates = candidates.select { |candidate| path_descends_from?(candidate, path) }
212
+ candidates = candidates.map { |candidate| candidate.sub(/(^#{Regexp.escape(path.chomp('/'))}\/[^\/]+\/?).*/, '\1') }.sort.uniq
213
+ candidates.unshift(path + '/..') if path != '/'
214
+
215
+ heading = "Index of <code>#{path}</code>"
216
+ content = candidates.map do |candidate|
217
+ "<li><a href = '#{candidate}'>#{candidate.sub(/.*?([^\/]+\/?)$/, '\1')}</a></li>"
218
+ end
219
+
220
+ self.create_page("Directory Index", heading, "<ul>#{content.join("\n")}</ul>")
221
+ end
222
+
223
+ def error_page_for_status(status, path)
224
+ case status
225
+ when 302 then title, heading, content = "302 Redirecting…", "Redirecting…", "Your browser should have redirected you."
226
+ when 403 then title, heading, content = "403 Forbidden", "Forbidden", "You don't have permission to access <code>#{path}</code> on this server."
227
+ when 404 then title, heading, content = "404 Not Found", "Not Found", "The requested URL <code>#{path}</code> was not found on this server."
228
+ else title, heading, content = "Error #{status}", "Error #{status}", "No detailed description of this error."
229
+ end
230
+ self.create_page(title, heading, content)
231
+ end
232
+
233
+ def websocket_script
234
+ <<~JS
235
+ const glim = {
236
+ connect: function (host, port, should_try, should_reload) {
237
+ const server = host + ":" + port
238
+ console.log("Connecting to Glim’s live reload server (" + server + ")…");
239
+
240
+ const socket = new WebSocket("ws://" + server + "/socket");
241
+
242
+ socket.onopen = () => {
243
+ console.log("Established connection: Live reload enabled.")
244
+ if(should_reload) {
245
+ document.location.reload(true);
246
+ }
247
+ };
248
+
249
+ socket.onmessage = (event) => {
250
+ console.log("Message from live reload server: " + event.data);
251
+
252
+ if(event.data == 'reload') {
253
+ document.location.reload(true);
254
+ }
255
+ else if(event.data == 'close') {
256
+ window.close();
257
+ }
258
+ };
259
+
260
+ socket.onclose = () => {
261
+ console.log("Lost connection: Live reload disabled.")
262
+
263
+ if(should_try) {
264
+ window.setTimeout(() => this.connect(host, port, should_try, true), 2500);
265
+ }
266
+ };
267
+ },
268
+ };
269
+
270
+ glim.connect('#{@config['host']}', #{@config['livereload_port']}, true /* should_try */, false /* should_reload */);
271
+ JS
272
+ end
273
+
274
+ def path_descends_from?(path, parent)
275
+ parent == '/' || path[parent.chomp('/').size] == '/' && path.start_with?(parent)
276
+ end
277
+
278
+ def create_page(title, heading, content)
279
+ <<~HTML
280
+ <style>body {
281
+ font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
282
+ }
283
+ </style>
284
+ <title>#{title}</title>
285
+ <h1>#{heading}</h1>
286
+ #{content}
287
+ HTML
288
+ end
289
+
290
+ def inject_reload_script(content)
291
+ return content unless @config['livereload']
292
+
293
+ script_tag = "<script src='#{@config['url']}/.ws/script.js'></script>"
294
+ if content =~ /<head.*?>/
295
+ content = "#$`#$&#{script_tag}#$'"
296
+ elsif content =~ /<html.*?>/
297
+ content = "#$`#$&#{script_tag}#$'"
298
+ else
299
+ content = script_tag + content
300
+ end
301
+ end
302
+
303
+ def files
304
+ @config.site.files_and_documents.select { |file| file.write? }
305
+ end
306
+
307
+ def mime_type_for(filename, encoding = nil)
308
+ if type = MIME::Types.type_for(filename).shift
309
+ if type.ascii? || type.media_type == 'text' || %w( ecmascript javascript ).include?(type.sub_type)
310
+ "#{type.content_type}; charset=#{encoding || @config['encoding']}"
311
+ else
312
+ type.content_type
313
+ end
314
+ else
315
+ 'application/octet-stream'
316
+ end
317
+ end
318
+ end
319
+
320
+ def self.start(config)
321
+ config['url'] = "http://#{config['host']}:#{config['port']}"
322
+ project_dir = config.site.project_dir
323
+
324
+ websocket_server, listener = nil, nil
325
+
326
+ if config['livereload']
327
+ websocket_server = WebSocket::Server.new(host: config['host'], port: config['livereload_port'])
328
+ websocket_server.start
329
+ end
330
+
331
+ server = WEBrick::HTTPServer.new(
332
+ BindAddress: config['host'],
333
+ Port: config['port'],
334
+ Logger: WEBrick::Log.new('/dev/null'),
335
+ AccessLog: [],
336
+ )
337
+
338
+ server.mount('/', Servlet, config)
339
+
340
+ if config['watch'] || config['livereload']
341
+ listener = Listen.to(project_dir) do |modified, added, removed|
342
+ paths = [ *modified, *added, *removed ]
343
+ $log.debug("File changes detected for: #{paths.select { |path| path.start_with?(project_dir) }.map { |path| Util.relative_path(path, project_dir) }.join(', ')}")
344
+ config.reload
345
+ websocket_server.broadcast('reload') if websocket_server
346
+ end
347
+ $log.debug("Watching #{project_dir} for changes")
348
+ listener.start
349
+ end
350
+
351
+ trap("INT") do
352
+ server.shutdown
353
+ end
354
+
355
+ if config['open_url'] && File.executable?('/usr/bin/open')
356
+ page = config.site.links['.']
357
+ system('/usr/bin/open', page ? page.url : config['url'])
358
+ end
359
+
360
+ $log.info("Starting server on #{config['url']}")
361
+ server.start
362
+ $log.info("Server shutting down…")
363
+
364
+ listener.stop if listener
365
+
366
+ if websocket_server
367
+ if config['open_url'] && File.executable?('/usr/bin/open')
368
+ websocket_server.broadcast('close')
369
+ end
370
+
371
+ websocket_server.shutdown
372
+ end
373
+ end
374
+ end
375
+ end
@@ -0,0 +1,115 @@
1
+ require 'logger'
2
+
3
+ $log = Logger.new(STDERR)
4
+ $log.formatter = proc do |severity, datetime, progname, msg|
5
+ "[#{datetime.strftime('%Y-%m-%d %H:%M:%S.%3N')}] [#{Process.pid}] %7s #{msg}\n" % "[#{severity}]"
6
+ end
7
+
8
+ class Profiler
9
+ @@instance = nil
10
+
11
+ def initialize(format = "Program ran in %.3f seconds")
12
+ @current = Entry.new(format)
13
+ end
14
+
15
+ def self.enabled=(flag)
16
+ @@instance.dump if @@instance
17
+ @@instance = flag ? Profiler.new : nil
18
+ end
19
+
20
+ def self.enabled
21
+ @@instance ? true : false
22
+ end
23
+
24
+ def self.run(action, &block)
25
+ if @@instance
26
+ @@instance.profile("#{action} took %.3f seconds", &block)
27
+ else
28
+ block.call
29
+ end
30
+ end
31
+
32
+ def self.group(group, &block)
33
+ if @@instance
34
+ @@instance.profile_group(group, &block)
35
+ else
36
+ block.call
37
+ end
38
+ end
39
+
40
+ def profile(format)
41
+ parent = @current
42
+ parent.add_child(@current = Entry.new(format))
43
+ res = yield
44
+ @current.finished!
45
+ @current = parent
46
+ res
47
+ end
48
+
49
+ def profile_group(group)
50
+ @current.group(group) do
51
+ yield
52
+ end
53
+ end
54
+
55
+ def dump
56
+ @current.dump
57
+ end
58
+
59
+ class Entry
60
+ attr_reader :duration
61
+
62
+ def initialize(format)
63
+ @format = format
64
+ @start = Time.now
65
+ end
66
+
67
+ def group(name)
68
+ @groups ||= {}
69
+ @groups[name] ||= { :duration => 0, :count => 0 }
70
+
71
+ previous_group, @current_group = @current_group, name
72
+
73
+ start = Time.now
74
+ res = yield
75
+ @groups[name][:duration] += Time.now - start
76
+ @groups[name][:count] += 1
77
+
78
+ @groups[previous_group][:duration] -= Time.now - start if previous_group
79
+ @current_group = previous_group
80
+
81
+ res
82
+ end
83
+
84
+ def add_child(child)
85
+ @children ||= []
86
+ @children << child
87
+ end
88
+
89
+ def finished!
90
+ @duration ||= Time.now - @start
91
+ end
92
+
93
+ def indent(level)
94
+ ' ' * level
95
+ end
96
+
97
+ def dump(level = 0)
98
+ self.finished!
99
+
100
+ STDERR.puts indent(level) + (@format % @duration)
101
+
102
+ if @groups
103
+ @groups.sort_by { |group, info| info[:duration] }.reverse.each do |group, info|
104
+ STDERR.puts indent(level+1) + "[#{group}: %.3f seconds, called #{info[:count]} time(s), %.3f seconds/time]" % [ info[:duration], info[:duration] / info[:count] ]
105
+ end
106
+ end
107
+
108
+ if @children
109
+ @children.sort_by { |child| child.duration }.reverse.each do |child|
110
+ child.dump(level + 1)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Glim
2
+ VERSION = '0.1'
3
+ end
metadata ADDED
@@ -0,0 +1,178 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: glim
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Allan Odgaard
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-09-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mercenary
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: liquid
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: kramdown
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.14'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.14'
55
+ - !ruby/object:Gem::Dependency
56
+ name: listen
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: websocket
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.2'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: mime-types
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.2'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.2'
97
+ - !ruby/object:Gem::Dependency
98
+ name: glim-sass-converter
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.1'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.1'
111
+ - !ruby/object:Gem::Dependency
112
+ name: glim-seo-tag
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.1'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.1'
125
+ - !ruby/object:Gem::Dependency
126
+ name: glim-feed
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.1'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.1'
139
+ description:
140
+ email:
141
+ executables:
142
+ - glim
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - bin/glim
147
+ - lib/cache.rb
148
+ - lib/commands.rb
149
+ - lib/exception.rb
150
+ - lib/liquid_ext.rb
151
+ - lib/local_server.rb
152
+ - lib/log_and_profile.rb
153
+ - lib/version.rb
154
+ homepage: https://macromates.com/glim/
155
+ licenses:
156
+ - MIT
157
+ metadata: {}
158
+ post_install_message:
159
+ rdoc_options: []
160
+ require_paths:
161
+ - lib
162
+ required_ruby_version: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ required_rubygems_version: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: '0'
172
+ requirements: []
173
+ rubyforge_project:
174
+ rubygems_version: 2.7.6
175
+ signing_key:
176
+ specification_version: 4
177
+ summary: Static site generator inspired by Jekyll but a lot faster
178
+ test_files: []