archival 0.0.2 → 0.0.7

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.
@@ -3,43 +3,74 @@
3
3
  require 'liquid'
4
4
  require 'tomlrb'
5
5
  require 'tags/layout'
6
+ require 'redcarpet'
6
7
 
7
8
  Liquid::Template.error_mode = :strict
8
9
  Liquid::Template.register_tag('layout', Layout)
9
10
 
10
11
  module Archival
12
+ class DuplicateKeyError < StandardError
13
+ end
14
+
11
15
  class Builder
12
16
  attr_reader :page_templates
13
17
 
14
18
  def initialize(config, *_args)
15
- @config = Config.new(config)
19
+ @config = config
20
+ @markdown = Redcarpet::Markdown.new(
21
+ Redcarpet::Render::HTML.new(prettify: true,
22
+ hard_wrap: true), no_intra_emphasis: true,
23
+ fenced_code_blocks: true,
24
+ autolink: true,
25
+ strikethrough: true,
26
+ underline: true
27
+ )
16
28
  refresh_config
17
29
  end
18
30
 
19
31
  def refresh_config
20
32
  @file_system = Liquid::LocalFileSystem.new(
21
- @config.root, '%s.liquid'
33
+ File.join(@config.root, @config.pages_dir), '%s.liquid'
22
34
  )
23
35
  @variables = {}
24
36
  @object_types = {}
25
37
  @page_templates = {}
38
+ @dynamic_pages = Set.new
39
+ @dynamic_templates = {}
26
40
 
27
- Liquid::Template.file_system = @file_system
41
+ Liquid::Template.file_system = Liquid::LocalFileSystem.new(
42
+ File.join(@config.root, @config.pages_dir), '_%s.liquid'
43
+ )
28
44
 
29
45
  objects_definition_file = File.join(@config.root,
30
46
  'objects.toml')
31
47
  if File.file? objects_definition_file
32
- @object_types = read_toml(objects_definition_file)
48
+ @object_types = Tomlrb.load_file(objects_definition_file)
33
49
  end
34
50
 
35
- update_pages
36
51
  update_objects
52
+ update_pages
53
+ end
54
+
55
+ def full_rebuild
56
+ Layout.reset_cache
57
+ refresh_config
37
58
  end
38
59
 
39
60
  def update_pages
40
61
  do_update_pages(File.join(@config.root, @config.pages_dir))
41
62
  end
42
63
 
64
+ def dynamic?(file)
65
+ @dynamic_pages.include? File.basename(file, '.liquid')
66
+ end
67
+
68
+ def template_for_page(template_file)
69
+ content = @file_system.read_template_file(template_file)
70
+ content += dev_mode_content if @config.dev_mode
71
+ Liquid::Template.parse(content)
72
+ end
73
+
43
74
  def do_update_pages(dir, prefix = nil)
44
75
  add_prefix = lambda { |entry|
45
76
  prefix ? File.join(prefix, entry) : entry
@@ -53,16 +84,13 @@ module Archival
53
84
  add_prefix(entry))
54
85
  end
55
86
  elsif File.file? File.join(dir, entry)
56
- if entry.end_with?('.liquid') && !(entry.start_with? '_')
57
- page_name = File.basename(entry,
58
- '.liquid')
59
- template_file = File.join(
60
- @config.pages_dir,
61
- add_prefix.call(page_name)
62
- )
63
- content = @file_system.read_template_file(template_file)
64
- @page_templates[add_prefix.call(page_name)] =
65
- Liquid::Template.parse(content)
87
+ page_name = File.basename(entry, '.liquid')
88
+ template_file = add_prefix.call(page_name)
89
+ if dynamic? entry
90
+ @dynamic_templates[template_file] = template_for_page(template_file)
91
+ elsif entry.end_with?('.liquid') && !(entry.start_with? '_')
92
+ @page_templates[template_file] =
93
+ template_for_page(template_file)
66
94
  end
67
95
  end
68
96
  end
@@ -75,30 +103,53 @@ module Archival
75
103
 
76
104
  def do_update_objects(dir)
77
105
  objects = {}
78
- @object_types.each do |name, _definition|
79
- objects[name] = []
106
+ @object_types.each do |name, definition|
107
+ objects[name] = {}
80
108
  obj_dir = File.join(dir, name)
81
109
  if File.directory? obj_dir
82
110
  Dir.foreach(obj_dir) do |file|
83
111
  if file.end_with? '.toml'
