archival 0.0.5 → 0.0.9

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.
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