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.
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This module implements an SSE (server-sent events) route, that emits a message
4
+ # to the client when a file has been changed. The module is signalled by the
5
+ # running app whenever a file change, when in watch mode (`-w`). This route
6
+ # resides by default at `/.syntropy/auto_refresh/watch.sse`.
7
+ #
8
+ # The complementary client-side party is implemented in a small JS script
9
+ # residing by default at `/.syntropy/auto_refresh/watch.js`.
10
+
11
+ # Returns a hash holding references to queues for ongoing `watch.sse` requests.
12
+ def watchers
13
+ @watchers ||= {}
14
+ end
15
+
16
+ # Signals a file change by pushing to all watcher queues.
17
+ def signal!
18
+ watchers.each_key { @machine.push(it, true) }
19
+ end
20
+
21
+ # Handles incoming requests to the `watch.sse` route. Adds a queue to the list
22
+ # of watchers, and waits for the queue to be signalled. In the absence of file
23
+ # change, a timeout occurs after one minute, and the request is terminated.
24
+ def call(req)
25
+ queue = UM::Queue.new
26
+ watchers[queue] = true
27
+
28
+ req.send_headers('Content-Type' => 'text/event-stream')
29
+ req.send_chunk("data: \n\n")
30
+ @machine.timeout(60, Timeout::Error) do
31
+ @machine.shift(queue)
32
+ req.send_chunk("data: refresh\n\n")
33
+ end
34
+ req.send_chunk("retry: 0\n\n", done: true) rescue nil
35
+ rescue Timeout::Error
36
+ req.send_chunk("retry: 0\n\n", done: true) rescue nil
37
+ rescue SystemCallError
38
+ # ignore
39
+ rescue => e
40
+ @logger&.error(
41
+ message: 'Unexpected error encountered while serving auto refresh watcher',
42
+ error: e
43
+ )
44
+ req.finish rescue nil
45
+ ensure
46
+ watchers.delete(queue)
47
+ end
48
+
49
+ export self
@@ -0,0 +1,61 @@
1
+ debug-attachment {
2
+ position: relative;
3
+ /* display: block; */
4
+ top: 0;
5
+ right: 0;
6
+ bottom: 0;
7
+ left: 0;
8
+ /* opacity: 0; */
9
+ width: 100%;
10
+ height: 100%;
11
+ }
12
+
13
+ debug-label {
14
+ display: block;
15
+ position: absolute;
16
+ top: -12px;
17
+ left: 4px;
18
+ background: rgba(0, 0, 0, 0.8);
19
+ color: white;
20
+ padding: 2px 6px;
21
+ font-size: 11px;
22
+ font-family: 'Segoe UI', Roboto, monospace;
23
+ font-weight: 500;
24
+ border-radius: 3px;
25
+ display: block;
26
+ z-index: 1001;
27
+ cursor: pointer;
28
+ /* transition: all 0.2s ease; */
29
+ white-space: nowrap;
30
+ line-height: 1.2;
31
+
32
+ a, a:hover, a:visited {
33
+ color: white;
34
+ }
35
+
36
+ &:hover {
37
+ display: block;
38
+ }
39
+
40
+ &.fn {
41
+ display: block;
42
+ }
43
+ }
44
+
45
+ body *:not(script)[data-syntropy-level]:hover {
46
+ outline: #13630c dotted 2px;
47
+ outline-offset: 2px;
48
+
49
+ /* debug-label {
50
+ display: block;
51
+ } */
52
+ }
53
+
54
+ body[data-syntropy-level="1"], body *:not(script)[data-syntropy-level="1"] {
55
+ outline: #2357aa dotted 2px;
56
+ outline-offset: 2px;
57
+ }
58
+
59
+ card {
60
+ display: block;
61
+ }
@@ -0,0 +1,57 @@
1
+ (() => {
2
+ const jsURL = import.meta.url;
3
+ const cssURL = jsURL.replace(/\.js$/, '.css');
4
+
5
+ const head = document.querySelector('head');
6
+ const link = document.createElement('link');
7
+ link.rel = 'stylesheet';
8
+ link.type = 'text/css';
9
+ link.href = cssURL;
10
+ head.appendChild(link);
11
+
12
+
13
+
14
+ let lastTarget = undefined;
15
+ document.querySelectorAll('[data-syntropy-level]').forEach((ele) => {
16
+ // ele.addEventListener('mouseover', (evt) => {
17
+ // if (evt.target != lastTarget) {
18
+
19
+ // }
20
+ // console.log(evt)
21
+ // });
22
+
23
+ // ele.addEventListener('mouseout', (evt) => {
24
+ // if (evt.target == last)
25
+ // });
26
+
27
+ const parent = ele.parentElement;
28
+ const attachment = document.createElement('debug-attachment');
29
+ const tag = ele.tagName;
30
+ if (tag == 'SCRIPT' || tag == 'HEAD') return;
31
+
32
+ const level = ele.dataset.syntropyLevel;
33
+ const href = ele.dataset.syntropyLoc;
34
+ const fn = ele.dataset.syntropyFn;
35
+
36
+ let attachToParent = false; //(tag != 'BODY');
37
+
38
+ if (level == '1') {
39
+ const cleanFn = fn.match(/([^\/]+)$/)[0];
40
+ attachment.innerHTML = `<debug-label class="fn"><a href="${href}">${cleanFn}</a></debug-label>`;
41
+ }
42
+ else {
43
+ attachToParent = true;
44
+ attachment.innerHTML = `<debug-label><a href="${href}">${tag}</a></debug-label>`;
45
+ }
46
+
47
+ // console.log(tag, attachToParent);
48
+ if (attachToParent) {
49
+ parent.style.position = 'relative';
50
+ parent.prepend(attachment);
51
+ }
52
+ else {
53
+ ele.style.position = 'relative';
54
+ ele.prepend(attachment);
55
+ }
56
+ });
57
+ })()
@@ -0,0 +1,28 @@
1
+ class JSONAPI {
2
+ constructor(url) {
3
+ this.url = url;
4
+ }
5
+
6
+ async get(q, params = {}) {
7
+ return await this.#query('get', q, params);
8
+ }
9
+
10
+ async post(q, params = {}) {
11
+ return await this.#query('post', q, params);
12
+ }
13
+
14
+ async #query(method, q, params = {}) {
15
+ const url = `${this.url}?q=${q}`;
16
+ const req = fetch(url, {
17
+ method: method
18
+ });
19
+ const response = await req;
20
+ if (!response.ok)
21
+ throw new Error(`Response status: ${response.status}`)
22
+
23
+ const result = await response.json();
24
+ return result.response;
25
+ }
26
+ }
27
+
28
+ export default JSONAPI;
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ export ->(req) { req.respond('pong') }
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ TAG_DEBUG_PROC = ->(level, fn, line, col) {
4
+ {
5
+ 'data-syntropy-level' => level,
6
+ 'data-syntropy-fn' => fn,
7
+ 'data-syntropy-loc' => "vscode://file/#{fn}:#{line}:#{col}"
8
+ }
9
+ }
10
+
11
+ P2::Compiler.html_debug_attribute_injector = TAG_DEBUG_PROC
@@ -5,7 +5,7 @@ require 'syntropy/errors'
5
5
  require 'json'
