archival 0.0.5 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c29406fcec39c52b6876b805dba5509d6f43da5805944a344b2c771214cd8ed
4
- data.tar.gz: 7319f9d6dace6b20c57e318d4df1c4dc80c1bd67673cee1b37d9a17ba35f823b
3
+ metadata.gz: dd9c755847bfaabb7c6dee0882426534c44c7130a8cbfe84a45b755bca91088a
4
+ data.tar.gz: 3bbd4cb0b322db5eff11b85d9afc9f76e4cdbe1bc166c530f015583369c7f141
5
5
  SHA512:
6
- metadata.gz: e05e6573f3e580d3a364826b7fd604559f2513e609a5436ba4170d606112d913e91d3cc0ec5498f4e0c418212b85068d7d859829030ee8b113a8909b04020e64
7
- data.tar.gz: 8c227320c6d961595b5ce545559df4cdddf42e1d27819c1e5013b1e8189932bb50b4047e47bc5f85c8af1b0109a9b582063e487fb926132248d766b19fc98498
6
+ metadata.gz: db20449d88ba3394580bd5efebd259a7479bd423fea6983a4790e21d124d831490f4e9f4bea3fe97411cc2b72898e93534be2b34d122e6c93e47290381cd74b3
7
+ data.tar.gz: f25b29c4fdb0b676dcee6156ab571877764ead475cac70944c6e81a1f5c445b3ebaf07b7266087b3163209184cf2c9dc50f33797105b8abb3a8e2564a19861c2
data/.rubocop.yml CHANGED
@@ -30,7 +30,7 @@ Metrics/BlockLength:
30
30
  Max: 150
31
31
 
32
32
  Metrics/ClassLength:
33
- Max: 150
33
+ Max: 250
34
34
 
35
35
  Metrics/CyclomaticComplexity:
36
36
  Max: 10
data/Gemfile CHANGED
@@ -7,4 +7,4 @@ gemspec
7
7
 
8
8
  gem 'rake', '~> 13.0'
9
9
  gem 'rspec', '~> 3.0'
10
- gem 'rubocop', '~> 1.22', require: false
10
+ gem 'rubocop', '~> 1.23', require: false
data/archival.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'archival'
5
- s.version = '0.0.5'
5
+ s.version = '0.0.9'
6
6
  s.summary = 'An incredibly simple CMS for durable websites'
7
7
  s.description = 'https://jesseditson.com/the-simplest-cms-part-1'
8
8
  s.authors = ['Jesse Ditson']
@@ -26,4 +26,8 @@ Gem::Specification.new do |s|
26
26
  s.add_dependency 'listen', '~> 3.7.0'
27
27
  s.add_dependency 'redcarpet', '~> 3.5.1'
28
28
  s.add_dependency 'tomlrb', '~> 2.0.1'
29
+
30
+ s.metadata = {
31
+ 'rubygems_mfa_required' => 'true'
32
+ }
29
33
  end
data/bin/bundle CHANGED
@@ -73,7 +73,7 @@ m = Module.new do
73
73
 
74
74
  requirement = bundler_gem_version.approximate_recommendation
75
75
 
76
- return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0")
76
+ return requirement unless Gem.rubygems_version < Gem::Version.new("2.7.0")
77
77
 
78
78
  requirement += ".a" if bundler_gem_version.prerelease?
79
79
 
@@ -2,49 +2,51 @@
2
2
 
3
3
  require 'liquid'
4
4
  require 'tomlrb'
5
- require 'tags/layout'
6
5
  require 'redcarpet'
7
-
8
- Liquid::Template.error_mode = :strict
9
- Liquid::Template.register_tag('layout', Layout)
6
+ require 'fileutils'
7
+ require 'tags/asset'
10
8
 
11
9
  module Archival
10
+ class DuplicateKeyError < StandardError
11
+ end
12
+
13
+ class DuplicateStaticFileError < StandardError
14
+ end
15
+
12
16
  class Builder
13
17
  attr_reader :page_templates