84
- object = read_toml(File.join(
85
- obj_dir, file
86
- ))
112
+ object = Tomlrb.load_file(File.join(
113
+ obj_dir, file
114
+ ))
87
115
  object[:name] =
88
116
  File.basename(file, '.toml')
89
- objects[name].push object
117
+ objects[name][object[:name]] = parse_object(object, definition)
118
+ if definition.key? 'template'
119
+ @dynamic_pages << definition['template']
120
+ end
90
121
  end
91
122
  end
92
123
  end
93
- objects[name] = objects[name].sort do |a, b|
94
- (a['order'] || a[:name]).to_s <=> (b['order'] || b[:name]).to_s
95
- end
124
+ objects[name] = sort_objects(objects[name])
96
125
  end
97
126
  @variables['objects'] = objects
98
127
  end
99
128
 
100
- def read_toml(file_path)
101
- Tomlrb.load_file(file_path)
129
+ def sort_objects(objects)
130
+ # Sort by either 'order' key or object name, depending on what is
131
+ # available.
132
+ sorted_by_keys = objects.sort_by do |name, obj|
133
+ obj.key?('order') ? obj['order'].to_s : name
134
+ end
135
+ sorted_objects = Archival::TemplateArray.new
136
+ sorted_by_keys.each do |d|
137
+ raise DuplicateKeyError if sorted_objects.key?(d[0])
138
+
139
+ sorted_objects.push(d[1])
140
+ sorted_objects[d[0]] = d[1]
141
+ end
142
+ sorted_objects
143
+ end
144
+
145
+ def parse_object(object, definition)
146
+ definition.each do |name, type|
147
+ case type
148
+ when 'markdown'
149
+ object[name] = @markdown.render(object[name]) if object[name]
150
+ end
151
+ end
152
+ object
102
153
  end
103
154
 
104
155
  def set_var(name, value)
@@ -110,18 +161,48 @@ module Archival
110
161
  template.render(@variables)
111
162
  end
112
163
 
164
+ def render_dynamic(page, obj)
165
+ template = @dynamic_templates[page]
166
+ template.render(@variables.merge({ page => obj }))
167
+ end
168
+
113
169
  def write_all
114
170
  Dir.mkdir(@config.build_dir) unless File.exist? @config.build_dir
115
171
  @page_templates.each_key do |template|
116
172
  out_dir = File.join(@config.build_dir,
117
173
  File.dirname(template))
118
174
  Dir.mkdir(out_dir) unless File.exist? out_dir