6
6
 
7
7
  module Syntropy
8
- class RPCAPI
8
+ class JSONAPI
9
9
  def initialize(env)
10
10
  @env = env
11
11
  end
@@ -6,15 +6,15 @@ module Syntropy
6
6
  # The ModuleLoader class implemenets a module loader. It handles loading of
7
7
  # modules, tracking of dependencies between modules, and invalidation of
8
8
  # loaded modules (following a change to the module file).
9
- #
9
+ #
10
10
  # A module may implement a route endpoint, a layout template, utility methods,
11
11
  # classes, or any other functionality needed by the web app.
12
- #
12
+ #
13
13
  # Modules are Ruby files that can import other modules as dependencies. A
14
14
  # module must export a single value, which can be a class, a template, a proc,
15
15
  # or any other Ruby object. A module can also export itself by calling `export
16
16
  # self`.
17
- #
17
+ #
18
18
  # Modules are referenced relative to the web app's root directory, without the
19
19
  # `.rb` extension. For example, for a site residing in `/my_site`, the
20
20
  # reference `_lib/foo` will point to a module residing in
@@ -23,7 +23,7 @@ module Syntropy
23
23
  attr_reader :modules
24
24
 
25
25
  # Instantiates a module loader
26
- #
26
+ #
27
27
  # @param env [Hash] environment hash