14
18
 
15
19
  def initialize(config, *_args)
16
20
  @config = config
17
- @markdown = Redcarpet::Markdown.new(
18
- Redcarpet::Render::HTML.new(prettify: true,
19
- hard_wrap: true), no_intra_emphasis: true,
20
- fenced_code_blocks: true,
21
- autolink: true,
22
- strikethrough: true,
23
- underline: true
24
- )
25
21
  refresh_config
22
+ Asset.helper_port = @config.helper_port
23
+ end
24
+
25
+ def pages_dir
26
+ File.join(@config.root, @config.pages_dir)
27
+ end
28
+
29
+ def objects_dir
30
+ File.join(@config.root, @config.objects_dir)
26
31
  end
27
32
 
28
33
  def refresh_config
29
34
  @file_system = Liquid::LocalFileSystem.new(
30
- File.join(@config.root, @config.pages_dir), '%s.liquid'
35
+ pages_dir, '%s.liquid'
31
36
  )
32
- @variables = {}
33
37
  @object_types = {}
34
38
  @page_templates = {}
39
+ @dynamic_types = Set.new
40
+ @dynamic_templates = {}
41
+ @parser = Archival::Parser.new(pages_dir)
35
42
 
36
43
  Liquid::Template.file_system = Liquid::LocalFileSystem.new(
37
- File.join(@config.root, @config.pages_dir), '_%s.liquid'
44
+ pages_dir, '_%s.liquid'
38
45
  )
39
46
 
40
- objects_definition_file = File.join(@config.root,
41
- 'objects.toml')
42
- if File.file? objects_definition_file
43
- @object_types = Tomlrb.load_file(objects_definition_file)
44
- end
47
+ @objects_definition_file = File.join(@config.root, 'objects.toml')
45
48
 
46
49
  update_pages
47
- update_objects
48
50
  end
49
51
 
50
52
  def full_rebuild
@@ -52,8 +54,46 @@ module Archival
52
54
  refresh_config
53
55
  end
54
56
 
55
- def update_pages
56
- do_update_pages(File.join(@config.root, @config.pages_dir))
57
+ def update_objects(_updated_objects = nil)
58
+ @object_types = {}
59
+ if File.file? @objects_definition_file
60
+ @object_types = Tomlrb.load_file(@objects_definition_file)
61
+ end
62
+ @dynamic_types = Set.new
63
+ @object_types.each do |_name, definition|
64
+ is_template = definition.key? 'template'
65
+ @dynamic_types << definition['template'] if is_template
66
+ end
67
+ # TODO: remove deleted dynamic pages
68
+ end
69
+
70
+ def update_pages(_updated_pages = nil, _updated_objects = nil)
71
+ update_objects
72
+ # TODO: remove deleted pages
73
+ do_update_pages(pages_dir)
74
+ end
75
+
76
+ def update_assets(changes)
77
+ changes.each do |change|
78
+ asset_path = File.join(@config.build_dir, change.path)
79
+ case change.type
80
+ when :removed
81
+ FileUtils.rm_rf asset_path
82
+ else
83
+ puts change.path
84
+ FileUtils.copy_entry File.join(@config.root, change.path), asset_path
85
+ end
86
+ end
87
+ end
88
+
89
+ def dynamic?(file)
90
+ @dynamic_types.include? File.basename(file, '.liquid')
91
+ end
92
+
93
+ def template_for_page(template_file)
94
+ content = @file_system.read_template_file(template_file)
95
+ content += dev_mode_content if @config.dev_mode
96
+ Liquid::Template.parse(content)
57
97
  end
58
98
 
59
99
  def do_update_pages(dir, prefix = nil)
@@ -69,77 +109,96 @@ module Archival
69
109
  add_prefix(entry))
70
110
  end
71
111
  elsif File.file? File.join(dir, entry)
