impression 0.11 → 0.12

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.
@@ -1,276 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'fileutils'
4
- require 'date'
5
- require 'yaml'
6
- require 'modulation'
7
- require 'papercraft'
8
-
9
- require_relative './resource'
10
- require_relative './file_tree'
11
-
12
- module Impression
13
-
14
- # `Jamstack` implements a resource that maps to a Jamstack app directory.
15
- class Jamstack < FileTree
16
- def initialize(**props)
17
- super
18
- @layouts = {}
19
- end
20
-
21
- # Returns a list of pages found in the given directory (relative to the base
22
- # directory). Each entry containins the absolute file path, the pretty URL,
23
- # the possible date parsed from the file name, and any other front matter
24
- # attributes (for .md files). This method will detect only pages with the
25
- # extensions .html, .md, .rb. The returned entries are sorted by file path.
26
- #
27
- # @param dir [String] relative directory
28
- # @return [Array<Hash>] array of page entries
29
- def page_list(dir)
30
- base = File.join(@directory, dir)
31
- Dir.glob('*.{html,md}', base: base)
32
- .map { |fn| get_path_info(File.join(dir, fn)) }# page_entry(fn, dir) }
33
- .sort_by { |i| i[:path] }
34
- end
35
-
36
- private
37
-
38
- DATE_REGEXP = /(\d{4}\-\d{2}\-\d{2})/.freeze
39
- FRONT_MATTER_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m.freeze
40
- MD_EXT_REGEXP = /\.md$/.freeze
41
- PAGE_EXT_REGEXP = /^(.+)\.(md|html|rb)$/.freeze
42
- INDEX_PAGE_REGEXP = /^(.+)?\/index$/.freeze
43
-
44
- YAML_OPTS = {
45
- permitted_classes: [Date],
46
- symbolize_names: true
47
- }.freeze
48
-
49
- # Returns the path info for the given file path.
50
- #
51
- # @param path [String] file path
52
- # @return [Hash] path info
53
- def file_info(path)
54
- info = super
55
- case info[:ext]
56
- when '.md'
57
- atts, content = parse_markdown_file(path)
58
- info = info.merge(atts)
59
- info[:html_content] = Papercraft.markdown(content)
60
- info[:kind] = :markdown
61
- when '.rb'
62
- info[:module] = import(path)
63
- info[:kind] = :module
64
- end
65
- if (m = path.match(DATE_REGEXP))
66
- info[:date] ||= Date.parse(m[1])
67
- end
68
-
69
- info
70
- end
71
-
72
- # Returns the pretty URL for the given relative path. For pages, the
73
- # extension is removed. For index pages, the index suffix is removed.
74
- #
75
- # @param relative_path [String] relative path
76
- # @return [String] pretty URL
77
- def pretty_url(relative_path)
78
- if (m = relative_path.match(PAGE_EXT_REGEXP))
79
- relative_path = m[1]
80
- end
81
- if (m = relative_path.match(INDEX_PAGE_REGEXP))
82
- relative_path = m[1] || '/'
83
- end
84
- relative_path == '/' ? absolute_path : File.join(absolute_path, relative_path)
85
- end
86
-
87
- # Renders a response according to the given path info.
88
- #
89
- # @param req [Qeweney::Request] request
90
- # @param path_info [Hash] path info
91
- # @return [void]
92
- def render_from_path_info(req, path_info)
93
- case (kind = path_info[:kind])
94
- when :not_found
95
- mod_path_info = up_tree_resource_module_path_info(req, path_info)
96
- if mod_path_info
97
- render_module(req, mod_path_info)
98
- else
99
- req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)
100
- end
101
- when :module
102
- render_module(req, path_info)
103
- when :markdown
104
- render_markdown_file(req, path_info)
105
- when :file
106
- render_file(req, path_info)
107
- else
108
- raise "Invalid path info kind #{kind.inspect}"
109
- end
110
- end
111
-
112
- # Returns the path info for an up-tree resource module, or false if not
113
- # found. the :up_tree_resource_module_path_info KV can be either:
114
- # - nil (default): up tree module search has not been performed.
115
- # - false: no up tree module was found.
116
- # - module path info: up tree module info (subsequent requests will be
117
- # directly routed to the module).
118
- #
119
- # @param req [Qeweney::Request] request
120
- # @param path_info [Hash] path info
121
- # @return [Hash, false] up-tree resource module path info
122
- def up_tree_resource_module_path_info(req, path_info)
123
- if path_info[:up_tree_resource_module_path_info].nil?
124
- if (mod_path_info = find_up_tree_resource_module(req, path_info))
125
- path_info[:up_tree_resource_module_path_info] = mod_path_info
126
- return mod_path_info;
127
- else
128
- path_info[:up_tree_resource_module_path_info] = false
129
- return false
130
- end
131
- end
132
- path_info[:up_tree_resource_module_path_info]
133
- end
134
-
135
- # Performs a recursive search for an up-tree resource module from the given
136
- # path info. If a resource module is found up the tree, its path_info is
137
- # returned, otherwise returns nil.
138
- #
139
- # @param req [Qeweney::Request] request
140
- # @param path_info [Hash] path info
141
- # @return [Hash, nil] up-tree resource module path info
142
- def find_up_tree_resource_module(req, path_info)
143
- relative_path = req.resource_relative_path
144
-
145
- while relative_path != path
146
- up_tree_path = File.expand_path('..', relative_path)
147
- up_tree_path_info = get_path_info(up_tree_path)
148
-
149
- case up_tree_path_info[:kind]
150
- when :not_found
151
- relative_path = up_tree_path
152
- next
153
- when :module
154
- return up_tree_path_info
155
- else
156
- return nil
157
- end
158
- end
159
- nil
160
- end
161
-
162
- # Renders a file response for the given request and the given path info,
163
- # according to the file type.
164
- #
165
- # @param req [Qeweney::Request] request
166
- # @param path_info [Hash] path info
167
- # @return [void]
168
- # def render_file(req, path_info)
169
- # case path_info[:kind]
170
- # else
171
- # req.serve_file(path_info[:path])
172
- # end
173
- # end
174
-
175
- # Renders a module. If the module is a Resource, it is mounted, and then the
176
- # request is rerouted from the new resource and rendered. If the module is a
177
- # Proc or a Papercraft::Template, it is rendered as such. Otherwise, an
178
- # error is raised.
179
- #
180
- # @param req [Qeweney::Request] request
181
- # @param path_info [Hash] path info
182
- # @return [void]
183
- def render_module(req, path_info)
184
- # p render_module: path_info
185
- case (mod = path_info[:module])
186
- when Module
187
- resource = mod.resource
188
- resource.remount(self, path_info[:url])
189
- # p path_info_url: path_info[:url], relative_path: req.resource_relative_path
190
- relative_url = path_info[:url].gsub(/^#{path}/, '')
191
- # p relative_url: relative_url
192
- req.recalc_resource_relative_path(relative_url)
193
- # p resource_relative_path: req.resource_relative_path
194
- resource.route(req).call(req)
195
- when Impression::Resource
196
- mod.remount(self, path_info[:url])
197
- req.recalc_resource_relative_path(path_info[:url])
198
- mod.route(req).call(req)
199
- when Proc, Papercraft::Template
200
- render_papercraft_module(req, mod)
201
- else
202
- raise "Unsupported module type #{mod.class}"
203
- end
204
- end
205
-
206
- # Renders a Papercraft module.
207
- #
208
- # @param mod [Module] Papercraft module
209
- # @param path_info [Hash] path info
210
- # @return [void]
211
- def render_papercraft_module(req, mod)
212
- template = Papercraft.html(mod)
213
- body = template.render(request: req, resource: self)
214
- req.respond(body, 'Content-Type' => template.mime_type)
215
- end
216
-
217
- # Renders a markdown file using a layout.
218
- #
219
- # @param req [Qeweney::Request] reqest
220
- # @param path_info [Hash] path info
221
- # @return [void]
222
- def render_markdown_file(req, path_info)
223
- layout = get_layout(path_info[:layout])
224
-
225
- html = layout.render(request: req, resource: self, **path_info) {
226
- emit path_info[:html_content]
227
- }
228
- req.respond(html, 'Content-Type' => layout.mime_type)
229
- end
230
-
231
- # Returns a layout component based on the given name. The given name
232
- # defaults to 'default' if nil.
233
- #
234
- # @param layout [String, nil] layout name
235
- # @return [Papercraft::Template] layout component
236
- def get_layout(layout)
237
- layout ||= 'default'
238
- path = File.join(@directory, "_layouts/#{layout}.rb")
239
- raise "Layout not found #{path}" unless File.file?(path)
240
-
241
- import path
242
- end
243
-
244
- # Parses the markdown file at the given path.
245
- #
246
- # @param path [String] file path
247
- # @return [Array] an tuple containing properties<Hash>, contents<String>
248
- def parse_markdown_file(path)
249
- content = IO.read(path) || ''
250
- atts = {}
251
-
252
- # Parse date from file name
253
- if (m = path.match(DATE_REGEXP))
254
- atts[:date] ||= Date.parse(m[1])
255
- end
256
-
257
- if (m = content.match(FRONT_MATTER_REGEXP))
258
- front_matter = m[1]
259
- content = m.post_match
260
-
261
- yaml = YAML.safe_load(front_matter, **YAML_OPTS)
262
- atts = atts.merge(yaml)
263
- end
264
-
265
- [atts, content]
266
- end
267
-
268
- # Returns the supported path extensions used for searching for files based
269
- # on pretty URLs.
270
- #
271
- # @return [Array] list of supported path extensions
272
- def supported_path_extensions
273
- [:html, :rb, :md]
274
- end
275
- end
276
- end
@@ -1,350 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'helper'
4
- require 'qeweney/test_adapter'
5
-
6
- class JamstackTest < MiniTest::Test
7
- JAMSTACK_PATH = File.join(__dir__, 'jamstack')
8
-
9
- def setup
10
- @jamstack = Impression::Jamstack.new(path: '/', directory: JAMSTACK_PATH)
11
- end
12
-
13
- def test_jamstack_routing
14
- req = mock_req(':method' => 'GET', ':path' => '/')
15
- assert_equal @jamstack, @jamstack.route(req)
16
-
17
- req = mock_req(':method' => 'GET', ':path' => '/nonexistent')
18
- assert_equal @jamstack, @jamstack.route(req)
19
-
20
- req = mock_req(':method' => 'GET', ':path' => '/index.html')
21
- assert_equal @jamstack, @jamstack.route(req)
22
-
23
- req = mock_req(':method' => 'GET', ':path' => '/foo')
24
- assert_equal @jamstack, @jamstack.route(req)
25
- end
26
-
27
- def static(path)
28
- IO.read(File.join(JAMSTACK_PATH, path))
29
- end
30
-
31
- def test_jamstack_response
32
- req = mock_req(':method' => 'GET', ':path' => '/roo')
33
- @jamstack.route_and_call(req)
34
- assert_equal Qeweney::Status::NOT_FOUND, req.response_status
35
-
36
- req = mock_req(':method' => 'GET', ':path' => '/foo2')
37
- @jamstack.route_and_call(req)
38
- assert_equal Qeweney::Status::NOT_FOUND, req.response_status
39
-
40
- req = mock_req(':method' => 'GET', ':path' => '/bar2')
41
- @jamstack.route_and_call(req)
42
- assert_equal Qeweney::Status::NOT_FOUND, req.response_status
43
-
44
- req = mock_req(':method' => 'GET', ':path' => '/assets/js/a.js')
45
- @jamstack.route_and_call(req)
46
- assert_response static('assets/js/a.js'), :js, req
47
-
48
- req = mock_req(':method' => 'GET', ':path' => '/foo')
49
- @jamstack.route_and_call(req)
50
-
51
- foo = Papercraft.html {
52
- html5 {
53
- head {
54
- title 'Foo title'
55
- }
56
- body {
57
- h1 'foo'
58
- }
59
- }
60
- }
61
- assert_response foo.render, :html, req
62
-
63
- req = mock_req(':method' => 'GET', ':path' => '/index')
64
- @jamstack.route_and_call(req)
65
-
66
- index = Papercraft.html {
67
- html5 {
68
- head {
69
- title 'Hello'
70
- }
71
- body {
72
- h1 'Index'
73
- }
74
- }
75
- }
76
- assert_response index.render, :html, req
77
-
78
- req = mock_req(':method' => 'GET', ':path' => '/')
79
- @jamstack.route_and_call(req)
80
- assert_response index.render, :html, req
81
-
82
- req = mock_req(':method' => 'GET', ':path' => '/bar')
83
- @jamstack.route_and_call(req)
84
- assert_response static('bar.html'), :html, req
85
-
86
- req = mock_req(':method' => 'GET', ':path' => '/baz')
87
- @jamstack.route_and_call(req)
88
-
89
- baz_index = Papercraft.html {
90
- html5 {
91
- head {
92
- title 'BarBar'
93
- }
94
- body {
95
- h1 'BarIndex'
96
- }
97
- }
98
- }
99
- assert_response baz_index.render, :html, req
100
-
101
- req = mock_req(':method' => 'GET', ':path' => '/articles/a')
102
- @jamstack.route_and_call(req)
103
-
104
- a = Papercraft.html {
105
- html5 {
106
- head {
107
- title 'AAA'
108
- }
109
- body {
110
- article {
111
- h2 'ZZZ', id: 'zzz'
112
- }
113
- }
114
- }
115
- }
116
- assert_response a.render, :html, req
117
- end
118
-
119
- def test_non_root_jamstack_response
120
- @jamstack = Impression::Jamstack.new(path: '/app', directory: JAMSTACK_PATH)
121
-
122
- req = mock_req(':method' => 'GET', ':path' => '/app/roo')
123
- @jamstack.route_and_call(req)
124
- assert_equal Qeweney::Status::NOT_FOUND, req.response_status
125
-
126
- req = mock_req(':method' => 'GET', ':path' => '/app/foo2')
127
- @jamstack.route_and_call(req)
128
- assert_equal Qeweney::Status::NOT_FOUND, req.response_status
129
-
130
- req = mock_req(':method' => 'GET', ':path' => '/app/bar2')
131
- @jamstack.route_and_call(req)
132
- assert_equal Qeweney::Status::NOT_FOUND, req.response_status
133
-
134
- req = mock_req(':method' => 'GET', ':path' => '/app/assets/js/a.js')
135
- @jamstack.route_and_call(req)
136
- assert_response static('assets/js/a.js'), :js, req
137
-
138
- req = mock_req(':method' => 'GET', ':path' => '/app/foo')
139
- @jamstack.route_and_call(req)
140
-
141
- foo = Papercraft.html {
142
- html5 {
143
- head {
144
- title 'Foo title'
145
- }
146
- body {
147
- h1 'foo'
148
- }
149
- }
150
- }
151
- assert_response foo.render, :html, req
152
-
153
- req = mock_req(':method' => 'GET', ':path' => '/app/index')
154
- @jamstack.route_and_call(req)
155
-
156
- index = Papercraft.html {
157
- html5 {
158
- head {
159
- title 'Hello'
160
- }
161
- body {
162
- h1 'Index'
163
- }
164
- }
165
- }
166
- assert_response index.render, :html, req
167
-
168
- req = mock_req(':method' => 'GET', ':path' => '/app/')
169
- @jamstack.route_and_call(req)
170
- assert_response index.render, :html, req
171
-
172
- req = mock_req(':method' => 'GET', ':path' => '/app/bar')
173
- @jamstack.route_and_call(req)
174
- assert_response static('bar.html'), :html, req
175
-
176
- req = mock_req(':method' => 'GET', ':path' => '/app/baz')
177
- @jamstack.route_and_call(req)
178
-
179
- baz_index = Papercraft.html {
180
- html5 {
181
- head {
182
- title 'BarBar'
183
- }
184
- body {
185
- h1 'BarIndex'
186
- }
187
- }
188
- }
189
- assert_response baz_index.render, :html, req
190
-
191
- req = mock_req(':method' => 'GET', ':path' => '/app/articles/a')
192
- @jamstack.route_and_call(req)
193
-
194
- a = Papercraft.html {
195
- html5 {
196
- head {
197
- title 'AAA'
198
- }
199
- body {
200
- article {
201
- h2 'ZZZ', id: 'zzz'
202
- }
203
- }
204
- }
205
- }
206
- assert_response a.render, :html, req
207
- end
208
-
209
- def test_page_list
210
- @jamstack = Impression::Jamstack.new(path: '/app', directory: JAMSTACK_PATH)
211
-
212
- list = @jamstack.page_list('/')
213
- assert_equal [
214
- { kind: :file, path: File.join(JAMSTACK_PATH, 'bar.html'), ext: '.html', url: '/app/bar' },
215
- { kind: :markdown, path: File.join(JAMSTACK_PATH, 'index.md'), ext: '.md', url: '/app',
216
- title: 'Hello', foo: 'BarBar', html_content: "<h1>Index</h1>\n" },
217
- ], list
218
-
219
-
220
- list = @jamstack.page_list('/articles')
221
-
222
- assert_equal [
223
- {
224
- kind: :markdown,
225
- path: File.join(JAMSTACK_PATH, 'articles/2008-06-14-manu.md'),
226
- url: '/app/articles/2008-06-14-manu',
227
- ext: '.md',
228
- title: 'MMM',
229
- layout: 'article',
230
- foo: {
231
- bar: {
232
- baz: 42
233
- }
234
- },
235
- html_content: "<h2 id=\"bbb\">BBB</h2>\n",
236
- date: Date.new(2008, 06, 14)
237
- },
238
- {
239
- kind: :markdown,
240
- path: File.join(JAMSTACK_PATH, 'articles/2009-06-12-noatche.md'),
241
- url: '/app/articles/2009-06-12-noatche',
242
- ext: '.md',
243
- title: 'NNN',
244
- layout: 'article',
245
- html_content: "<h2 id=\"ccc\">CCC</h2>\n",
246
- date: Date.new(2009, 06, 12)
247
- },
248
- {
249
- kind: :markdown,
250
- path: File.join(JAMSTACK_PATH, 'articles/a.md'),
251
- url: '/app/articles/a',
252
- ext: '.md',
253
- title: 'AAA',
254
- layout: 'article',
255
- html_content: "<h2 id=\"zzz\">ZZZ</h2>\n",
256
- },
257
- ], list
258
- end
259
-
260
- def test_template_resource_and_request
261
- req = mock_req(':method' => 'GET', ':path' => '/foobar?q=42')
262
- @jamstack.route_and_call(req)
263
-
264
- foo = Papercraft.html {
265
- html5 {
266
- head {
267
- title 'Foobar'
268
- }
269
- body {
270
- h1 '42'
271
- a 'MMM', href: '/articles/2008-06-14-manu'
272
- a 'NNN', href: '/articles/2009-06-12-noatche'
273
- a 'AAA', href: '/articles/a'
274
- }
275
- }
276
- }
277
- assert_response foo.render, :html, req
278
- end
279
-
280
- def path_info(path)
281
- @jamstack.send(:get_path_info, path)
282
- end
283
-
284
- def test_path_info
285
- assert_equal({
286
- kind: :markdown,
287
- path: File.join(JAMSTACK_PATH, 'index.md'),
288
- ext: '.md',
289
- url: '/',
290
- title: 'Hello',
291
- foo: 'BarBar',
292
- html_content: "<h1>Index</h1>\n"
293
- }, path_info('/index'))
294
-
295
- assert_equal({
296
- kind: :markdown,
297
- path: File.join(JAMSTACK_PATH, 'index.md'),
298
- ext: '.md',
299
- url: '/',
300
- title: 'Hello',
301
- foo: 'BarBar',
302
- html_content: "<h1>Index</h1>\n"
303
- }, path_info('/'))
304
-
305
- assert_equal({
306
- kind: :file,
307
- path: File.join(JAMSTACK_PATH, 'assets/js/a.js'),
308
- ext: '.js',
309
- url: '/assets/js/a.js'
310
- }, path_info('/assets/js/a.js'))
311
-
312
- assert_equal({
313
- kind: :not_found,
314
- }, path_info('/js/b.js'))
315
- end
316
-
317
- def test_resource_loading
318
- req = mock_req(':method' => 'GET', ':path' => '/resources/greeter?name=world')
319
- route = @jamstack.route(req)
320
- assert_equal @jamstack, route
321
-
322
- req = mock_req(':method' => 'GET', ':path' => '/resources/greeter?name=world')
323
- @jamstack.route_and_call(req)
324
- assert_response 'Hello, world!', :text, req
325
-
326
- req = mock_req(':method' => 'GET', ':path' => '/resources/recurse/resources/greeter?name=foo')
327
- @jamstack.route_and_call(req)
328
- assert_response 'Hello, foo!', :text, req
329
- end
330
-
331
- def test_recursive_resource_loading_on_non_root_jamstack
332
- jamstack = Impression::Jamstack.new(path: '/foo/bar', directory: JAMSTACK_PATH)
333
-
334
- req = mock_req(':method' => 'GET', ':path' => '/foo/bar/resources/greeter?name=world')
335
- route = jamstack.route(req)
336
- assert_equal jamstack, route
337
-
338
- req = mock_req(':method' => 'GET', ':path' => '/foo/bar/resources/greeter?name=world')
339
- jamstack.route_and_call(req)
340
- assert_response 'Hello, world!', :text, req
341
-
342
- req = mock_req(':method' => 'GET', ':path' => '/foo/bar/resources/recurse/resources/greeter?name=foo')
343
- jamstack.route_and_call(req)
344
- assert_response 'Hello, foo!', :text, req
345
-
346
- # req = mock_req(':method' => 'GET', ':path' => '/foo/bar/resources/recurse/bar')
347
- # @jamstack.route_and_call(req)
348
- # assert_response static('bar.html'), :html, req
349
- end
350
- end