28
28
  # @return [void]
29
29
  def initialize(env)
@@ -34,7 +34,7 @@ module Syntropy
34
34
  end
35
35
 
36
36
  # Loads a module (if not already loaded) and returns its export value.
37
- #
37
+ #
38
38
  # @param ref [String] module reference
39
39
  # @return [any] export value
40
40
  def load(ref)
@@ -46,7 +46,7 @@ module Syntropy
46
46
  # underlying file (in order to cause reloading of the module). The module
47
47
  # will be removed from the modules map, as well as modules dependending on
48
48
  # it.
49
- #
49
+ #
50
50
  # @param fn [String] module filename
51
51
  # @return [void]
52
52
  def invalidate_fn(fn)
@@ -62,7 +62,7 @@ module Syntropy
62
62
  # underlying file (in order to cause reloading of the module). The module
63
63
  # will be removed from the modules map, as well as modules dependending on
64
64
  # it.
65
- #
65
+ #
66
66
  # @param ref [String] module reference
67
67
  # @return [void]
68
68
  def invalidate_ref(ref)
@@ -74,7 +74,7 @@ module Syntropy
74
74
  end
75
75
 
76
76
  # Registers reverse dependencies for the given module reference.
77
- #
77
+ #
78
78
  # @param ref [String] module reference
79
79
  # @param deps [Array<String>] array of dependencies for the given module
80
80
  # @return [void]
@@ -82,14 +82,14 @@ module Syntropy
82
82
  deps.each do
83
83
  entry = @modules[it]
84
84
  next if !entry
85
-
85
+
86
86
  entry[:reverse_deps] << ref
87
87
  end
88
88
  end
89
89
 
90
90
  # Loads a module and returns a module entry. Any dependencies (using
91
91
  # `import`) are loaded as well.
92
- #
92
+ #
93
93
  # @param ref [String] module reference
94
94
  # @return [Hash] module entry
95
95
  def load_module(ref)
@@ -112,7 +112,7 @@ module Syntropy
112
112
 
113
113
  # Transforms the given export value. If the value is nil, an exception is
114
114
  # raised.
115
- #
115
+ #
116
116
  # @param export_value [any] module's export value
117
117
  # @return [any] transformed value
118
118
  def transform_module_export_value(export_value)
@@ -132,6 +132,19 @@ module Syntropy
132
132
  # The Syntropy::Module class implements a reloadable module. A module is a
133
133
  # `.rb` source file that implements a route endpoint, a template, utility
134
134
  # methods or any other functionality needed by the web app.
135
+ #
136
+ # The following instance variables are available to modules:
137
+ #
138
+ # - `@env`: the app environment hash
139
+ # - `@machine`: a reference to the UringMachine instance
140
+ # - `@module_loader`: a reference to the module loader
141
+ # - `@app`: a reference to the app
142
+ # - `@ref`: the module's logical path (path relative to the app root)
143
+ # - `@logger`: a reference to the app's logger
144
+ #
145
+ # In addition, the module code also has access to the `MODULE` constant which
146
+ # is set to `self`, and may be used to refer to various methods defined in the
147
+ # module.
135
148
  class Module
136
149
  # Loads a module, returning the module instance
137
150
  def self.load(env, code, fn)
@@ -148,7 +161,7 @@ module Syntropy
148
161
  end
149
162
 
150
163
  # Initializes a module with the given environment hash.
151
- #
164
+ #
152
165
  # @param env [Hash] environment hash
153
166
  # @return [void]
154
167
  def initialize(**env)
@@ -157,6 +170,7 @@ module Syntropy
157
170
  @module_loader = env[:module_loader]
158
171
  @app = env[:app]
159
172
  @ref = env[:ref]
173
+ @logger = env[:logger]
160
174
  singleton_class.const_set(:MODULE, self)
161
175
  end
