syntropy 0.38.0 → 0.39.0
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 +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +2 -0
- data/TODO.md +12 -3
- data/cmd/console.rb +2 -2
- data/cmd/serve.rb +3 -4
- data/cmd/test.rb +12 -4
- data/examples/blog/app/posts/[id]/edit.rb +3 -3
- data/examples/blog/app/posts/[id]/index.rb +8 -10
- data/examples/blog/app/posts/index.rb +6 -8
- data/examples/blog/app/posts/new.rb +3 -3
- data/lib/syntropy/app.rb +25 -59
- data/lib/syntropy/controller_extensions.rb +3 -22
- data/lib/syntropy/markdown.rb +115 -11
- data/lib/syntropy/module_loader.rb +85 -64
- data/lib/syntropy/request/request_info.rb +8 -0
- data/lib/syntropy/request/response.rb +1 -0
- data/lib/syntropy/test.rb +11 -2
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +2 -1
- data/test/fixtures/app/_layout/default.rb +1 -1
- data/test/fixtures/app/_layout/kuku.rb +8 -0
- data/test/fixtures/app/_lib/circular/a.rb +2 -0
- data/test/fixtures/app/_lib/circular/b.rb +2 -0
- data/test/fixtures/app/_lib/circular/c.rb +2 -0
- data/test/fixtures/app/mod/concurrent.rb +9 -0
- data/test/test_app.rb +1 -1
- data/test/test_caching.rb +3 -3
- data/test/test_markdown.rb +268 -0
- data/test/test_module_loader.rb +28 -3
- data/test/test_request.rb +7 -0
- data/test/test_response.rb +30 -0
- data/test/test_schema.rb +1 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c6b3b46669d534dbaf6517708d7fef38fd44bf20bd99513e065d173ca231116d
|
|
4
|
+
data.tar.gz: 2aaa7078c2c81adae4bae1141130bfedffd08c2239ea492b2c0076ad40ae9dcf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '00499df3f4e992f461a36a33efb467d46d719e5b29c6756f0bdfc6a88dbf85355619fef1a61e39a96ec36d25b492c8a862db47b551f725ec0d841fc0ec66105a'
|
|
7
|
+
data.tar.gz: 7d8a2f86e69873779e03a9acc65fed022c05ecd543ce48a9e0fa77d3fadc774712fc2627c8a299a1ee764e3eb3800cf4cb31ccafd5e91e035bc37bda401e8c11
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
# 0.39.0 2026-06-21
|
|
2
|
+
|
|
3
|
+
- Add support for embedded Papercraft snippets in Markdown files
|
|
4
|
+
- Reimplement Markdown rendering
|
|
5
|
+
- Increase cache-control max-age to one week
|
|
6
|
+
- Add support for relative redirects, add `Request#rel`
|
|
7
|
+
- Add support for concurrent loading, add mutex lock to module loader
|
|
8
|
+
- Fix `Syntropy.load_config`
|
|
9
|
+
- Read module source file using UringMachine
|
|
10
|
+
- Raise on circular module dependency
|
|
11
|
+
- Streamline CLI options across commands
|
|
12
|
+
|
|
13
|
+
# 0.38.1 2026-06-13
|
|
14
|
+
|
|
15
|
+
- Fix builtin app, controller extensions
|
|
16
|
+
|
|
1
17
|
# 0.38.0 2026-06-13
|
|
2
18
|
|
|
3
19
|
- Reimplement controller extensions: `dispatch_by_host`,
|
data/README.md
CHANGED
|
@@ -52,6 +52,8 @@ Syntropy is based on:
|
|
|
52
52
|
- [Extralite](https://github.com/digital-fabric/extralite) a fast and innovative
|
|
53
53
|
SQLite wrapper for Ruby.
|
|
54
54
|
|
|
55
|
+
Syntropy is 100% brain-coded.
|
|
56
|
+
|
|
55
57
|
## Examples
|
|
56
58
|
|
|
57
59
|
To get a taste of some of Syntropy's capabilities, you can run the included
|
data/TODO.md
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
## Immediate
|
|
2
2
|
|
|
3
|
+
- [ ] Ability to load modules from builtin applet
|
|
4
|
+
- [ ] Can we mount them on the app's module loader?
|
|
5
|
+
|
|
3
6
|
- [ ] Controllers
|
|
4
|
-
- [ ]
|
|
5
|
-
|
|
7
|
+
- [ ] add dispatch_json_rpc, dispatch_json_api
|
|
8
|
+
|
|
9
|
+
- [ ] Pub/sub
|
|
10
|
+
- [ ] Ruby side
|
|
11
|
+
- [ ] JS side
|
|
12
|
+
- [ ] Reimplement `auto_refresh` using a *default* event bus provided by
|
|
13
|
+
Syntropy
|
|
14
|
+
|
|
15
|
+
## Collections
|
|
6
16
|
|
|
7
17
|
- [ ] Collection - treat directories and files as collections of data.
|
|
8
18
|
|
|
@@ -34,7 +44,6 @@
|
|
|
34
44
|
|
|
35
45
|
- [ ] Improve serving of static files:
|
|
36
46
|
- [ ] support for compression
|
|
37
|
-
- [ ] support for caching headers
|
|
38
47
|
- [ ] add `Request#render_static_file(route, fn)
|
|
39
48
|
|
|
40
49
|
## Missing for a first public release
|
data/cmd/console.rb
CHANGED
|
@@ -17,11 +17,11 @@ env = {
|
|
|
17
17
|
parser = OptionParser.new do |o|
|
|
18
18
|
o.banner = 'Usage: syntropy console [options]'
|
|
19
19
|
|
|
20
|
-
o.on('-a', '--app PATH', 'Set app directory (default: ./app') do |path|
|
|
20
|
+
o.on('-a', '--app PATH', 'Set app directory (default: ./app)') do |path|
|
|
21
21
|
env[:app_root] = path
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
o.on('-c', '--config PATH', 'Set config directory (default: ./config') do |path|
|
|
24
|
+
o.on('-c', '--config PATH', 'Set config directory (default: ./config)') do |path|
|
|
25
25
|
env[:config_root] = path
|
|
26
26
|
end
|
|
27
27
|
|
data/cmd/serve.rb
CHANGED
|
@@ -20,17 +20,16 @@ env = {
|
|
|
20
20
|
parser = OptionParser.new do |o|
|
|
21
21
|
o.banner = 'Usage: syntropy serve [options]'
|
|
22
22
|
|
|
23
|
-
o.on('-a', '--app PATH', 'Set app directory (default: ./app') do |path|
|
|
23
|
+
o.on('-a', '--app PATH', 'Set app directory (default: ./app)') do |path|
|
|
24
24
|
env[:app_root] = path
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
o.on('-b', '--bind
|
|
28
|
-
'Bind address (default: http://0.0.0.0:1234). You can specify this flag multiple times to bind to multiple addresses.') do
|
|
27
|
+
o.on('-b', '--bind ADDRESS', String, 'Bind address (default: 0.0.0.0:1234)') do
|
|
29
28
|
env[:bind] ||= []
|
|
30
29
|
env[:bind] << it
|
|
31
30
|
end
|
|
32
31
|
|
|
33
|
-
o.on('-c', '--config PATH', 'Set config directory (default: ./config') do |path|
|
|
32
|
+
o.on('-c', '--config PATH', 'Set config directory (default: ./config)') do |path|
|
|
34
33
|
env[:config_root] = path
|
|
35
34
|
end
|
|
36
35
|
|
data/cmd/test.rb
CHANGED
|
@@ -16,11 +16,11 @@ MINITEST_ARGV = []
|
|
|
16
16
|
parser = OptionParser.new do |o|
|
|
17
17
|
o.banner = 'Usage: syntropy test [options]'
|
|
18
18
|
|
|
19
|
-
o.on('-a', '--app PATH', 'Set app directory (default: ./app') do |path|
|
|
19
|
+
o.on('-a', '--app PATH', 'Set app directory (default: ./app)') do |path|
|
|
20
20
|
env[:app_root] = path
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
o.on('-c', '--config PATH', 'Set config directory (default: ./config') do |path|
|
|
23
|
+
o.on('-c', '--config PATH', 'Set config directory (default: ./config)') do |path|
|
|
24
24
|
env[:config_root] = path
|
|
25
25
|
end
|
|
26
26
|
|
|
@@ -29,6 +29,10 @@ parser = OptionParser.new do |o|
|
|
|
29
29
|
exit
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
o.on('-f', '--file PATH', 'Set test file') do |path|
|
|
33
|
+
env[:test_file] = path
|
|
34
|
+
end
|
|
35
|
+
|
|
32
36
|
o.on('-m', '--mount PATH', 'Set mount path (default: /)') do |path|
|
|
33
37
|
env[:mount_path] = path
|
|
34
38
|
env[:builtin_applet_path] = File.join(path, '.syntropy')
|
|
@@ -82,7 +86,11 @@ Syntropy.load_config(env)
|
|
|
82
86
|
$stdout.sync = true
|
|
83
87
|
$stderr.sync = true
|
|
84
88
|
|
|
85
|
-
|
|
89
|
+
if env[:test_file]
|
|
90
|
+
require(File.expand_path(env[:test_file]))
|
|
91
|
+
else
|
|
92
|
+
Dir.glob("#{File.expand_path(env[:test_root])}/test_*.rb").each { require(it) }
|
|
93
|
+
end
|
|
86
94
|
|
|
87
95
|
def restart_on_file_change(machine, dir, restart_argv)
|
|
88
96
|
machine.file_watch(dir, UM::IN_CREATE | UM::IN_DELETE | UM::IN_CLOSE_WRITE) {
|
|
@@ -92,7 +100,7 @@ def restart_on_file_change(machine, dir, restart_argv)
|
|
|
92
100
|
exec('ruby', __FILE__, *restart_argv)
|
|
93
101
|
end
|
|
94
102
|
|
|
95
|
-
Syntropy::Test.
|
|
103
|
+
Syntropy::Test.global_env = env
|
|
96
104
|
Minitest.run MINITEST_ARGV
|
|
97
105
|
|
|
98
106
|
if env[:watch_mode]
|
|
@@ -9,14 +9,14 @@ def get(req)
|
|
|
9
9
|
raise Syntropy::Error.not_found if !post
|
|
10
10
|
|
|
11
11
|
req.respond_html(
|
|
12
|
-
@template.render(post:)
|
|
12
|
+
@template.render(post:, req:)
|
|
13
13
|
)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
@template = @layout.apply { |post:, **props|
|
|
16
|
+
@template = @layout.apply { |post:, req:, **props|
|
|
17
17
|
h1 "Edit blog post"
|
|
18
18
|
div {
|
|
19
|
-
form(action: "
|
|
19
|
+
form(action: req.rel(".."), method: 'post') {
|
|
20
20
|
div {
|
|
21
21
|
label 'Title', for: 'title'
|
|
22
22
|
input name: 'title', type: 'text', value: post[:title]
|
|
@@ -8,9 +8,7 @@ def get(req)
|
|
|
8
8
|
post = @posts.get(id)
|
|
9
9
|
raise Syntropy::Error.not_found if !post
|
|
10
10
|
|
|
11
|
-
req.respond_html(
|
|
12
|
-
@template.render(post:, req:)
|
|
13
|
-
)
|
|
11
|
+
req.respond_html(@template.render(post:, req:))
|
|
14
12
|
end
|
|
15
13
|
|
|
16
14
|
def post(req)
|
|
@@ -32,15 +30,15 @@ def delete(req)
|
|
|
32
30
|
id = req.route_params['id'].to_i
|
|
33
31
|
|
|
34
32
|
deleted = @posts.delete(id)
|
|
35
|
-
raise BadRequestError,
|
|
33
|
+
raise BadRequestError, 'Failed to delete post' if deleted != 1
|
|
36
34
|
|
|
37
35
|
req.flash[:notice] = 'Post was successfully destroyed.'
|
|
38
|
-
req.redirect
|
|
36
|
+
req.redirect '..', Syntropy::HTTP::SEE_OTHER
|
|
39
37
|
end
|
|
40
38
|
|
|
41
|
-
@template = @layout.apply { |post:, **props|
|
|
42
|
-
h1
|
|
43
|
-
p
|
|
39
|
+
@template = @layout.apply { |post:, req:, **props|
|
|
40
|
+
h1 'My blog'
|
|
41
|
+
p req.flash[:notice], style: 'color: green'
|
|
44
42
|
div {
|
|
45
43
|
h2 {
|
|
46
44
|
a post[:title]
|
|
@@ -48,9 +46,9 @@ end
|
|
|
48
46
|
p post[:body]
|
|
49
47
|
}
|
|
50
48
|
p {
|
|
51
|
-
a
|
|
49
|
+
a 'Edit', href: req.rel('./edit')
|
|
52
50
|
span '|'
|
|
53
|
-
a
|
|
51
|
+
a 'Back to posts', href: req.rel('..')
|
|
54
52
|
}
|
|
55
53
|
div {
|
|
56
54
|
form(method: 'post') {
|
|
@@ -5,9 +5,7 @@ export dispatch_by_http_method
|
|
|
5
5
|
|
|
6
6
|
def get(req)
|
|
7
7
|
posts = @posts.get_all
|
|
8
|
-
req.respond_html(
|
|
9
|
-
@template.render(posts:, req:)
|
|
10
|
-
)
|
|
8
|
+
req.respond_html(@template.render(posts:, req:))
|
|
11
9
|
end
|
|
12
10
|
|
|
13
11
|
def post(req)
|
|
@@ -17,16 +15,16 @@ def post(req)
|
|
|
17
15
|
id = @posts.create(title, body)
|
|
18
16
|
|
|
19
17
|
req.flash[:notice] = 'Post was successfully created.'
|
|
20
|
-
req.redirect("
|
|
18
|
+
req.redirect("./#{id}")
|
|
21
19
|
end
|
|
22
20
|
|
|
23
|
-
@template = @layout.apply {
|
|
21
|
+
@template = @layout.apply { |req:, **props|
|
|
24
22
|
h1 "My awesome blog"
|
|
25
|
-
p
|
|
23
|
+
p req.flash[:notice], style: 'color: green'
|
|
26
24
|
props[:posts].each { |post|
|
|
27
25
|
div {
|
|
28
26
|
h2 {
|
|
29
|
-
a post[:title], href: "
|
|
27
|
+
a post[:title], href: req.rel("./#{post[:id]}")
|
|
30
28
|
}
|
|
31
29
|
p post[:body]
|
|
32
30
|
}
|
|
@@ -34,7 +32,7 @@ end
|
|
|
34
32
|
|
|
35
33
|
div {
|
|
36
34
|
p {
|
|
37
|
-
a "New post", href: '
|
|
35
|
+
a "New post", href: req.rel('./new')
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
38
|
}
|
|
@@ -5,14 +5,14 @@ export dispatch_by_http_method
|
|
|
5
5
|
|
|
6
6
|
def get(req)
|
|
7
7
|
req.respond_html(
|
|
8
|
-
@template.render
|
|
8
|
+
@template.render(req:)
|
|
9
9
|
)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
@template = @layout.apply {
|
|
12
|
+
@template = @layout.apply { |req:, **props|
|
|
13
13
|
h1 "Create blog post"
|
|
14
14
|
div {
|
|
15
|
-
form(action: "
|
|
15
|
+
form(action: req.rel(".."), method: 'post') {
|
|
16
16
|
div {
|
|
17
17
|
label 'Title', for: 'title'
|
|
18
18
|
input name: 'title', type: 'text'
|
data/lib/syntropy/app.rb
CHANGED
|
@@ -25,6 +25,24 @@ module Syntropy
|
|
|
25
25
|
site_file_app(env) || default_app(env)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
BUILTIN_APPLET_app_root = File.expand_path(File.join(__dir__, 'applets/builtin'))
|
|
29
|
+
|
|
30
|
+
# Creates a builtin applet with the given environment hash. By default the
|
|
31
|
+
# builtin applet is mounted at /.syntropy.
|
|
32
|
+
#
|
|
33
|
+
# @param env [Hash] app environment
|
|
34
|
+
# @param mount_path [String] mount path for the builtin applet
|
|
35
|
+
# @return [Syntropy::App] applet
|
|
36
|
+
def builtin_applet(env, mount_path: '/.syntropy')
|
|
37
|
+
new(
|
|
38
|
+
machine: env[:machine],
|
|
39
|
+
app_root: BUILTIN_APPLET_app_root,
|
|
40
|
+
mount_path: mount_path,
|
|
41
|
+
builtin_applet_path: nil,
|
|
42
|
+
watch_files: nil
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
28
46
|
private
|
|
29
47
|
|
|
30
48
|
# Creates a multi-hostname app if a _site.rb file is detected.
|
|
@@ -171,7 +189,7 @@ module Syntropy
|
|
|
171
189
|
# @return [void]
|
|
172
190
|
def mount_builtin_applet
|
|
173
191
|
path = @env[:builtin_applet_path]
|
|
174
|
-
@builtin_applet ||=
|
|
192
|
+
@builtin_applet ||= App.builtin_applet(@env, mount_path: path)
|
|
175
193
|
@routing_tree.mount_applet(path, @builtin_applet)
|
|
176
194
|
end
|
|
177
195
|
|
|
@@ -233,6 +251,8 @@ module Syntropy
|
|
|
233
251
|
}
|
|
234
252
|
end
|
|
235
253
|
|
|
254
|
+
DEFAULT_CACHE_CONTROL = 'max-age=604800' # one week
|
|
255
|
+
|
|
236
256
|
# Serves a static file from the given target hash with cache validation.
|
|
237
257
|
#
|
|
238
258
|
# @param req [Syntropy::Request] request
|
|
@@ -241,7 +261,7 @@ module Syntropy
|
|
|
241
261
|
def serve_static_file(req, target)
|
|
242
262
|
validate_static_file_info(target)
|
|
243
263
|
cache_opts = {
|
|
244
|
-
cache_control:
|
|
264
|
+
cache_control: DEFAULT_CACHE_CONTROL,
|
|
245
265
|
last_modified: target[:last_modified_date],
|
|
246
266
|
etag: target[:etag]
|
|
247
267
|
}
|
|
@@ -299,63 +319,9 @@ module Syntropy
|
|
|
299
319
|
# @param route [Hash] route entry
|
|
300
320
|
# @return [Proc] route proc
|
|
301
321
|
def markdown_route_proc(route)
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
req.respond_by_http_method(
|
|
306
|
-
'head' => [nil, headers],
|
|
307
|
-
'get' => -> { [render_markdown(route), headers] }
|
|
308
|
-
)
|
|
309
|
-
}
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
# Renders and returns the given markdown route as HTML.
|
|
313
|
-
#
|
|
314
|
-
# @param route [Hash] route entry
|
|
315
|
-
# @return [String] rendered HTML
|
|
316
|
-
def render_markdown(route)
|
|
317
|
-
atts, md = Syntropy::Markdown.parse(route[:target][:fn], @env)
|
|
318
|
-
|
|
319
|
-
layout = compute_markdown_layout(route, atts)
|
|
320
|
-
Papercraft.html(layout, md:, **atts)
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
def compute_markdown_layout(route, atts)
|
|
324
|
-
if (layout = atts[:layout])
|
|
325
|
-
route[:applied_layouts] ||= {}
|
|
326
|
-
route[:applied_layouts][layout] ||= markdown_layout_template(layout)
|
|
327
|
-
else
|
|
328
|
-
default_markdown_layout_template
|
|
329
|
-
end
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
# Returns a markdown template based on the given layout.
|
|
333
|
-
#
|
|
334
|
-
# @param layout [String] layout name
|
|
335
|
-
# @return [Proc] layout template
|
|
336
|
-
def markdown_layout_template(layout)
|
|
337
|
-
@layouts ||= {}
|
|
338
|
-
template = @module_loader.load("_layout/#{layout}")
|
|
339
|
-
@layouts[layout] = Papercraft.apply(template) { |md:, **| markdown(md) }
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
# Returns the default markdown layout, which renders to HTML and includes a
|
|
343
|
-
# title, the markdown content, and emits code for auto refreshing the page
|
|
344
|
-
# on file change.
|
|
345
|
-
#
|
|
346
|
-
# @return [Proc] default Markdown layout template
|
|
347
|
-
def default_markdown_layout_template
|
|
348
|
-
@default_markdown_layout ||= ->(md:, **atts) {
|
|
349
|
-
html5 {
|
|
350
|
-
head {
|
|
351
|
-
title atts[:title]
|
|
352
|
-
}
|
|
353
|
-
body {
|
|
354
|
-
markdown md
|
|
355
|
-
auto_refresh! if Syntropy.dev_mode
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
322
|
+
env = @env.merge(module_loader: @module_loader)
|
|
323
|
+
atts, md = Syntropy::Markdown.parse_file(route[:target][:fn], env)
|
|
324
|
+
Syntropy::Markdown.make_controller(env, atts, md)
|
|
359
325
|
end
|
|
360
326
|
|
|
361
327
|
# Returns the route proc for a module route.
|
|
@@ -46,15 +46,14 @@ module Syntropy
|
|
|
46
46
|
|
|
47
47
|
# Returns a list of parsed markdown pages at the given path.
|
|
48
48
|
#
|
|
49
|
-
# @param env [Hash] app environment hash
|
|
50
49
|
# @param ref [String] directory path
|
|
51
50
|
# @return [Array<Hash>] array of page entries
|
|
52
|
-
def page_list(
|
|
53
|
-
full_path = File.join(env[:app_root], ref)
|
|
51
|
+
def page_list(ref)
|
|
52
|
+
full_path = File.join(@env[:app_root], ref)
|
|
54
53
|
raise 'Not a directory' if !File.directory?(full_path)
|
|
55
54
|
|
|
56
55
|
Dir[File.join(full_path, '*.md')].sort.map {
|
|
57
|
-
atts, markdown = Syntropy::Markdown.
|
|
56
|
+
atts, markdown = Syntropy::Markdown.parse_file(it, @env)
|
|
58
57
|
{ atts:, markdown: }
|
|
59
58
|
}
|
|
60
59
|
end
|
|
@@ -66,24 +65,6 @@ module Syntropy
|
|
|
66
65
|
Syntropy::App.new(**)
|
|
67
66
|
end
|
|
68
67
|
|
|
69
|
-
BUILTIN_APPLET_app_root = File.expand_path(File.join(__dir__, 'applets/builtin'))
|
|
70
|
-
|
|
71
|
-
# Creates a builtin applet with the given environment hash. By default the
|
|
72
|
-
# builtin applet is mounted at /.syntropy.
|
|
73
|
-
#
|
|
74
|
-
# @param env [Hash] app environment
|
|
75
|
-
# @param mount_path [String] mount path for the builtin applet
|
|
76
|
-
# @return [Syntropy::App] applet
|
|
77
|
-
def builtin_applet(env, mount_path: '/.syntropy')
|
|
78
|
-
app(
|
|
79
|
-
machine: env[:machine],
|
|
80
|
-
app_root: BUILTIN_APPLET_app_root,
|
|
81
|
-
mount_path: mount_path,
|
|
82
|
-
builtin_applet_path: nil,
|
|
83
|
-
watch_files: nil
|
|
84
|
-
)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
68
|
private
|
|
88
69
|
|
|
89
70
|
# Finds sites in the root directory for the given environment hash, adds
|
data/lib/syntropy/markdown.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'yaml'
|
|
4
|
+
require 'securerandom'
|
|
4
5
|
|
|
5
6
|
module Syntropy
|
|
6
7
|
# Markdown parsing.
|
|
@@ -11,30 +12,125 @@ module Syntropy
|
|
|
11
12
|
symbolize_names: true
|
|
12
13
|
}.freeze
|
|
13
14
|
|
|
15
|
+
class Controller
|
|
16
|
+
def initialize(env, atts, md)
|
|
17
|
+
@env = env
|
|
18
|
+
@atts = atts
|
|
19
|
+
@md = md
|
|
20
|
+
@module_loader = env[:module_loader]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_proc
|
|
24
|
+
->(req) {
|
|
25
|
+
case req.method
|
|
26
|
+
when 'head'
|
|
27
|
+
req.respond_html(nil)
|
|
28
|
+
when 'get'
|
|
29
|
+
md = process_md_embeds
|
|
30
|
+
html = render(md)
|
|
31
|
+
req.respond_html(html)
|
|
32
|
+
else
|
|
33
|
+
req.respond(nil, ':status' => HTTP::METHOD_NOT_ALLOWED)
|
|
34
|
+
end
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def process_md_embeds
|
|
41
|
+
return @md if @embedded_templates&.empty?
|
|
42
|
+
|
|
43
|
+
@embedded_templates = {}
|
|
44
|
+
@md.gsub(/^```ruby\n# render: true\n(.*?)\r?\n```\n/m) {
|
|
45
|
+
snippet = Regexp.last_match[1]
|
|
46
|
+
templ = @embedded_templates[snippet] ||= prepare_snippet_template(snippet)
|
|
47
|
+
Papercraft.html(templ)
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def prepare_snippet_template(snippet, location = nil)
|
|
52
|
+
fn = "/tmp/snippet-#{SecureRandom.hex(8)}.rb"
|
|
53
|
+
src = "->() do\n#{snippet}\nend"
|
|
54
|
+
IO.write(fn, src)
|
|
55
|
+
instance_eval src, fn
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def render(md)
|
|
59
|
+
@template ||= make_template
|
|
60
|
+
Papercraft.html(@template, md: md, **@atts)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def make_template
|
|
64
|
+
layout = make_layout
|
|
65
|
+
Papercraft.apply(layout) { |md:, **| markdown(md) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def make_layout
|
|
69
|
+
return default_layout if !@atts[:layout]
|
|
70
|
+
raise Error, 'Missing module loader' if !@module_loader
|
|
71
|
+
|
|
72
|
+
@module_loader.load("_layout/#{@atts[:layout]}")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def default_layout
|
|
76
|
+
->(**atts) {
|
|
77
|
+
html5 {
|
|
78
|
+
head {
|
|
79
|
+
title atts[:title] if atts[:title]
|
|
80
|
+
}
|
|
81
|
+
body {
|
|
82
|
+
render_children(**atts)
|
|
83
|
+
auto_refresh!
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
14
90
|
class << self
|
|
15
91
|
# Parses the markdown file at the given path.
|
|
16
92
|
#
|
|
17
93
|
# @param path [String] file path
|
|
18
94
|
# @return [Array] an tuple containing properties<Hash>, contents<String>
|
|
19
|
-
def
|
|
20
|
-
|
|
95
|
+
def parse_file(path, env)
|
|
96
|
+
md = IO.read(path) || ''
|
|
21
97
|
atts = {}
|
|
22
|
-
|
|
23
|
-
parse_date(path, atts)
|
|
24
|
-
content = parse_content(content, atts)
|
|
25
98
|
atts[:url] = path_to_url(path, env[:app_root]) if env[:app_root]
|
|
99
|
+
parse_date(atts, path)
|
|
100
|
+
|
|
101
|
+
parse_md(atts, md)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def parse_md(atts, md)
|
|
105
|
+
html = parse_content(atts, md)
|
|
106
|
+
[atts, html]
|
|
107
|
+
end
|
|
26
108
|
|
|
27
|
-
|
|
109
|
+
def make_controller(env, atts, md)
|
|
110
|
+
Controller.new(env, atts, md).to_proc
|
|
111
|
+
# layout = setup_layout_template(env, atts)
|
|
112
|
+
|
|
113
|
+
# ->(req) {
|
|
114
|
+
# case req.method
|
|
115
|
+
# when 'head'
|
|
116
|
+
# req.respond_html(nil)
|
|
117
|
+
# when 'get'
|
|
118
|
+
# html = render_md(env, atts, md)
|
|
119
|
+
# req.respond_html(html)
|
|
120
|
+
# else
|
|
121
|
+
# req.respond(nil, ':status' => HTTP::METHOD_NOT_ALLOWED)
|
|
122
|
+
# end
|
|
123
|
+
# }
|
|
28
124
|
end
|
|
29
125
|
|
|
30
126
|
private
|
|
31
127
|
|
|
32
128
|
# Parses date information from the given path.
|
|
33
129
|
#
|
|
34
|
-
# @param path [String] file path
|
|
35
130
|
# @param atts [Hash] file attributes
|
|
131
|
+
# @param path [String] file path
|
|
36
132
|
# @return [void]
|
|
37
|
-
def parse_date(
|
|
133
|
+
def parse_date(atts, path)
|
|
38
134
|
# Parse date from file name
|
|
39
135
|
if (m = path.match(/(\d{4}-\d{2}-\d{2})/))
|
|
40
136
|
atts[:date] ||= Date.parse(m[1])
|
|
@@ -43,10 +139,10 @@ module Syntropy
|
|
|
43
139
|
|
|
44
140
|
# Parses the markdown content and front matter attributes from the given content.
|
|
45
141
|
#
|
|
46
|
-
# @param content [String] file content
|
|
47
142
|
# @param atts [Hash] file attributes
|
|
143
|
+
# @param content [String] file content
|
|
48
144
|
# @return [String] parsed markdown content
|
|
49
|
-
def parse_content(
|
|
145
|
+
def parse_content(atts, content)
|
|
50
146
|
if (m = content.match(FRONT_MATTER_REGEXP))
|
|
51
147
|
front_matter = m[1]
|
|
52
148
|
content = m.post_match
|
|
@@ -63,7 +159,15 @@ module Syntropy
|
|
|
63
159
|
# @param app_root [String] app root directory
|
|
64
160
|
# @return [String] url
|
|
65
161
|
def path_to_url(path, app_root)
|
|
66
|
-
|
|
162
|
+
if app_root == '/'
|
|
163
|
+
path.gsub(/\.md$/, '')
|
|
164
|
+
else
|
|
165
|
+
path.gsub(/#{app_root}/, '').gsub(/\.md$/, '')
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def render_md(env, atts, md)
|
|
170
|
+
|
|
67
171
|
end
|
|
68
172
|
end
|
|
69
173
|
end
|