syntropy 0.38.1 → 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 +12 -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 +6 -58
- data/lib/syntropy/controller_extensions.rb +1 -1
- data/lib/syntropy/markdown.rb +115 -11
- data/lib/syntropy/module_loader.rb +85 -56
- 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,15 @@
|
|
|
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
|
+
|
|
1
13
|
# 0.38.1 2026-06-13
|
|
2
14
|
|
|
3
15
|
- Fix builtin app, controller extensions
|
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
|
@@ -251,6 +251,8 @@ module Syntropy
|
|
|
251
251
|
}
|
|
252
252
|
end
|
|
253
253
|
|
|
254
|
+
DEFAULT_CACHE_CONTROL = 'max-age=604800' # one week
|
|
255
|
+
|
|
254
256
|
# Serves a static file from the given target hash with cache validation.
|
|
255
257
|
#
|
|
256
258
|
# @param req [Syntropy::Request] request
|
|
@@ -259,7 +261,7 @@ module Syntropy
|
|
|
259
261
|
def serve_static_file(req, target)
|
|
260
262
|
validate_static_file_info(target)
|
|
261
263
|
cache_opts = {
|
|
262
|
-
cache_control:
|
|
264
|
+
cache_control: DEFAULT_CACHE_CONTROL,
|
|
263
265
|
last_modified: target[:last_modified_date],
|
|
264
266
|
etag: target[:etag]
|
|
265
267
|
}
|
|
@@ -317,63 +319,9 @@ module Syntropy
|
|
|
317
319
|
# @param route [Hash] route entry
|
|
318
320
|
# @return [Proc] route proc
|
|
319
321
|
def markdown_route_proc(route)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
req.respond_by_http_method(
|
|
324
|
-
'head' => [nil, headers],
|
|
325
|
-
'get' => -> { [render_markdown(route), headers] }
|
|
326
|
-
)
|
|
327
|
-
}
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
# Renders and returns the given markdown route as HTML.
|
|
331
|
-
#
|
|
332
|
-
# @param route [Hash] route entry
|
|
333
|
-
# @return [String] rendered HTML
|
|
334
|
-
def render_markdown(route)
|
|
335
|
-
atts, md = Syntropy::Markdown.parse(route[:target][:fn], @env)
|
|
336
|
-
|
|
337
|
-
layout = compute_markdown_layout(route, atts)
|
|
338
|
-
Papercraft.html(layout, md:, **atts)
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
def compute_markdown_layout(route, atts)
|
|
342
|
-
if (layout = atts[:layout])
|
|
343
|
-
route[:applied_layouts] ||= {}
|
|
344
|
-
route[:applied_layouts][layout] ||= markdown_layout_template(layout)
|
|
345
|
-
else
|
|
346
|
-
default_markdown_layout_template
|
|
347
|
-
end
|
|
348
|
-
end
|
|
349
|
-
|
|
350
|
-
# Returns a markdown template based on the given layout.
|
|
351
|
-
#
|
|
352
|
-
# @param layout [String] layout name
|
|
353
|
-
# @return [Proc] layout template
|
|
354
|
-
def markdown_layout_template(layout)
|
|
355
|
-
@layouts ||= {}
|
|
356
|
-
template = @module_loader.load("_layout/#{layout}")
|
|
357
|
-
@layouts[layout] = Papercraft.apply(template) { |md:, **| markdown(md) }
|
|
358
|
-
end
|
|
359
|
-
|
|
360
|
-
# Returns the default markdown layout, which renders to HTML and includes a
|
|
361
|
-
# title, the markdown content, and emits code for auto refreshing the page
|
|
362
|
-
# on file change.
|
|
363
|
-
#
|
|
364
|
-
# @return [Proc] default Markdown layout template
|
|
365
|
-
def default_markdown_layout_template
|
|
366
|
-
@default_markdown_layout ||= ->(md:, **atts) {
|
|
367
|
-
html5 {
|
|
368
|
-
head {
|
|
369
|
-
title atts[:title]
|
|
370
|
-
}
|
|
371
|
-
body {
|
|
372
|
-
markdown md
|
|
373
|
-
auto_refresh! if Syntropy.dev_mode
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
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)
|
|
377
325
|
end
|
|
378
326
|
|
|
379
327
|
# Returns the route proc for a module route.
|
|
@@ -53,7 +53,7 @@ module Syntropy
|
|
|
53
53
|
raise 'Not a directory' if !File.directory?(full_path)
|
|
54
54
|
|
|
55
55
|
Dir[File.join(full_path, '*.md')].sort.map {
|
|
56
|
-
atts, markdown = Syntropy::Markdown.
|
|
56
|
+
atts, markdown = Syntropy::Markdown.parse_file(it, @env)
|
|
57
57
|
{ atts:, markdown: }
|
|
58
58
|
}
|
|
59
59
|
end
|
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
|