162
176
 
@@ -165,7 +179,7 @@ module Syntropy
165
179
  # Exports the given value. This value will be used as the module's
166
180
  # entrypoint. It can be any Ruby value, but for a route module would
167
181
  # normally be a proc.
168
- #
182
+ #
169
183
  # @param v [any] export value
170
184
  # @return [void]
171
185
  def export(v)
@@ -173,7 +187,7 @@ module Syntropy
173
187
  end
174
188
 
175
189
  # Returns the list of module references imported by the module.
176
- #
190
+ #
177
191
  # @return [Array] array of module references
178
192
  def __dependencies__
179
193
  @__dependencies__ ||= []
@@ -181,7 +195,7 @@ module Syntropy
181
195
 
182
196
  # Imports the module corresponding to the given reference. The return value
183
197
  # is the module's export value.
184
- #
198
+ #
185
199
  # @param ref [String] module reference
186
200
  # @return [any] loaded dependency's export value
187
201
  def import(ref)
@@ -191,7 +205,7 @@ module Syntropy
191
205
  end
192
206
 
193
207
  # Creates and returns a P2 template created with the given block.
194
- #
208
+ #
195
209
  # @param proc [Proc, nil] template proc or nil
196
210
  # @param block [Proc] template block
197
211
  # @return [P2::Template] template
@@ -202,8 +216,24 @@ module Syntropy
202
216
  P2::Template.new(proc)
203
217
  end
204
218
 
219
+ # Creates and returns a P2 XML template created with the given block.
220
+ #
221
+ # @param proc [Proc, nil] template proc or nil
222
+ # @param block [Proc] template block
223
+ # @return [P2::Template] template
224
+ def template_xml(proc = nil, &block)
225
+ proc ||= block
226
+ raise "No template block/proc given" if !proc
227
+
228
+ P2::Template.new(proc, mode: :xml)
229
+ rescue => e
230
+ p e
231
+ p e.backtrace
232
+ raise
233
+ end
234
+
205
235
  # Returns a list of pages found at the given ref.
206
- #
236
+ #
207
237
  # @param ref [String] directory reference
208
238
  # @return [Array] array of pages found in directory
209
239
  def page_list(ref)
@@ -211,7 +241,7 @@ module Syntropy
211
241
  end
212
242
 
213
243
  # Creates and returns a Syntropy app for the given environment.
214
- #
244
+ #
215
245
  # @param env [Hash] environment
216
246
  def app(**env)
217
247
  Syntropy::App.new(**(@env.merge(env)))
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'p2'
4
+
5
+ P2.extension(
6
+ 'auto_refresh_watch!': ->(loc = '/.syntropy') {
7
+ script(src: File.join(loc, 'auto_refresh/watch.js'), type: 'module')
8
+ },
9
+ 'debug_template!': ->(loc = '/.syntropy') {
10
+ script(src: File.join(loc, 'debug/debug.js'), type: 'module')
11
+ }
12
+ )
@@ -55,15 +55,13 @@ module Syntropy
55
55
  @dynamic_map = {}
56
56
  @env = env
57
57
  @root = compute_tree
58
- @static_map.freeze
59
- @dynamic_map.freeze
60
58
  end
61
59
 
62
60
  # Returns the generated router proc for the routing tree
63
61
  #
64
62
  # @return [Proc] router proc
65
63
  def router_proc
66
- @router_proc ||= compile_router_proc
64
+ @router_proc ||= generate_router_proc
67
65
  end
68
66
 
69
67
  # Computes a "clean" URL path for the given path. Modules and markdown are