119
- out_path = File.join(@config.build_dir,
175
+ out_path = File.join(out_dir,
120
176
  "#{template}.html")
121
177
  File.open(out_path, 'w+') do |file|
122
178
  file.write(render(template))
123
179
  end
124
180
  end
181
+ @dynamic_pages.each do |page|
182
+ out_dir = File.join(@config.build_dir, page)
183
+ Dir.mkdir(out_dir) unless File.exist? out_dir
184
+ objects = @variables['objects'][page]
185
+ objects.each do |obj|
186
+ out_path = File.join(out_dir, "#{obj[:name]}.html")
187
+ obj['path'] = out_path
188
+ File.open(out_path, 'w+') do |file|
189
+ file.write(render_dynamic(page, obj))
190
+ end
191
+ end
192
+ end
193
+ return if @config.dev_mode
194
+
195
+ # in production, also copy all assets to the dist folder.
196
+ @config.assets_dirs.each do |asset_dir|
197
+ FileUtils.copy_entry File.join(@config.root, asset_dir),
198
+ File.join(@config.build_dir, asset_dir)
199
+ end
200
+ end
201
+
202
+ private
203
+
204
+ def dev_mode_content
205
+ "<script src=\"http://localhost:#{@config.helper_port}/js/archival-helper.js\" type=\"application/javascript\"></script>" # rubocop:disable Layout/LineLength
125
206
  end
126
207
  end
127
208
  end
@@ -1,16 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'tomlrb'
4
+
3
5
  module Archival
4
6
  class Config
5
- attr_reader :pages_dir, :objects_dir, :root, :build_dir
7
+ attr_reader :pages_dir, :objects_dir, :assets_dirs, :root, :build_dir,
8
+ :helper_port, :dev_mode
6
9
 
7
- def initialize(config)
8
- @pages_dir = config['pages'] || 'pages'
9
- @objects_dir = config['objects'] || 'objects'
10
+ def initialize(config = {})
10
11
  @root = config['root'] || Dir.pwd
11
- @build_dir = config['build_dir'] || File.join(
12
+ manifest = load_manifest
13
+ @pages_dir = config['pages'] || manifest['pages'] || 'pages'
14
+ @objects_dir = config['objects'] || manifest['objects'] || 'objects'
15
+ @build_dir = config['build_dir'] || manifest['build_dir'] || File.join(
12
16
  @root, 'dist'
13
17
  )
18
+ @helper_port = config['helper_port'] || manifest['helper_port'] || 2701
19
+ @assets_dirs = config['assets_dirs'] || manifest['assets'] || []
20
+ @dev_mode = config[:dev_mode] || false
21
+ end
22
+
23
+ def load_manifest
24
+ manifest_file = File.join(@root, 'manifest.toml')
25
+ return Tomlrb.load_file(manifest_file) if File.file? manifest_file
26
+
27
+ {}
14
28
  end
15
29
  end
16
30
  end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'open-uri'
5
+
6
+ module Archival
7
+ class HelperServer
8
+ attr_reader :page
9
+
10
+ def initialize(port, build_dir)
11
+ @port = port
12
+ @build_dir = build_dir
13
+ @helper_dir = File.expand_path(File.join(File.dirname(__FILE__),
14
+ '../../helper'))
15
+ end
16
+
17
+ def start
18
+ server = TCPServer.new @port
19
+ loop do
20
+ Thread.start(server.accept) do |client|
21
+ req = ''
22
+ method = nil
23
+ path = nil
24
+ while (line = client.gets) && (line != "\r\n")
25
+ unless method
26
+ req_info = line.split
27
+ method = req_info[0]
28
+ path = req_info[1]
29
+ end
30
+ req += line
31
+ end
32
+ client.close unless req
33
+ handle_request(client, req, method, path)
34
+ end
35
+ end
36
+ end
37
+
38
+ def refresh_client
39
+ ws_sendmessage('refresh')
40
+ end
41
+
42
+ private
43
+
44
+ MAGIC_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
45
+
46
+ def handle_request(client, req, method, path)
47
+ if method == 'GET' && path.start_with?('/js/')
48
+ # For static paths, just serve the files they refer to.
49
+ http_response(client, type: 'application/javascript') do
50
+ serve_static(client, path)
51
+ end
52
+ client.close
53
+ elsif (matches = req.match(/^Sec-WebSocket-Key: (\S+)/))
54
+ websocket_key = matches[1]
55
+ # puts "Websocket handshake detected with key: #{websocket_key}"
56
+ connect_socket(client, websocket_key)
57
+ else
58
+ client.close
59
+ end
60
+ end
61
+
62
+ def connect_socket(client, websocket_key)
63
+ @socket = client
64
+ response_key = Digest::SHA1.base64digest([websocket_key,
65
+ MAGIC_GUID].join)
66
+ # puts "Responding to handshake with key: #{response_key}"
67
+
68
+ @socket.write "HTTP/1.1 101 Switching Protocols\r\n"
69
+ @socket.write "Upgrade: websocket\r\n"
70
+ @socket.write "Connection: Upgrade\r\n"
71
+ @socket.write "Sec-WebSocket-Accept: #{response_key}\r\n"
72
+ @socket.write "\r\n"
73
+
74
+ # puts 'Handshake completed.'
75
+ ws_loop
76
+ end
77
+
78
+ def ws_loop
79
+ loop do
80
+ msg = ws_getmessage
81
+ next unless msg
82
+
83
+ if msg == 'connected'
84
+ ws_sendmessage('ready')
85
+ elsif msg.start_with?('page:')
86
+ page_path = Pathname.new(msg.sub(/^page:/, ''))
87
+ @page = page_path.relative_path_from(@build_dir)
88
+ ws_sendmessage('ok')
89
+ end
90
+ end
91
+ end
92
+
93
+ def validate_ws_message
94
+ first_byte = @socket.getbyte
95
+ return unless first_byte
96
+
97
+ fin = first_byte & 0b10000000
98
+ opcode = first_byte & 0b00001111
99
+
100
+ # Our server only supports single-frame, text messages.
101
+ # Raise an exception if the client tries to send anything else.
102
+ raise "We don't support continuations" unless fin
103
+ raise 'We only support opcode 1' unless opcode == 1
104
+
105
+ second_byte = @socket.getbyte
106
+ is_masked = second_byte & 0b10000000
107
+ payload_size = second_byte & 0b01111111
108
+
109
+ raise 'frame masked incorrectly' unless is_masked
110
+ raise 'payload must be < 126 bytes in length' unless payload_size < 126
111
+
112
+ payload_size
113
+ end
114
+
115
+ def ws_getmessage
116
+ payload_size = validate_ws_message
117
+ return unless payload_size
118
+
119
+ # warn "Payload size: #{payload_size} bytes"
120
+
121
+ mask = 4.times.map { @socket.getbyte }
122
+ # warn "Got mask: #{mask.inspect}"
123
+
124
+ data = payload_size.times.map { @socket.getbyte }
125
+ # warn "Got masked data: #{data.inspect}"
126
+
127
+ unmasked_data = data.each_with_index.map do |byte, i|
128
+ byte ^ mask[i % 4]
129
+ end
130
+ # warn "Unmasked the data: #{unmasked_data.inspect}"
131
+
132
+ unmasked_data.pack('C*').force_encoding('utf-8')
133
+ end
134
+
135
+ def ws_sendmessage(message)
136
+ return unless @socket
137
+
138
+ output = [0b10000001, message.size, message]
139
+ @socket.write output.pack("CCA#{message.size}")
140
+ end
141
+
142
+ def serve_static(client, path)
143
+ buffer = File.open(File.join(@helper_dir, path)).read
144
+ buffer.sub! '$PORT', @port.to_s
145
+ client.print buffer
146
+ end
147
+
148
+ def http_response(client, config)
149
+ status = config[:status] ||= 200
150
+ type = config[:type] ||= 'text/html'
151
+ client.print "HTTP/1.1 #{status}\r\n"
152
+ client.print "Content-Type: #{type}\r\n"
153
+ client.print "\r\n"
154
+ yield
155
+ end
156
+ end
157
+ end
@@ -4,40 +4,83 @@ require 'listen'
4
4
  require 'pathname'
5
5
 
6
6
  module Archival
7
- def self.child?(parent, child)
8
- path = Pathname.new(child)
9
- return true if path.fnmatch?(File.join(parent, '**'))
10
-
11
- false
7
+ def listen(config = {})
8
+ @config = Config.new(config.merge(dev_mode: true))
9
+ builder = Builder.new(@config)
10
+ Logger.benchmark('built') do
11
+ builder.write_all
12
+ end
13
+ ignore = %r{/dist/}
14
+ listener = Listen.to(@config.root,
15
+ ignore: ignore) do |modified, added, removed|
16
+ updated_pages = []
17
+ updated_objects = []
18
+ updated_assets = []
19
+ (modified + added + removed).each do |file|
20
+ case change_type(file)
21
+ when :pages
22
+ updated_pages << file
23
+ when :objects
24
+ updated_objects << file
25
+ when :assets
26
+ updated_assets << file
27
+ end
28
+ end
29
+ @server.refresh_client if rebuild?(builder, updated_objects,
30
+ updated_pages, updated_assets)
31
+ end
32
+ listener.start
33
+ serve_helpers
12
34
  end
13
35
 
14
- def self.process_change?(file, builder)
15
- if child?(File.join(@config.root, @config.pages_dir), file)
36
+ module_function :listen
37
+
38
+ class << self
39
+ private
40
+
41
+ def child?(parent, child)
42
+ path = Pathname.new(child)
43
+ return true if path.fnmatch?(File.join(parent, '**'))
44
+
45
+ false
46
+ end
47
+
48
+ def change_type(file)
16
49
  # a page was modified, rebuild the pages.
17
- builder.update_pages
18
- return true
19
- elsif child?(File.join(@config.root, @config.objects_dir), file)
50
+ return :pages if child?(File.join(@config.root, @config.pages_dir),
51
+ file)
20
52
  # an object was modified, rebuild the objects.
21
- builder.update_objects
22
- return true
53
+ return :objects if child?(File.join(@config.root, @config.objects_dir),
54
+ file)
55
+
56
+ # layout and other assets. For now, this is everything.
57
+ @config.assets_dirs.each do |dir|
58
+ return :assets if child?(File.join(@config.root, dir), file)
59
+ end
60
+ return :assets if child?(File.join(@config.root, 'layout'), file)
61
+ return :assets if ['manifest.toml',
62
+ 'objects.toml'].include? File.basename(file)
63
+
64
+ :none
23
65
  end
24
- false
25
- end
26
66
 
27
- def listen(config)
28
- @config = Config.new(config)
29
- builder = Builder.new(config)
30
- builder.write_all
31
- listener = Listen.to(@config.root) do |modified, added, removed|
32
- needs_update = false
33
- (modified + added + removed).each do |file|
34
- needs_update = true if process_change?(file, builder)
67
+ def rebuild?(builder, updated_objects, updated_pages, updated_assets)
68
+ if updated_pages.empty? && updated_objects.empty? && updated_assets.empty?
69
+ return false
70
+ end
71
+
72
+ Logger.benchmark('rebuilt') do
73
+ builder.update_objects if updated_objects.length
74
+ builder.update_pages if updated_pages.length
75
+ builder.full_rebuild if updated_assets.length
76
+ builder.write_all
35
77
  end
36
- builder.write_all if needs_update
78
+ true
37
79
  end
38
- listener.start
39
- listener
40
- end
41
80
 
42
- module_function :listen
81
+ def serve_helpers
82
+ @server = HelperServer.new(@config.helper_port, @config.build_dir)
83
+ @server.start
84
+ end
85
+ end
43
86
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark'
4
+
5
+ module Archival
6
+ class Logger
7
+ def self.benchmark(message, &block)
8
+ Benchmark.bm do |bm|
9
+ bm.report(message, &block)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -27,19 +27,15 @@ class RakeTasks
27
27
  build_dir = Dir.pwd
28
28
 
29
29
  task 'build' do
30
- builder = Archival::Builder.new('root' => build_dir)
31
- builder.write_all
30
+ Archival::Logger.benchmark('built') do
31
+ config = Archival::Config.new('root' => build_dir)
32
+ builder = Archival::Builder.new(config)
33
+ builder.write_all
34
+ end
32
35
  end
33
36
 
34
37
  task 'run' do
35
- Archival.listen(build_dir)
36
- begin
37
- sleep
38
- rescue Interrupt
39
- # Don't print a stack when a user interrupts, as this is the right way
40
- # to stop the development server.
41
- puts ''
42
- end
38
+ Archival.listen('root' => build_dir)
43
39
  end
44
40
 
45
41
  RakeTasks.instance = self
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Archival
4
+ class TemplateArray < Array
5
+ alias subscript_access []
6
+ alias subscript_write []=
7
+
8
+ def initialize(*args)
9
+ super(*args)
10
+ @data = {}
11
+ end
12
+
13
+ def [](*args)
14
+ key = args[0]
15
+ return @data[key] if key.is_a? String
16
+ return @data[key] if key.is_a? Symbol
17
+
18
+ subscript_access(*args)
19
+ end
20
+
21
+ def []=(*args)
22
+ key = args[0]
23
+ if key.is_a?(String) || key.is_a?(Symbol)
24
+ @data[key] = args[1]
25
+ return
26
+ end
27
+ subscript_write(*args)
28
+ end
29
+
30
+ def key?(key)
31
+ @data.key?(key)
32
+ end
33
+ end
34
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Archival
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.7'
5
5
  end
data/lib/archival.rb CHANGED
@@ -5,6 +5,9 @@ module Archival
5
5
  end
6
6
 
7
7
  require 'archival/version'
8
+ require 'archival/template_array'
9
+ require 'archival/logger'
8
10
  require 'archival/config'
11
+ require 'archival/helper_server'
9
12
  require 'archival/builder'
10
13
  require 'archival/listen'
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archival",
3
- "version": "0.0.2",
3
+ "version": "0.0.7",
4
4
  "description": "An incredibly simple CMS for durable websites",
5
5
  "bin": "build.rb",
6
6
  "directories": {
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: archival
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jesse Ditson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-11-05 00:00:00.000000000 Z
11
+ date: 2021-11-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: liquid
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: 3.7.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: redcarpet
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 3.5.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 3.5.1
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: tomlrb
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -76,17 +90,22 @@ files:
76
90
  - bin/ldiff
77
91
  - bin/listen
78
92
  - bin/rake
93
+ - bin/redcarpet
79
94
  - bin/rspec
80
95
  - bin/rubocop
81
96
  - bin/ruby-parse
82
97
  - bin/ruby-rewrite
83
98
  - bin/setup
84
99
  - exe/archival
100
+ - helper/js/archival-helper.js
85
101
  - lib/archival.rb
86
102
  - lib/archival/builder.rb
87
103
  - lib/archival/config.rb
104
+ - lib/archival/helper_server.rb
88
105
  - lib/archival/listen.rb
106
+ - lib/archival/logger.rb
89
107
  - lib/archival/rake_tasks.rb
108
+ - lib/archival/template_array.rb
90
109
  - lib/archival/version.rb
91
110
  - lib/tags/layout.rb
92
111
  - package.json