72
- if entry.end_with?('.liquid') && !(entry.start_with? '_')
73
- page_name = File.basename(entry,
74
- '.liquid')
75
- template_file = add_prefix.call(page_name)
76
- content = @file_system.read_template_file(template_file)
77
- content += dev_mode_content if @config.dev_mode
78
- @page_templates[add_prefix.call(page_name)] =
79
- Liquid::Template.parse(content)
112
+ page_name = File.basename(entry, '.liquid')
113
+ template_file = add_prefix.call(page_name)
114
+ if dynamic? entry
115
+ @dynamic_templates[template_file] = template_for_page(template_file)
116
+ elsif entry.end_with?('.liquid') && !(entry.start_with? '_')
117
+ @page_templates[template_file] =
118
+ template_for_page(template_file)
80
119
  end
81
120
  end
82
121
  end
83
122
  end
84
123
 
85
- def update_objects
86
- do_update_objects(File.join(@config.root,
87
- @config.objects_dir))
124
+ def read_objects(type)
125
+ obj_dir = File.join(objects_dir, type)
126
+ return unless File.directory? obj_dir
127
+
128
+ Dir.foreach(obj_dir) do |file|
129
+ if file.end_with? '.toml'
130
+ object = Tomlrb.load_file(File.join(
131
+ obj_dir, file
132
+ ))
133
+ object[:name] =
134
+ File.basename(file, '.toml')
135
+ yield object[:name], object
136
+ end
137
+ end
138
+ end
139
+
140
+ def path_for_template(name, type)
141
+ Pathname.new(File.join(pages_dir, type, "#{name}.html"))
88
142
  end
89
143
 
90
- def do_update_objects(dir)
144
+ def objects_for_template(template_path)
91
145
  objects = {}
92
- @object_types.each do |name, definition|
93
- objects[name] = {}
94
- obj_dir = File.join(dir, name)
95
- if File.directory? obj_dir
96
- Dir.foreach(obj_dir) do |file|
97
- if file.end_with? '.toml'
98
- object = Tomlrb.load_file(File.join(
99
- obj_dir, file
100
- ))
101
- object[:name] =
102
- File.basename(file, '.toml')
103
- objects[name][object[:name]] = parse_object(object, definition)
104
- end
146
+ @object_types.each do |type, definition|
147
+ objects[type] = {}
148
+ is_dynamic = @dynamic_types.include? type
149
+ read_objects type do |name, object|
150
+ objects[type][name] = @parser.parse_object(
151
+ object, definition, template_path
152
+ )
153
+ if is_dynamic
154
+ path = path_for_template(name, type)
155
+ objects[type][name]['path'] =
156
+ path.relative_path_from(File.dirname(template_path)).to_s
105
157
  end
106
158
  end
107
- objects[name] = sort_objects(objects[name])
159
+ objects[type] = sort_objects(objects[type])
108
160
  end
109
- @variables['objects'] = objects
161
+ objects
110
162
  end
111
163
 
112
164
  def sort_objects(objects)
113
- # Since objects are hashes but we'd like them to be iterable based on
114
- # arbitrary "order" keys, and in ruby hashes enumerate in insert order,
115
- # we just need to re-insert in the correct order.
116
- sorted_keys = objects.sort_by do |name, obj|
117
- obj['order'].to_s || name
165
+ # Sort by either 'order' key or object name, depending on what is
166
+ # available.
167
+ sorted_by_keys = objects.sort_by do |name, obj|
168
+ obj.key?('order') ? obj['order'].to_s : name
118
169
  end
119
- sorted_objects = {}
120
- sorted_keys.each do |d|
170
+ sorted_objects = Archival::TemplateArray.new
171
+ sorted_by_keys.each do |d|
172
+ raise DuplicateKeyError if sorted_objects.key?(d[0])
173
+
174
+ sorted_objects.push(d[1])
121
175
  sorted_objects[d[0]] = d[1]
122
176
  end
123
177
  sorted_objects
124
178
  end
125
179
 
