syntropy 0.15.1 → 0.17
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 +21 -0
- data/README.md +37 -5
- data/TODO.md +145 -0
- data/bin/syntropy +14 -5
- data/examples/card.rb +12 -0
- data/examples/counter.js +20 -0
- data/examples/counter.rb +23 -0
- data/examples/counter_api.rb +22 -0
- data/examples/favicon.ico +0 -0
- data/examples/index.md +8 -0
- data/examples/templates.rb +40 -0
- data/lib/syntropy/app.rb +58 -22
- data/lib/syntropy/applets/builtin/auto_refresh/watch.js +12 -0
- data/lib/syntropy/applets/builtin/auto_refresh/watch.sse.rb +49 -0
- data/lib/syntropy/applets/builtin/debug/debug.css +61 -0
- data/lib/syntropy/applets/builtin/debug/debug.js +57 -0
- data/lib/syntropy/applets/builtin/json_api.js +28 -0
- data/lib/syntropy/applets/builtin/ping.rb +3 -0
- data/lib/syntropy/dev_mode.rb +11 -0
- data/lib/syntropy/{rpc_api.rb → json_api.rb} +1 -1
- data/lib/syntropy/module.rb +48 -18
- data/lib/syntropy/p2_extensions.rb +12 -0
- data/lib/syntropy/routing_tree.rb +77 -5
- data/lib/syntropy/utils.rb +11 -0
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +3 -3
- data/syntropy.gemspec +5 -6
- data/test/app/api+.rb +1 -1
- data/test/app/rss.rb +3 -0
- data/test/bm_router_proc.rb +201 -0
- data/test/test_app.rb +5 -5
- data/test/test_json_api.rb +59 -0
- data/test/test_routing_tree.rb +38 -3
- metadata +28 -25
- data/test/test_rpc_api.rb +0 -41
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: db440894351239f4fdc164cd3550d741919a8fed568f20620040cca54b7dbebd
|
4
|
+
data.tar.gz: 165d4a52b7d7ce7fb8377d7b0e96f108ef7c65cfc768cf59ec73b97a4f7c84e0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d2bb77e92b44ca5e66a46c4427a4705cb2316b42372a71ce3c1e250bf1f93bccbe9201fe9602c174a6315880f33d47338133c121871561ab31e98c5a095bb385
|
7
|
+
data.tar.gz: c1069d92fbdb6a4a9eb792fa26c16e3d3494c882eb8a8d09e211a0f16b7b880bab8e0004a74cf4d1969398c2185091282bc3a4eebbf37b325c31ae06d52d3cd0
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,24 @@
|
|
1
|
+
# 0.17 2025-09-11
|
2
|
+
|
3
|
+
- Move repo to [digital-fabric](https://github.com/digital-fabric/syntropy)
|
4
|
+
|
5
|
+
# 0.16 2025-09-11
|
6
|
+
|
7
|
+
- `syntropy` script:
|
8
|
+
- Remove trailing slash for root dir in syntropy script
|
9
|
+
- Rename `-w/--watch` option to `-d/--dev` for development mode
|
10
|
+
- Fix `--mount` option
|
11
|
+
- Add builtin `/.syntropy` applet for builtin features:
|
12
|
+
- auto refresh for web pages
|
13
|
+
- JSON API
|
14
|
+
- Template debugging frontend tools
|
15
|
+
- ping route
|
16
|
+
- home page with links to examples
|
17
|
+
- Implement applet mounting and loading
|
18
|
+
- Remove Papercraft dependency
|
19
|
+
- Add support for P2 XML templates, using `#template_xml`
|
20
|
+
- Update P2, TP2
|
21
|
+
|
1
22
|
# 0.15 2025-08-31
|
2
23
|
|
3
24
|
- Implement invalidation of reverse dependencies on module file change
|
data/README.md
CHANGED
@@ -9,10 +9,10 @@
|
|
9
9
|
<a href="http://rubygems.org/gems/syntropy">
|
10
10
|
<img src="https://badge.fury.io/rb/syntropy.svg" alt="Ruby gem">
|
11
11
|
</a>
|
12
|
-
<a href="https://github.com/
|
13
|
-
<img src="https://github.com/
|
12
|
+
<a href="https://github.com/digital-fabric/syntropy/actions">
|
13
|
+
<img src="https://github.com/digital-fabric/syntropy/actions/workflows/test.yml/badge.svg" alt="Tests">
|
14
14
|
</a>
|
15
|
-
<a href="https://github.com/
|
15
|
+
<a href="https://github.com/digital-fabric/syntropy/blob/master/LICENSE">
|
16
16
|
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License">
|
17
17
|
</a>
|
18
18
|
</p>
|
@@ -38,7 +38,7 @@ Syntropy is based on:
|
|
38
38
|
|
39
39
|
- [UringMachine](https://github.com/digital-fabric/uringmachine) - a lean mean
|
40
40
|
[io_uring](https://unixism.net/loti/what_is_io_uring.html) machine for Ruby.
|
41
|
-
- [TP2](https://github.com/
|
41
|
+
- [TP2](https://github.com/digital-fabric/tp2) - an io_uring-based web server for
|
42
42
|
concurrent Ruby apps.
|
43
43
|
- [Qeweney](https://github.com/digital-fabric/qeweney) a uniform interface for
|
44
44
|
working with HTTP requests and responses.
|
@@ -46,6 +46,16 @@ Syntropy is based on:
|
|
46
46
|
- [Extralite](https://github.com/digital-fabric/extralite) a fast and innovative
|
47
47
|
SQLite wrapper for Ruby.
|
48
48
|
|
49
|
+
## Examples
|
50
|
+
|
51
|
+
To get a taste of some of Syntropy's capabilities, you can run the included
|
52
|
+
examples site inside the Syntropy repository:
|
53
|
+
|
54
|
+
```bash
|
55
|
+
$ cd syntropy
|
56
|
+
$ bundle exec syntropy -d examples
|
57
|
+
```
|
58
|
+
|
49
59
|
## Routing
|
50
60
|
|
51
61
|
Syntropy routes request by following the tree structure of the Syntropy app. A
|
@@ -93,6 +103,28 @@ Some conventions employed in Syntropy-based web apps:
|
|
93
103
|
- The Syntrpy router accepts clean URLs for Ruby modules and Markdown files. It
|
94
104
|
also accepts clean URLs for `index.html` files.
|
95
105
|
|
106
|
+
## Running Syntropy
|
107
|
+
|
108
|
+
Note: Syntropy runs exclusively on Linux and requires kernel version >= 6.4.
|
109
|
+
|
110
|
+
To start a web server on the working directory, use the `syntropy` command:
|
111
|
+
|
112
|
+
```bash
|
113
|
+
$ # install syntropy:
|
114
|
+
$ gem install syntropy
|
115
|
+
$ # run syntropy
|
116
|
+
$ syntropy path/to/my_site
|
117
|
+
```
|
118
|
+
|
119
|
+
To get help for the different options available, run `syntropy -h`.
|
120
|
+
|
121
|
+
## Development mode
|
122
|
+
|
123
|
+
When developing and making changes to your site, you can run Syntropy in
|
124
|
+
development mode, which automatically reloads changed modules and provides tools
|
125
|
+
to automatically refresh open web pages and debug HTML templates. To start
|
126
|
+
Syntropy in development mode, run `syntropy -d path/to/my_site`.
|
127
|
+
|
96
128
|
## What does a Syntropic Ruby module look like?
|
97
129
|
|
98
130
|
Consider `site/archive.rb` in the file tree above. We want to get a list of
|
@@ -121,7 +153,7 @@ But a module can also be something completely different:
|
|
121
153
|
|
122
154
|
```ruby
|
123
155
|
# api/v1.rb
|
124
|
-
class APIV1 < Syntropy::
|
156
|
+
class APIV1 < Syntropy::JSONAPI
|
125
157
|
def initialize(db)
|
126
158
|
@db = db
|
127
159
|
end
|
data/TODO.md
CHANGED
@@ -22,6 +22,8 @@
|
|
22
22
|
article.layout #=>
|
23
23
|
article.render_proc #=> (load layout, apply article)
|
24
24
|
article.render #=> (render to HTML)
|
25
|
+
|
26
|
+
# there should also be methods for creating, updating and deleting of articles/items.
|
25
27
|
...
|
26
28
|
```
|
27
29
|
|
@@ -30,6 +32,149 @@
|
|
30
32
|
- [ ] support for caching headers
|
31
33
|
- [ ] add `Request#render_static_file(route, fn)
|
32
34
|
|
35
|
+
- [ ] Serving of built-in assets (mostly JS)
|
36
|
+
- [ ] JS lib for RPC API
|
37
|
+
|
38
|
+
## Missing for a first public release
|
39
|
+
|
40
|
+
- [ ] Logo
|
41
|
+
- [ ] Website
|
42
|
+
- [ ] Frontend part of RPC API
|
43
|
+
- [v] Auto-refresh page when file changes
|
44
|
+
- [ ] Examples
|
45
|
+
- [ ] Reactive app - counter or some other simple app showing interaction with server
|
46
|
+
- [ ] ?
|
47
|
+
|
48
|
+
## Counter example
|
49
|
+
|
50
|
+
Here's a react component (from https://fresh.deno.dev/):
|
51
|
+
|
52
|
+
```jsx
|
53
|
+
// islands/Counter.tsx
|
54
|
+
import { useSignal } from "@preact/signals";
|
55
|
+
|
56
|
+
export default function Counter(props) {
|
57
|
+
const count = useSignal(props.start);
|
58
|
+
|
59
|
+
return (
|
60
|
+
<div>
|
61
|
+
<h3>Interactive island</h3>
|
62
|
+
<p>The server supplied the initial value of {props.start}.</p>
|
63
|
+
<div>
|
64
|
+
<button onClick={() => count.value -= 1}>-</button>
|
65
|
+
<div>{count}</div>
|
66
|
+
<button onClick={() => count.value += 1}>+</button>
|
67
|
+
</div>
|
68
|
+
</div>
|
69
|
+
);
|
70
|
+
}
|
71
|
+
```
|
72
|
+
|
73
|
+
How do we do this with Syntropy? Can we make a component that does the
|
74
|
+
templating and the reactivity in a single file? Can we wrap reactivity in a
|
75
|
+
component that has its own state? And where does the state live?
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
class Counter < Syntropy::Component
|
79
|
+
def initialize(start:, **props)
|
80
|
+
@count = reactive()
|
81
|
+
end
|
82
|
+
|
83
|
+
def template
|
84
|
+
div {
|
85
|
+
h3 'Interactive island'
|
86
|
+
p "The server supplied the initial value of #props[:start]}"
|
87
|
+
div {
|
88
|
+
button '-', on_click: -> { @count.value -= 1 }
|
89
|
+
div @count.value
|
90
|
+
button '+', on_click: -> { @count.value += 1 }
|
91
|
+
}
|
92
|
+
}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
Hmm, don't know if the complexity is worth it. It's an abstraction that's very
|
98
|
+
costly - both in terms of complexity of computation, and in terms of having a
|
99
|
+
clear mental model of what's happening under the hood.
|
100
|
+
|
101
|
+
I think a more logical approach is to stay with well-defined boundaries between
|
102
|
+
computation on the frontend and computation on the backend, and a having a clear
|
103
|
+
understanding of where things happen:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
class Counter < Syntropy::Component
|
107
|
+
def incr
|
108
|
+
update(value: @props[:value] + 1)
|
109
|
+
end
|
110
|
+
|
111
|
+
def decr
|
112
|
+
update(value: @props[:value] - 1)
|
113
|
+
end
|
114
|
+
|
115
|
+
def template
|
116
|
+
div {
|
117
|
+
h3 'Interactive island'
|
118
|
+
div {
|
119
|
+
button '-', syn_click: 'decr'
|
120
|
+
div @props[:value]
|
121
|
+
button '+', syn_click: 'incr'
|
122
|
+
}
|
123
|
+
}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
Now, we can do all kinds of wrapping with scripts and ids and stuff to make this
|
129
|
+
work, but still, it would be preferable for the interactivity to be expressed in
|
130
|
+
JS. Maybe we just need a way to include a script that acts on the local HTML
|
131
|
+
code. How can we do this without writing a web component etc?
|
132
|
+
|
133
|
+
One way is to assign a random id to the template, then have a script that works
|
134
|
+
on it locally.
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
export template { |**props|
|
138
|
+
id = SecureRandom.hex(4)
|
139
|
+
div(id: id) {
|
140
|
+
h3 'Interactive island'
|
141
|
+
div {
|
142
|
+
button '-', syn_action: "decr"
|
143
|
+
div props[:value], syn_value: "value"
|
144
|
+
button '+', syn_action: "incr"
|
145
|
+
}
|
146
|
+
script <<~JS
|
147
|
+
const state = { value: #{props[:value]} }
|
148
|
+
const root = document.querySelector('##{id}')
|
149
|
+
const decr = root.querySelector('[syn-action="decr"]')
|
150
|
+
const incr = root.querySelector('[syn-action="incr"]')
|
151
|
+
const value = root.querySelector('[syn-value="value"]')
|
152
|
+
const updateValue = (v) => { state.value = v; value.innerText = String(v) }
|
153
|
+
|
154
|
+
decr.addEventListener('click', (e) => { updateValue(state.value - 1) })
|
155
|
+
incr.addEventListener('click', (e) => { updateValue(state.value + 1) })
|
156
|
+
JS
|
157
|
+
}
|
158
|
+
}
|
159
|
+
```
|
160
|
+
|
161
|
+
How can we make this less verbose, less painful, less error-prone?
|
162
|
+
|
163
|
+
One way is to say - we don't worry about this on the backend, we just write
|
164
|
+
normal JS for the frontend and forget about the whole thing. Another way is to
|
165
|
+
provide a set of tools for making this less painful:
|
166
|
+
|
167
|
+
- Add some fancier abstractions on top of the JS RPC lib
|
168
|
+
- Add some template extensions that inject JS into the generated HTML
|
169
|
+
|
170
|
+
## Testing facilities
|
171
|
+
|
172
|
+
- What do we need to test?
|
173
|
+
- Routes
|
174
|
+
- Route responses
|
175
|
+
- Changes to state / DB
|
176
|
+
-
|
177
|
+
|
33
178
|
## Support for applets
|
34
179
|
|
35
180
|
- can be implemented as separate gems
|
data/bin/syntropy
CHANGED
@@ -7,7 +7,8 @@ require 'optparse'
|
|
7
7
|
env = {
|
8
8
|
mount_path: '/',
|
9
9
|
banner: Syntropy::BANNER,
|
10
|
-
logger: true
|
10
|
+
logger: true,
|
11
|
+
builtin_applet_path: '/.syntropy'
|
11
12
|
}
|
12
13
|
|
13
14
|
parser = OptionParser.new do |o|
|
@@ -24,7 +25,8 @@ parser = OptionParser.new do |o|
|
|
24
25
|
env[:logger] = nil
|
25
26
|
end
|
26
27
|
|
27
|
-
o.on('-
|
28
|
+
o.on('-d', '--dev', 'Development mode') do
|
29
|
+
env[:dev_mode] = true
|
28
30
|
env[:watch_files] = 0.1
|
29
31
|
end
|
30
32
|
|
@@ -33,8 +35,14 @@ parser = OptionParser.new do |o|
|
|
33
35
|
exit
|
34
36
|
end
|
35
37
|
|
36
|
-
o.on('-m', '--mount', 'Set mount path (default: /)') do
|
37
|
-
|
38
|
+
o.on('-m', '--mount PATH', 'Set mount path (default: /)') do |path|
|
39
|
+
p mount: path
|
40
|
+
env[:mount_path] = path
|
41
|
+
env[:builtin_applet_path] = File.join(path, '.syntropy')
|
42
|
+
end
|
43
|
+
|
44
|
+
o.on('--no-builtin-applet', 'Do not mount builtin applet') do
|
45
|
+
env[:builtin_applet_path] = nil
|
38
46
|
end
|
39
47
|
|
40
48
|
o.on('-v', '--version', 'Show version') do
|
@@ -54,7 +62,7 @@ rescue StandardError => e
|
|
54
62
|
exit
|
55
63
|
end
|
56
64
|
|
57
|
-
env[:root_dir] = ARGV.shift || '.'
|
65
|
+
env[:root_dir] = (ARGV.shift || '.').gsub(/\/$/, '')
|
58
66
|
|
59
67
|
if !File.directory?(env[:root_dir])
|
60
68
|
puts "#{File.expand_path(env[:root_dir])} Not a directory"
|
@@ -70,6 +78,7 @@ env[:machine] = Syntropy.machine = UM.new
|
|
70
78
|
env[:logger] = env[:logger] && TP2::Logger.new(env[:machine], **env)
|
71
79
|
|
72
80
|
require 'syntropy/version'
|
81
|
+
require 'syntropy/dev_mode' if env[:dev_mode]
|
73
82
|
|
74
83
|
env[:logger]&.info(message: "Running Syntropy version #{Syntropy::VERSION}")
|
75
84
|
app = Syntropy::App.load(env)
|
data/examples/card.rb
ADDED
data/examples/counter.js
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
import JSONAPI from '/.syntropy/json_api.js'
|
2
|
+
|
3
|
+
(() => {
|
4
|
+
const api = new JSONAPI('/counter_api');
|
5
|
+
|
6
|
+
const value = document.querySelector('#value');
|
7
|
+
const decr = document.querySelector('#decr');
|
8
|
+
const incr = document.querySelector('#incr');
|
9
|
+
|
10
|
+
decr.addEventListener('click', async () => {
|
11
|
+
const result = await api.post('decr')
|
12
|
+
value.innerText = String(result);
|
13
|
+
});
|
14
|
+
|
15
|
+
incr.addEventListener('click', async () => {
|
16
|
+
const result = await api.post('incr')
|
17
|
+
value.innerText = String(result);
|
18
|
+
});
|
19
|
+
|
20
|
+
})()
|
data/examples/counter.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
CounterAPI = import 'counter_api'
|
2
|
+
|
3
|
+
export template {
|
4
|
+
html5 {
|
5
|
+
body {
|
6
|
+
p { a '< Home', href: '/' }
|
7
|
+
|
8
|
+
h1 'Counter'
|
9
|
+
|
10
|
+
div {
|
11
|
+
button '-', id: 'decr'
|
12
|
+
value CounterAPI.value, id: 'value'
|
13
|
+
button '+', id: 'incr'
|
14
|
+
}
|
15
|
+
}
|
16
|
+
script src: '/counter.js', type: 'module'
|
17
|
+
style <<~CSS
|
18
|
+
div { font-weight: bold; font-size: 1.3em }
|
19
|
+
value { display: inline-block; padding: 0 1em; color: blue; width: 1em }
|
20
|
+
CSS
|
21
|
+
auto_refresh_watch!
|
22
|
+
}
|
23
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CounterAPI < Syntropy::JSONAPI
|
4
|
+
def initialize(env)
|
5
|
+
@env = env
|
6
|
+
@value = env[:counter_value] || 0
|
7
|
+
end
|
8
|
+
|
9
|
+
def value(req = nil)
|
10
|
+
@value
|
11
|
+
end
|
12
|
+
|
13
|
+
def incr!(req)
|
14
|
+
@value += 1
|
15
|
+
end
|
16
|
+
|
17
|
+
def decr!(req)
|
18
|
+
@value -= 1
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
export CounterAPI
|
File without changes
|
data/examples/index.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
Card = import 'card'
|
2
|
+
|
3
|
+
export template {
|
4
|
+
html5 {
|
5
|
+
head {
|
6
|
+
style {
|
7
|
+
raw <<~CSS
|
8
|
+
div {
|
9
|
+
display: grid;
|
10
|
+
grid-template-columns: 1fr 1fr;
|
11
|
+
}
|
12
|
+
span.foo {
|
13
|
+
color: white;
|
14
|
+
background-color: blue;
|
15
|
+
padding: 1em;
|
16
|
+
}
|
17
|
+
span.bar {
|
18
|
+
color: white;
|
19
|
+
background-color: green;
|
20
|
+
padding: 1em;
|
21
|
+
}
|
22
|
+
CSS
|
23
|
+
}
|
24
|
+
}
|
25
|
+
body {
|
26
|
+
p { a '< Home', href: '/' }
|
27
|
+
|
28
|
+
h1 'Testing'
|
29
|
+
|
30
|
+
div {
|
31
|
+
span 'foo', class: 'foo'
|
32
|
+
span 'bar', class: 'bar'
|
33
|
+
}
|
34
|
+
|
35
|
+
Card()
|
36
|
+
}
|
37
|
+
auto_refresh_watch!
|
38
|
+
debug_template!
|
39
|
+
}
|
40
|
+
}
|
data/lib/syntropy/app.rb
CHANGED
@@ -43,6 +43,7 @@ module Syntropy
|
|
43
43
|
@root_dir = File.expand_path(env[:root_dir])
|
44
44
|
@mount_path = env[:mount_path]
|
45
45
|
@env = env
|
46
|
+
@logger = env[:logger]
|
46
47
|
|
47
48
|
@module_loader = Syntropy::ModuleLoader.new(app: self, **env)
|
48
49
|
setup_routing_tree
|
@@ -70,15 +71,22 @@ module Syntropy
|
|
70
71
|
proc = route[:proc] ||= compute_route_proc(route)
|
71
72
|
proc.(req)
|
72
73
|
rescue StandardError => e
|
73
|
-
@
|
74
|
-
message: "Error while serving request",
|
74
|
+
@logger&.error(
|
75
|
+
message: "Error while serving request: #{e.message}",
|
75
76
|
method: req.method,
|
76
|
-
path: req.path
|
77
|
+
path: req.path,
|
78
|
+
error: e
|
77
79
|
)
|
78
80
|
error_handler = get_error_handler(route)
|
79
81
|
error_handler.(req, e)
|
80
82
|
end
|
81
83
|
|
84
|
+
def route(path, params = {}, compute_proc: false)
|
85
|
+
route = @router_proc.(path, params)
|
86
|
+
route[:proc] ||= compute_route_proc(route) if compute_proc
|
87
|
+
route
|
88
|
+
end
|
89
|
+
|
82
90
|
private
|
83
91
|
|
84
92
|
# Instantiates a routing tree with the app settings, and generates a router
|
@@ -89,9 +97,16 @@ module Syntropy
|
|
89
97
|
@routing_tree = Syntropy::RoutingTree.new(
|
90
98
|
root_dir: @root_dir, mount_path: @mount_path, **@env
|
91
99
|
)
|
100
|
+
mount_builtin_applet if @env[:builtin_applet_path]
|
92
101
|
@router_proc = @routing_tree.router_proc
|
93
102
|
end
|
94
103
|
|
104
|
+
def mount_builtin_applet
|
105
|
+
path = @env[:builtin_applet_path]
|
106
|
+
@builtin_applet ||= Syntropy.builtin_applet(@env, mount_path: path)
|
107
|
+
@routing_tree.mount_applet(path, @builtin_applet)
|
108
|
+
end
|
109
|
+
|
95
110
|
# Computes the route proc for the given route, wrapping it in hooks found up
|
96
111
|
# the routing tree.
|
97
112
|
#
|
@@ -146,9 +161,9 @@ module Syntropy
|
|
146
161
|
if (layout = atts[:layout])
|
147
162
|
route[:applied_layouts] ||= {}
|
148
163
|
proc = route[:applied_layouts][layout] ||= markdown_layout_proc(layout)
|
149
|
-
html = proc.render(md
|
164
|
+
html = proc.render(md:, **atts)
|
150
165
|
else
|
151
|
-
html =
|
166
|
+
html = default_markdown_layout_proc.render(md:, **atts)
|
152
167
|
end
|
153
168
|
html
|
154
169
|
end
|
@@ -160,9 +175,22 @@ module Syntropy
|
|
160
175
|
@layouts[layout] = template.apply { |md:, **| markdown(md) }
|
161
176
|
end
|
162
177
|
|
178
|
+
def default_markdown_layout_proc
|
179
|
+
@default_markdown_layout ||= ->(md:, **atts) {
|
180
|
+
html5 {
|
181
|
+
head {
|
182
|
+
title atts[:title]
|
183
|
+
}
|
184
|
+
body {
|
185
|
+
markdown md
|
186
|
+
auto_refresh_watch! if @env[:dev_mode]
|
187
|
+
}
|
188
|
+
}
|
189
|
+
}
|
190
|
+
end
|
191
|
+
|
163
192
|
def module_route_proc(route)
|
164
193
|
ref = @routing_tree.fn_to_rel_path(route[:target][:fn])
|
165
|
-
# ref = route[:target][:fn].sub(@mount_path, '')
|
166
194
|
mod = @module_loader.load(ref)
|
167
195
|
compute_module_proc(mod)
|
168
196
|
end
|
@@ -171,31 +199,23 @@ module Syntropy
|
|
171
199
|
case mod
|
172
200
|
when P2::Template
|
173
201
|
p2_template_proc(mod)
|
174
|
-
when Papercraft::Template
|
175
|
-
papercraft_template_proc(mod)
|
176
202
|
else
|
177
203
|
mod
|
178
204
|
end
|
179
205
|
end
|
180
206
|
|
181
207
|
def p2_template_proc(template)
|
208
|
+
xml_mode = template.mode == :xml
|
182
209
|
template = template.proc
|
183
|
-
|
210
|
+
mime_type = xml_mode ? 'text/xml; charset=UTF-8' : 'text/html; charset=UTF-8'
|
211
|
+
headers = { 'Content-Type' => mime_type }
|
184
212
|
|
185
|
-
->
|
186
|
-
req.respond_by_http_method(
|
187
|
-
'head' => [nil, headers],
|
188
|
-
'get' => -> { [template.render, headers] }
|
189
|
-
)
|
190
|
-
}
|
191
|
-
end
|
213
|
+
get_proc = xml_mode ? -> { [template.render_xml, headers] } : -> { [template.render, headers] }
|
192
214
|
|
193
|
-
def papercraft_template_proc(template)
|
194
|
-
headers = { 'Content-Type' => template.mime_type }
|
195
215
|
->(req) {
|
196
216
|
req.respond_by_http_method(
|
197
217
|
'head' => [nil, headers],
|
198
|
-
'get' =>
|
218
|
+
'get' => get_proc
|
199
219
|
)
|
200
220
|
}
|
201
221
|
end
|
@@ -232,7 +252,7 @@ module Syntropy
|
|
232
252
|
DEFAULT_ERROR_HANDLER = ->(req, err) {
|
233
253
|
msg = err.message
|
234
254
|
msg = nil if msg.empty? || (req.method == 'head')
|
235
|
-
req.respond(msg, ':status' => Syntropy::Error.http_status(err))
|
255
|
+
req.respond(msg, ':status' => Syntropy::Error.http_status(err)) rescue nil
|
236
256
|
}
|
237
257
|
|
238
258
|
# Returns an error handler for the given route. If route is nil, looks up
|
@@ -286,7 +306,7 @@ module Syntropy
|
|
286
306
|
# setup tasks
|
287
307
|
@machine.sleep 0.2
|
288
308
|
route_count = @routing_tree.static_map.size + @routing_tree.dynamic_map.size
|
289
|
-
@
|
309
|
+
@logger&.info(
|
290
310
|
message: "Serving from #{@root_dir} (#{route_count} routes found)"
|
291
311
|
)
|
292
312
|
|
@@ -302,7 +322,7 @@ module Syntropy
|
|
302
322
|
wf = @env[:watch_files]
|
303
323
|
period = wf.is_a?(Numeric) ? wf : 0.1
|
304
324
|
Syntropy.file_watch(@machine, @root_dir, period: period) do |event, fn|
|
305
|
-
@
|
325
|
+
@logger&.info(message: 'File change detected', fn: fn)
|
306
326
|
@module_loader.invalidate_fn(fn)
|
307
327
|
debounce_file_change
|
308
328
|
end
|
@@ -324,7 +344,23 @@ module Syntropy
|
|
324
344
|
@machine.sleep(0.1)
|
325
345
|
setup_routing_tree
|
326
346
|
@routing_tree_reloader = nil
|
347
|
+
signal_auto_refresh_watchers!
|
327
348
|
end
|
328
349
|
end
|
350
|
+
|
351
|
+
def signal_auto_refresh_watchers!
|
352
|
+
return if !@builtin_applet
|
353
|
+
|
354
|
+
watcher_route_path = File.join(@env[:builtin_applet_path], 'auto_refresh/watch.sse')
|
355
|
+
watcher_route = @builtin_applet.route(watcher_route_path, compute_proc: true)
|
356
|
+
|
357
|
+
watcher_mod = watcher_route[:proc]
|
358
|
+
watcher_mod.signal!
|
359
|
+
rescue => e
|
360
|
+
@logger&.error(
|
361
|
+
message: 'Unexpected error while signalling auto refresh watcher',
|
362
|
+
error: e
|
363
|
+
)
|
364
|
+
end
|
329
365
|
end
|
330
366
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
(() => {
|
2
|
+
const jsURL = import.meta.url;
|
3
|
+
const sseURL = jsURL.replace(/\.js$/, '.sse');
|
4
|
+
const eventSource = new EventSource(sseURL);
|
5
|
+
|
6
|
+
eventSource.addEventListener('message', (msg) => {
|
7
|
+
if (msg.data != '') window.location.reload();
|
8
|
+
})
|
9
|
+
eventSource.addEventListener('error', () => {
|
10
|
+
console.log(`Failed to connect to auto refresh watcher (${sseURL})`);
|
11
|
+
})
|
12
|
+
})()
|