glim 0.1.1

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,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: []