126
- def parse_object(object, definition)
127
- definition.each do |name, type|
128
- case type
129
- when 'markdown'
130
- object[name] = @markdown.render(object[name]) if object[name]
131
- end
132
- end
133
- object
134
- end
135
-
136
- def set_var(name, value)
137
- @variables[name] = value
138
- end
139
-
140
180
  def render(page)
181
+ dir = File.join(pages_dir, File.dirname(page))
141
182
  template = @page_templates[page]
142
- template.render(@variables)
183
+ template_path = File.join(dir, page)
184
+ parsed_objects = objects_for_template(template_path)
185
+ template.render('objects' => parsed_objects,
186
+ 'template_path' => template_path)
187
+ end
188
+
189
+ def render_dynamic(type, name)
190
+ dir = File.join(pages_dir, type)
191
+ template = @dynamic_templates[type]
192
+ template_path = File.join(dir, name)
193
+ parsed_objects = objects_for_template(template_path)
194
+ obj = parsed_objects[type][name]
195
+ vars = {}
196
+ .merge(
197
+ 'objects' => parsed_objects,
198
+ 'template_path' => template_path
199
+ )
200
+ .merge({ type => obj })
201
+ template.render(vars)
143
202
  end
144
203
 
145
204
  def write_all
@@ -148,23 +207,60 @@ module Archival
148
207
  out_dir = File.join(@config.build_dir,
149
208
  File.dirname(template))
150
209
  Dir.mkdir(out_dir) unless File.exist? out_dir
151
- out_path = File.join(@config.build_dir,
152
- "#{template}.html")
210
+ out_path = File.join(out_dir, "#{template}.html")
153
211
  File.open(out_path, 'w+') do |file|
154
212
  file.write(render(template))
155
213
  end
156
214
  end
157
- return if @config.dev_mode
215
+ @dynamic_types.each do |type|
216
+ out_dir = File.join(@config.build_dir, type)
217
+ Dir.mkdir(out_dir) unless File.exist? out_dir
218
+ read_objects(type) do |name|
219
+ out_path = File.join(out_dir, "#{name}.html")
220
+ File.open(out_path, 'w+') do |file|
221
+ file.write(render_dynamic(type, name))
222
+ end
223
+ end
224
+ end
158
225
 
159
- # in production, also copy all assets to the dist folder.
226
+ # in production (or init), also copy all assets to the dist folder.
227
+ # in dev, they will be copied as they change.
160
228
  @config.assets_dirs.each do |asset_dir|
161
- FileUtils.copy_entry File.join(@config.root, asset_dir),
162
- File.join(@config.build_dir, asset_dir)
229
+ asset_path = File.join(@config.build_dir, asset_dir)
230
+ next if @config.dev_mode && File.exist?(asset_path)
231
+
232
+ source_path = File.join(@config.root, asset_dir)
233
+ next unless File.exist?(source_path)
234
+
235
+ FileUtils.copy_entry source_path, asset_path
163
236
  end
237
+
238
+ copy_static
164
239
  end
165
240
 
166
241
  private
167
242
 
243
+ def copy_static
244
+ static_dir = File.join(@config.root, @config.static_dir)
245
+
246
+ # same for the static dir, but just the content.
247
+ return unless File.exist?(static_dir)
248
+
249
+ copied_static_files = Set[Dir.children(@config.build_dir)]
250
+ Dir.children(static_dir).each do |child|
251
+ raise DuplicateStaticFileError if copied_static_files.include?(child)
252
+
253
+ copied_static_files << child
254
+ asset_path = File.join(@config.build_dir, child)
255
+ next if @config.dev_mode && File.exist?(asset_path)
256
+
257
+ source_path = File.join(static_dir, child)
258
+ next unless File.exist?(source_path)
259
+
260
+ FileUtils.copy_entry source_path, asset_path
261
+ end
262
+ end
263
+
168
264
  def dev_mode_content
169
265
  "<script src=\"http://localhost:#{@config.helper_port}/js/archival-helper.js\" type=\"application/javascript\"></script>" # rubocop:disable Layout/LineLength
170
266
  end