@@ -93,6 +91,17 @@ module Syntropy
93
91
  fn.sub(/^#{Regexp.escape(@root_dir)}\//, '').sub(/\.[^\.]+$/, '')
94
92
  end
95
93
 
94
+ # Mounts the given applet on the routng tree at the given (absolute) mount
95
+ # path. This method must be called before the router proc is generated.
96
+ #
97
+ # @param path [String] absolute mount path for the applet
98
+ # @param applet [Syntropy::App, Proc] applet
99
+ # @return [void]
100
+ def mount_applet(path, applet)
101
+ path = rel_mount_path(path)
102
+ mount_applet_on_tree(@root, path, applet)
103
+ end
104
+
96
105
  private
97
106
 
98
107
  # Maps extensions to route kind.
@@ -136,6 +145,66 @@ module Syntropy
136
145
  compute_route_directory(dir: @root_dir, rel_path: '/', parent: nil)
137
146
  end
138
147
 
148
+ # Converts the given absolute path to a relative one (relative to the
149
+ # routing tree's mount path).
150
+ #
151
+ # @param path [String] absolute mount path
152
+ # @return [String] relative mount path
153
+ def rel_mount_path(path)
154
+ if @mount_path == '/'
155
+ path.sub(/^\//, '')
156
+ else
157
+ path.sub(/^#{Regexp.escape(@mount_path)}\//, '')
158
+ end
159
+ end
160
+
161
+ # Mounts the given applet as a child of the given entry. If the given
162
+ # (relative) path is nested, drills down the given entry's subtree and
163
+ # automatically creates intermediate children entries. If a child entry
164
+ # already exists for the given path, an error is raised. The given applet
165
+ # may be an instance of `Syntropy::App` or a proc.
166
+ #
167
+ # @param entry [Hash] route entry on which to mount the applet
168
+ # @param path [String] relative path
169
+ # @param applet [Syntropy::App, Proc] applet
170
+ # @return [void]
171
+ def mount_applet_on_tree(entry, path, applet)
172
+ if (m = path.match(/^([^\/]+)\/(.+)$/))
173
+ child_entry = find_or_create_child_entry(entry, m[1])
174
+ mount_applet_on_tree(child_entry, m[2], applet)
175
+ else
176
+ child_entry = entry[:children] && entry[:children][path]
177
+ raise Syntropy::Error, "Could not mount applet, entry already exists" if child_entry
178
+
179
+ applet_path = File.join(entry[:path], path)
180
+ applet_entry = {
181
+ parent: entry,
182
+ path: applet_path,
183
+ handle_subtree: true,
184
+ target: { kind: :module },
185
+ proc: applet
186
+ }
187
+
188
+ (entry[:children] ||= {})[path] = applet_entry
189
+ @dynamic_map[applet_path] = applet_entry
190
+ end
191
+ end
192
+
193
+ # Finds or creates a child entry with the given name on the given parent
194
+ # entry.
195
+ #
196
+ # @param parent [Hash] parent entry
197
+ # @param name [String] child's name
198
+ # @return [Hash] child entry
199
+ def find_or_create_child_entry(parent, name)
200
+ parent[:children] ||= {}
201
+ parent[:children][name] ||= {
202
+ parent: parent,
203
+ path: File.join(parent[:path], name),
204
+ children: {}
205
+ }
206
+ end
207
+
139
208
  # Computes a route entry for a directory.
140
209
  #
141
210
  # @param dir [String] directory path
@@ -341,10 +410,13 @@ module Syntropy
341
410
  entry[:param] ? '[]' : File.basename(entry[:path]).gsub(/\+$/, '')
342
411
  end
343
412
 
344
- # Generates and returns a router proc based on the routing tree.
413
+ # Freezes the static and dynamic maps, generates and returns a router proc
414
+ # based on the routing tree.
345
415
  #
346
416
  # @return [Proc] router proc
347
- def compile_router_proc
417
+ def generate_router_proc
418
+ @static_map.freeze
419
+ @dynamic_map.freeze
348
420
  code = generate_routing_tree_code
349
421
  eval(code, binding, '(router)', 1)
350
422
  end
@@ -37,5 +37,16 @@ module Syntropy
37
37
  def app(**env)
38
38
  Syntropy::App.new(**env)
39
39
  end
40
+
41
+ BUILTIN_APPLET_ROOT_DIR = File.expand_path(File.join(__dir__, 'applets/builtin'))
42
+ def builtin_applet(env, mount_path: '/.syntropy')
43
+ app(
44
+ machine: env[:machine],
45
+ root_dir: BUILTIN_APPLET_ROOT_DIR,
46
+ mount_path: mount_path,
47
+ builtin_applet_path: nil,
48
+ watch_files: nil
49
+ )
50
+ end
40
51
  end
41
52
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.15.1'
4
+ VERSION = '0.17'
5
5
  end
data/lib/syntropy.rb CHANGED
@@ -4,7 +4,6 @@ require 'qeweney'
4
4
  require 'uringmachine'
5
5
  require 'tp2'
6
6
  require 'p2'
7
- require 'papercraft'
8
7
 
9
8
  require 'syntropy/app'
10
9
  require 'syntropy/connection_pool'
@@ -12,8 +11,9 @@ require 'syntropy/errors'
12
11
  require 'syntropy/markdown'
13
12
  require 'syntropy/module'
14
13
  require 'syntropy/request_extensions'
14
+ require 'syntropy/p2_extensions'
15
15
  require 'syntropy/routing_tree'
16
- require 'syntropy/rpc_api'
16
+ require 'syntropy/json_api'
17
17
  require 'syntropy/side_run'
18
18
  require 'syntropy/utils'
19
19
 
@@ -47,7 +47,7 @@ module Syntropy
47
47
  " #{GREEN}ooooo\n"\
48
48
  " #{GREEN} ooo vvv #{CLEAR}Syntropy - a web framework for Ruby\n"\
49
49
  " #{GREEN} o vvvvv #{CLEAR}--------------------------------------\n"\
50
- " #{GREEN} #{YELLOW}|#{GREEN} vvv o #{CLEAR}https://github.com/noteflakes/syntropy\n"\
50
+ " #{GREEN} #{YELLOW}|#{GREEN} vvv o #{CLEAR}https://github.com/digital-fabric/syntropy\n"\
51
51
  " #{GREEN} :#{YELLOW}|#{GREEN}:::#{YELLOW}|#{GREEN}::#{YELLOW}|#{GREEN}:\n"\
52
52
  "#{YELLOW}+++++++++++++++++++++++++++++++++++++++++++++++++++++++++\e[0m\n\n"
53
53
  end
data/syntropy.gemspec CHANGED
@@ -9,11 +9,11 @@ Gem::Specification.new do |s|
9
9
  s.email = 'sharon@noteflakes.com'
10
10
  s.files = `git ls-files`.split
11
11
 
12
- s.homepage = 'https://github.com/noteflakes/syntropy'
12
+ s.homepage = 'https://github.com/digital-fabric/syntropy'
13
13
  s.metadata = {
14
- 'homepage_uri' => 'https://github.com/noteflakes/syntropy',
14
+ 'homepage_uri' => 'https://github.com/digital-fabric/syntropy',
15
15
  'documentation_uri' => 'https://www.rubydoc.info/gems/syntropy',
16
- 'changelog_uri' => 'https://github.com/noteflakes/syntropy/blob/master/CHANGELOG.md'
16
+ 'changelog_uri' => 'https://github.com/digital-fabric/syntropy/blob/master/CHANGELOG.md'
17
17
  }
18
18
  s.rdoc_options = ['--title', 'Extralite', '--main', 'README.md']
19
19
  s.extra_rdoc_files = ['README.md']
@@ -23,10 +23,9 @@ Gem::Specification.new do |s|
23
23
 
24
24
  s.add_dependency 'extralite', '2.13'
25
25
  s.add_dependency 'json', '2.13.2'
26
- s.add_dependency 'p2', '2.8'
27
- s.add_dependency 'papercraft', '1.4'
26
+ s.add_dependency 'p2', '2.13'
28
27
  s.add_dependency 'qeweney', '0.22'
29
- s.add_dependency 'tp2', '0.15'
28
+ s.add_dependency 'tp2', '0.16'
30
29
  s.add_dependency 'uringmachine', '0.18'
31
30
 
32
31
  s.add_dependency 'listen', '3.9.0'
data/test/app/api+.rb CHANGED
@@ -1,4 +1,4 @@
1
- class API < Syntropy::RPCAPI
1
+ class API < Syntropy::JSONAPI
2
2
  def initialize(env)
3
3
  super(env)
4
4
  @count = 0
data/test/app/rss.rb ADDED
@@ -0,0 +1,3 @@
1
+ export template_xml {
2
+ link 'foo'
3
+ }