@@ -4,8 +4,8 @@ require 'tomlrb'
4
4
 
5
5
  module Archival
6
6
  class Config
7
- attr_reader :pages_dir, :objects_dir, :assets_dirs, :root, :build_dir,
8
- :helper_port, :dev_mode
7
+ attr_reader :pages_dir, :objects_dir, :static_dir, :assets_dirs, :root,
8
+ :build_dir, :helper_port, :dev_mode
9
9
 
10
10
  def initialize(config = {})
11
11
  @root = config['root'] || Dir.pwd
@@ -17,6 +17,7 @@ module Archival
17
17
  )
18
18
  @helper_port = config['helper_port'] || manifest['helper_port'] || 2701
19
19
  @assets_dirs = config['assets_dirs'] || manifest['assets'] || []
20
+ @static_dir = config['static_dir'] || manifest['static'] || 'static'
20
21
  @dev_mode = config[:dev_mode] || false
21
22
  end
22
23
 
@@ -44,16 +44,23 @@ module Archival
44
44
  MAGIC_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
45
45
 
46
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.
47
+ if method == 'GET' && path.start_with?('/js/archival-helper.js')
48
+ # For this special file, serve it from the helper dir
49
49
  http_response(client, type: 'application/javascript') do
50
- serve_static(client, path)
50
+ serve_static(client, path, @helper_dir)
51
51
  end
52
52
  client.close
53
53
  elsif (matches = req.match(/^Sec-WebSocket-Key: (\S+)/))
54
54
  websocket_key = matches[1]
55
55
  # puts "Websocket handshake detected with key: #{websocket_key}"
56
56
  connect_socket(client, websocket_key)
57
+ elsif method == 'GET'
58
+ # For static paths, just serve the files they refer to.
59
+ # TODO: mime type should be inferred from file type
60
+ http_response(client, type: 'application/javascript') do
61
+ serve_static(client, path)
62
+ end
63
+ client.close
57
64
  else
58
65
  client.close
59
66
  end
@@ -99,8 +106,9 @@ module Archival
99
106
 
100
107
  # Our server only supports single-frame, text messages.
101
108
  # 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
109
+ raise 'Archival dev server does not support continuations' unless fin
110
+ # Some browsers send this regardless, so ignore it to keep the noise down.
111
+ return unless opcode == 1
104
112
 
105
113
  second_byte = @socket.getbyte
106
114
  is_masked = second_byte & 0b10000000
@@ -139,8 +147,8 @@ module Archival
139
147
  @socket.write output.pack("CCA#{message.size}")
140
148
  end
141
149
 
142
- def serve_static(client, path)
143
- buffer = File.open(File.join(@helper_dir, path)).read
150
+ def serve_static(client, path, base = @build_dir)
151
+ buffer = File.open(File.join(base, path)).read
144
152
  buffer.sub! '$PORT', @port.to_s
145
153
  client.print buffer
146
154
  end
@@ -150,6 +158,7 @@ module Archival
150
158
  type = config[:type] ||= 'text/html'
151
159
  client.print "HTTP/1.1 #{status}\r\n"
152
160
  client.print "Content-Type: #{type}\r\n"
161
+ client.print "Access-Control-Allow-Origin: *\r\n"
153
162
  client.print "\r\n"
154
163
  yield
155
164
  end
@@ -4,6 +4,8 @@ require 'listen'
4
4
  require 'pathname'
5
5
 
6
6
  module Archival
7
+ Change = Struct.new(:path, :type)
8
+
7
9
  def listen(config = {})
8
10
  @config = Config.new(config.merge(dev_mode: true))
9
11
  builder = Builder.new(@config)
@@ -13,21 +15,27 @@ module Archival
13
15
  ignore = %r{/dist/}
14
16
  listener = Listen.to(@config.root,
15
17
  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
18
+ changes = {
19
+ pages: [],
20
+ objects: [],
21
+ assets: [],
22
+ layout: [],
23
+ config: []
24
+ }
25
+ add_change = lambda { |file, type|
26
+ c_type = change_type(file)
27
+ changes[c_type] << change(file, type) unless c_type == :none
28
+ }
29
+ added.each do |file|
30
+ add_change.call(file, :added)
31
+ end
32
+ modified.each do |file|
33
+ add_change.call(file, :modified)
28
34
  end
29
- @server.refresh_client if rebuild?(builder, updated_objects,
30
- updated_pages, updated_assets)
35
+ removed.each do |file|
36
+ add_change.call(file, :removed)
37
+ end
38
+ @server.refresh_client if rebuild?(builder, changes)
31
39
  end
32
40
  listener.start
33
41
  serve_helpers
@@ -45,6 +53,13 @@ module Archival
45
53
  false
46
54
  end
47
55
 
56
+ def change(file, type)
57
+ c = Change.new
58
+ c.path = Pathname.new(file).relative_path_from(@config.root)
59
+ c.type = type
60
+ c
61
+ end
62
+
48
63
  def change_type(file)
49
64
  # a page was modified, rebuild the pages.
50
65
  return :pages if child?(File.join(@config.root, @config.pages_dir),
@@ -53,26 +68,36 @@ module Archival
53
68
  return :objects if child?(File.join(@config.root, @config.objects_dir),
54
69
  file)
55
70
 
56
- # layout and other assets. For now, this is everything.
71
+ # an asset was changed, which just means to copy or delete it
57
72
  @config.assets_dirs.each do |dir|
58
73
  return :assets if child?(File.join(@config.root, dir), file)
59
74
  end
60
- return :assets if child?(File.join(@config.root, 'layout'), file)
61
- return :assets if ['manifest.toml',
75
+
76
+ # a static file was changed - copy or delete those too.
77
+ return :assets if child?(File.join(@config.root, @config.static_dir),
78
+ file)
79
+
80
+ # other special files
81
+ return :layout if child?(File.join(@config.root, 'layout'), file)
82
+ return :config if ['manifest.toml',
62
83
  'objects.toml'].include? File.basename(file)
63
84
 
64
85
  :none
65
86
  end
66
87
 
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
88
+ def rebuild?(builder, changes)
89
+ return false if changes.values.all?(&:empty?)
71
90
 
72
91
  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
92
+ if changes[:pages].length || changes[:objects].length
93
+ builder.update_pages(changes[:pages], changes[:objects])
94
+ end
95
+ builder.update_assets(changes[:assets]) if changes[:assets].length
96
+ if changes[:assets].length || changes[:layouts] || changes[:config]
97
+ # TODO: optimization: this could operate on the known subset of
98
+ # changes instead...
99
+ builder.full_rebuild
100
+ end
76
101
  builder.write_all
77
102
  end
78
103
  true
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redcarpet'
4
+
5
+ module Archival
6
+ class MarkdownRenderer < Redcarpet::Render::HTML
7
+ def autolink(link, _link_type, _opts)
8
+ # TODO: handle link_type?
9
+ "<a href=\"#{rewrite_link(link)}\">#{rewrite_link(link)}</a>"
10
+ end
11
+
12
+ def link(link, title, content)
13
+ "<a href=\"#{rewrite_link(link)}\" title=\"#{title}\">#{content}</a>"
14
+ end
15
+
16
+ def image(link, title, alt_text)
17
+ "<img src=\"#{rewrite_link(link)}\" \
18
+ title=\"#{title}\" alt=\"#{alt_text}\"/>"
19
+ end
20
+
21
+ private
22
+
23
+ def rewrite_link(link)
24
+ if link.start_with?('http') || link.start_with?('mailto') ||
25
+ link.start_with?('www') || link.start_with?('/')
26
+ link
27
+ end
28
+ template_dir = File.dirname(@options[:template_file])
29
+ resolved_link = Pathname.new(File.join(@options[:pages_root], link))
30
+ resolved_link.relative_path_from(template_dir)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'liquid'
4
+ require 'redcarpet'
5
+
6
+ module Archival
7
+ class Parser
8
+ def initialize(pages_root)
9
+ @pages_root = pages_root
10
+ end
11
+
12
+ def parse_object(object, definition, template_file)
13
+ markdown = Redcarpet::Markdown.new(
14
+ Archival::MarkdownRenderer.new(prettify: true,
15
+ hard_wrap: true),
16
+ no_intra_emphasis: true,
17
+ fenced_code_blocks: true,
18
+ autolink: true,
19
+ strikethrough: true,
20
+ underline: true,
21
+ template_file: template_file,
22
+ pages_root: @pages_root
23
+ )
24
+ definition.each do |name, type|
25
+ case type
26
+ when 'markdown'
27
+ object[name] = markdown.render(object[name]) if object.key? name
28
+ end
29
+ end
30
+ object
31
+ end
32
+ end
33
+ end
@@ -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.5'
4
+ VERSION = '0.0.9'
5
5
  end
data/lib/archival.rb CHANGED
@@ -1,12 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'liquid'
4
+ require 'tags/layout'
5
+ require 'tags/asset'
6
+
3
7
  module Archival
4
8
  # Main Archival module. See https://archival.dev for docs.
5
9
  end
6
10
 
11
+ Liquid::Template.error_mode = :strict
12
+ Liquid::Template.register_tag('layout', Layout)
13
+ Liquid::Template.register_tag('asset', Asset)
14
+
7
15
  require 'archival/version'
16
+ require 'archival/template_array'
8
17
  require 'archival/logger'
9
18
  require 'archival/config'
19
+ require 'archival/markdown_renderer'
10
20
  require 'archival/helper_server'
21
+ require 'archival/parser'
11
22
  require 'archival/builder'
12
23
  require 'archival/listen'
data/lib/tags/asset.rb ADDED
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'liquid'
4
+
5
+ class Asset < Liquid::Tag
6
+ # Adds an `asset` tag to liquid. Usage:
7
+ #
8
+ # {% asset "path/to/asset.png" %}
9
+ #
10
+ # This will replace the tag with a relative path to the asset from the
11
+ # current template. Using normal tags will work from the root, but when
12
+ # building dynamic pages or reusing layouts, asset paths are dynamic and
13
+ # will need to be rewritten.
14
+
15
+ prepend Liquid::Tag::Disableable
16
+
17
+ SYNTAX = /(#{Liquid::QuotedFragment}+)/o.freeze
18
+
19
+ def initialize(tag_name, markup, tokens)
20
+ super
21
+ raise AssetError, 'Invalid layout syntax' unless markup =~ SYNTAX
22
+
23
+ @path = parse_expression(Regexp.last_match(1))
24
+ # This is defaulted to the pages dir, because it represents the structure
25
+ # of our website. Asset directories are copied as siblings at runtime.
26
+ @@root_dir ||= File.join(Dir.pwd, 'pages')
27
+
28
+ @attributes = {}
29
+
30
+ markup.scan(Liquid::TagAttributes) do |key, value|
31
+ @attributes[key] = parse_expression(value)
32
+ end
33
+ end
34
+
35
+ def self.root_dir=(dir)
36
+ @@root_dir = dir
37
+ end
38
+
39
+ def self.helper_port=(port)
40
+ @@helper_port = port
41
+ end
42
+
43
+ def render_to_output_buffer(context, output)
44
+ unless @@root_dir
45
+ raise AssetError,
46
+ 'root_dir must be set on Archival::Asset'
47
+ end
48
+
49
+ unless context.key? 'template_path'
50
+ raise AssetError,
51
+ 'template_path must be provided to parse when using assets'
52
+ end
53
+ template_path = File.dirname(context['template_path'])
54
+ abs_asset_path = Pathname.new(File.join(@@root_dir, @path))
55
+ asset_path = abs_asset_path.relative_path_from(template_path).cleanpath.to_s
56
+ output << if @attributes['serve'] == true
57
+ "http://localhost:#{@@helper_port}/#{asset_path}"
58
+ else
59
+ asset_path
60
+ end
61
+ output
62
+ end
63
+ end
64
+
65
+ class AssetError < Liquid::Error
66
+ end
data/lib/tags/layout.rb CHANGED
@@ -25,7 +25,7 @@ class Layout < Liquid::Tag
25
25
  super
26
26
 
27
27
  @page_content = []
28
- raise 'Invalid layout syntax' unless markup =~ SYNTAX
28
+ raise LayoutError, 'Invalid layout syntax' unless markup =~ SYNTAX
29
29
 
30
30
  layout_name = Regexp.last_match(1)
31
31
  @layout_name_expr = parse_expression(layout_name)
@@ -40,7 +40,9 @@ class Layout < Liquid::Tag
40
40
  base_path = Dir.pwd
41
41
  layout_dir = 'layout'
42
42
  layout_path = File.join(base_path, layout_dir)
43
- raise "Layout dir #{layout_path} not found" unless File.exist? layout_path
43
+ unless File.exist? layout_path
44
+ raise LayoutError, "Layout dir #{layout_path} not found"
45
+ end
44
46
 
45
47
  layout_path
46
48
  end
@@ -55,11 +57,15 @@ class Layout < Liquid::Tag
55
57
  )
56
58
 
57
59
  next unless File.basename(f, '.*') == layout_name
58
- raise "More than one layout named #{layout_name} found." if found_layout
60
+ if found_layout
61
+ raise LayoutError, "More than one layout named #{layout_name} found."
62
+ end
59
63
 
60
64
  found_layout = File.join(layout_path, f)
61
65
  end
62
- raise "No layouts named #{layout_name} found." if found_layout.nil?
66
+ if found_layout.nil?
67
+ raise LayoutError, "No layouts named #{layout_name} found."
68
+ end
63
69
 
64
70
  layout = File.read(found_layout)
65
71
  @@layout_cache[layout_name] =
@@ -77,7 +83,7 @@ class Layout < Liquid::Tag
77
83
 
78
84
  def render_to_output_buffer(context, output)
79
85
  layout_name = context.evaluate(@layout_name_expr)
80
- raise 'Bad layout name argument' unless layout_name
86
+ raise LayoutError, 'Bad layout name argument' unless layout_name
81
87
 
82
88
  layout = load_layout(layout_name)
83
89
 
@@ -104,3 +110,6 @@ class Layout < Liquid::Tag
104
110
  output
105
111
  end
106
112
  end
113
+
114
+ class LayoutError < Liquid::Error
115
+ end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archival",
3
- "version": "0.0.5",
3
+ "version": "0.0.9",
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.5
4
+ version: 0.0.9
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-07 00:00:00.000000000 Z
11
+ date: 2022-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: liquid
@@ -104,14 +104,19 @@ files:
104
104
  - lib/archival/helper_server.rb
105
105
  - lib/archival/listen.rb
106
106
  - lib/archival/logger.rb
107
+ - lib/archival/markdown_renderer.rb
108
+ - lib/archival/parser.rb
107
109
  - lib/archival/rake_tasks.rb
110
+ - lib/archival/template_array.rb
108
111
  - lib/archival/version.rb
112
+ - lib/tags/asset.rb
109
113
  - lib/tags/layout.rb
110
114
  - package.json
111
115
  homepage: https://archival.dev
112
116
  licenses:
113
117
  - Unlicense
114
- metadata: {}
118
+ metadata:
119
+ rubygems_mfa_required: 'true'
115
120
  post_install_message:
116
121
  rdoc_options: []
117
122
  require_paths:
@@ -127,7 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
127
132
  - !ruby/object:Gem::Version
128
133
  version: '0'
129
134
  requirements: []
130
- rubygems_version: 3.2.22
135
+ rubygems_version: 3.3.3
131
136
  signing_key:
132
137
  specification_version: 4
133
138
  summary: An incredibly simple CMS for durable websites