porous 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +11 -6
- data/README.md +16 -6
- data/lib/porous/application.rb +33 -0
- data/lib/porous/cli/build.rb +37 -3
- data/lib/porous/cli/server.rb +23 -2
- data/lib/porous/cli/template/pages/home.rb +3 -1
- data/lib/porous/injection.rb +4 -6
- data/lib/porous/logger.rb +9 -0
- data/lib/porous/page.rb +3 -0
- data/lib/porous/router.rb +2 -2
- data/lib/porous/server.rb +22 -49
- data/lib/porous/version.rb +1 -1
- data/lib/porous.rb +5 -2
- data/lib/virtual_dom/dom.rb +2 -2
- data/opal/porous/application.rb +15 -0
- data/opal/porous/component/class_methods.rb +3 -3
- data/opal/porous/component.rb +1 -1
- data/opal/porous/injection.rb +5 -1
- data/opal/porous/page.rb +16 -0
- data/opal/porous/router.rb +153 -0
- data/opal/porous/routes.rb +94 -0
- data/opal/porous.rb +29 -0
- metadata +8 -3
- data/opal/porous/browser.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 605209154f6a44bc2c7a84934bf661de4c721a2a5ed5f3b9c21556064ac1672e
|
4
|
+
data.tar.gz: 27eb154e0be5f0b524e723355c1544541b26207f36e49eb372cea7d06e24cdd5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f4244bbc93b9c53b55e486d52420653ce8ffd5ee5e8d642b91e25aafc439ea2c48b5622c84167083b93c05ef424f5ea2b5964b7a4696665e92ffab68d13a43b9
|
7
|
+
data.tar.gz: 58e28a76335c29b19f57c8b68e9c1c6bd00136b753025b230bcabf4a041a872e0efe8a8ebf233a15c7249de018662f4de9c240b27c10701989faeb71231bc866
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,15 +1,20 @@
|
|
1
1
|
## [Planned]
|
2
2
|
|
3
|
-
-
|
4
|
-
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
- WebSockets support
|
4
|
+
- Production mode
|
5
|
+
|
6
|
+
## [0.2.0] - 18 February 2024
|
7
|
+
|
8
|
+
- Server-side hot reloading (browser reloads on changes)
|
9
|
+
- Dynamic page metadata (title and description)
|
10
|
+
- Less noisy logging (silence logging for Rack::Static)
|
11
|
+
- Client-side component rendering (sans SVG support)
|
12
|
+
- Client-side routing / navigation
|
8
13
|
- Client-side hot reloading
|
9
14
|
|
10
15
|
## [0.1.1] - 17 February 2024
|
11
16
|
|
12
|
-
- Server-side reloading
|
17
|
+
- Server-side reloading (server requests reflect changes)
|
13
18
|
|
14
19
|
## [0.1.0] - 15 February 2024
|
15
20
|
|
data/README.md
CHANGED
@@ -1,11 +1,19 @@
|
|
1
|
-
# Porous
|
1
|
+
# 🧽 Porous
|
2
2
|
|
3
|
-
Porous is a web engine that uses isomorphic Ruby components to build a Progressive Web App. Its use is analogous to a web framework, but the approach is entirely different.
|
3
|
+
Porous is a web engine that uses isomorphic Ruby components to build a Progressive Web App. Its use is analogous to a web framework, but the approach is entirely different. You write only
|
4
4
|
|
5
5
|
This project is a work-in-progress and is not yet even in the Proof of Concept phase. However, if you are interested in a full-stack, everything included solution, that only requires you to use one language (that is arguably easy and enjoyable to write) then feel free to follow this project.
|
6
6
|
|
7
7
|
The closest thing to this I could find was [Volt](https://github.com/voltrb/volt) or [Silica](https://github.com/youchan/silica), neither of which are active or match the overall development flow I'm looking for.
|
8
8
|
|
9
|
+
## Current Features
|
10
|
+
|
11
|
+
- 🙅 No bundled runtime (only code unique to your app needs to be in your repository)
|
12
|
+
- 🖥️ Server-side rendering (server responds with the entire initial page populated for SEO)
|
13
|
+
- 💻 Client-side rendering (application bundle is served and interactions and subsequent pages are rendered client-side)
|
14
|
+
- 🌄 Serves static files (from `static` folder)
|
15
|
+
- 🔥 Hot reloading (via HTTP polling and browser refresh)
|
16
|
+
|
9
17
|
## Design
|
10
18
|
|
11
19
|
Applications are composed of `Page`s which are in turn composed of `Component`s. Data is persisted as `Entity`s in configurable store options (memory, disk, database). Client-server communication occurs as `Event`s over WebSockets.
|
@@ -16,7 +24,7 @@ A page is conceptually similar to what would be rendered when visiting a specifi
|
|
16
24
|
|
17
25
|
### Components
|
18
26
|
|
19
|
-
A component is any composable unit of code responsible for rendering markup, potentially based on some state. This is somewhat equivalent to Web Components, in that it can also have some behaviour attached. But it can also simply be
|
27
|
+
A component is any composable unit of code responsible for rendering markup, potentially based on some state. This is somewhat equivalent to Web Components, in that it can also have some behaviour attached. But it can also simply be used to remove code duplication. Essentially any markup that has behaviour attached or would otherwise create code duplication should probably be in Components.
|
20
28
|
|
21
29
|
### Entities
|
22
30
|
|
@@ -36,11 +44,13 @@ Porous is not a framework. You don't build an application with it as a dependenc
|
|
36
44
|
|
37
45
|
Porous is still pre-alpha and so is not ready for usage yet, but the general idea is that you would define your application's entities, pages, components and events in Ruby scripts structured in a specific way. Then you would simply run `porous` while pointing it to that folder and it will spin up a Rack-compatible web server for you to use.
|
38
46
|
|
39
|
-
|
47
|
+
> ⚠️ Expect any and all APIs to change radically until version 1.0! Hence why it won't be documented or properly tested until things settle to a more stable state.
|
48
|
+
|
49
|
+
To start a new Porous project simply `gem install porous` using whichever Ruby environment you want to use (Ruby 3.0+). Then change to that directory and run:
|
40
50
|
|
41
51
|
$ porous server
|
42
52
|
|
43
|
-
By default Porous will run at `
|
53
|
+
By default Porous will run at `localhost:9292`. Now you can edit `pages/home.rb` or add more pages. Files you modify will be hot-reloaded so you can simply open the page in your browser and edit the file. Hot-reloading will be improved once WebSockets support is implemented.
|
44
54
|
|
45
55
|
### Running examples
|
46
56
|
|
@@ -66,4 +76,4 @@ Everyone interacting in the Porous project's codebases, issue trackers, chat roo
|
|
66
76
|
|
67
77
|
## Acknowledgements
|
68
78
|
|
69
|
-
I'd like to thank Michał Kalbarczyk ([fazibear](https://github.com/fazibear)) for his work done on [Inesita](https://github.com/inesita-rb/inesita) and his [VirtualDOM wrapper](https://github.com/fazibear/opal-virtual-dom) which served as the starting point for my implementation of Porous.
|
79
|
+
I'd like to thank Michał Kalbarczyk ([fazibear](https://github.com/fazibear)) for his work done on [Inesita](https://github.com/inesita-rb/inesita) and his [VirtualDOM wrapper](https://github.com/fazibear/opal-virtual-dom) which served as the starting point for my implementation of Porous. While my final approach may deviate significantly from theirs, having code to review and a workable starting point was invaluable.
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Porous
|
4
|
+
class Application
|
5
|
+
include Porous::Component
|
6
|
+
|
7
|
+
# rubocop:disable Metrics/AbcSize
|
8
|
+
def render
|
9
|
+
html do
|
10
|
+
head do
|
11
|
+
meta charset: 'UTF-8'
|
12
|
+
meta name: 'viewport', content: 'width=device-width, initial-scale=1.0'
|
13
|
+
|
14
|
+
if props[:title]
|
15
|
+
title do
|
16
|
+
text props[:title]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
meta name: 'description', content: props[:description] if props[:description]
|
20
|
+
|
21
|
+
script src: '/static/dist/application.js'
|
22
|
+
script src: '/static/dist/reload.js'
|
23
|
+
script src: 'https://cdn.tailwindcss.com'
|
24
|
+
end
|
25
|
+
|
26
|
+
body class: 'bg-gray-50 dark:bg-gray-900' do
|
27
|
+
component Porous::Router, props: { path: props[:path], query: props[:query] }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
# rubocop:enable Metrics/AbcSize
|
32
|
+
end
|
33
|
+
end
|
data/lib/porous/cli/build.rb
CHANGED
@@ -8,17 +8,51 @@ module Porous
|
|
8
8
|
|
9
9
|
namespace :build
|
10
10
|
|
11
|
-
|
11
|
+
def self.exit_on_failure?
|
12
|
+
true
|
13
|
+
end
|
12
14
|
|
15
|
+
desc 'build', 'Build static assets'
|
13
16
|
def build
|
14
|
-
empty_directory 'static/dist', force: options[:force]
|
17
|
+
empty_directory 'static/dist', verbose: false, force: options[:force]
|
15
18
|
transpile
|
19
|
+
live_reload
|
16
20
|
end
|
17
21
|
|
22
|
+
# rubocop:disable Metrics/BlockLength
|
18
23
|
no_commands do
|
19
24
|
def transpile
|
20
|
-
|
25
|
+
components = Dir.glob(File.join('{components,pages}', '**', '*.rb')).map do |relative_path|
|
26
|
+
"require '#{relative_path}'"
|
27
|
+
end
|
28
|
+
build_string = "require 'porous'; #{components.join ";"}".gsub '.rb', ''
|
29
|
+
builder = Opal::Builder.new
|
30
|
+
builder.build_str build_string, '(inline)'
|
31
|
+
File.binwrite "#{Dir.pwd}/static/dist/application.js", builder.to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
# rubocop:disable Metrics/MethodLength
|
35
|
+
def live_reload
|
36
|
+
timestamp = Time.now.to_i.to_s
|
37
|
+
File.write "#{Dir.pwd}/static/dist/timestamp", timestamp
|
38
|
+
builder = Opal::Builder.new
|
39
|
+
script = <<-BROWSER
|
40
|
+
$document.ready do
|
41
|
+
every 0.1 do
|
42
|
+
Browser::HTTP.get('/static/dist/timestamp').then do |response|
|
43
|
+
return unless response.success?
|
44
|
+
timestamp = response.text.to_i
|
45
|
+
TIMESTAMP ||= timestamp
|
46
|
+
$document.location.reload if TIMESTAMP < timestamp
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
BROWSER
|
51
|
+
builder.build_str script, '(inline)'
|
52
|
+
File.binwrite "#{Dir.pwd}/static/dist/reload.js", builder.to_s
|
21
53
|
end
|
54
|
+
# rubocop:enable Metrics/MethodLength
|
22
55
|
end
|
56
|
+
# rubocop:enable Metrics/BlockLength
|
23
57
|
end
|
24
58
|
end
|
data/lib/porous/cli/server.rb
CHANGED
@@ -16,11 +16,32 @@ module Porous
|
|
16
16
|
method_option :host,
|
17
17
|
aliases: :h,
|
18
18
|
type: :string,
|
19
|
-
default: '
|
19
|
+
default: 'localhost',
|
20
20
|
desc: 'The host address Porous will bind to'
|
21
21
|
|
22
|
+
MONITORING = %w[components pages].freeze
|
23
|
+
|
22
24
|
def server
|
23
|
-
|
25
|
+
MONITORING.each { |path| FileUtils.mkdir_p path }
|
26
|
+
build
|
27
|
+
start_live_reload
|
28
|
+
Rackup::Server.start environment: 'none', app: Porous::Server.new
|
29
|
+
end
|
30
|
+
|
31
|
+
no_commands do
|
32
|
+
def start_live_reload
|
33
|
+
opts = { only: /\.rb$/, relative: true }
|
34
|
+
@listener = Listen.to(*MONITORING, opts) do |modified, added, _removed|
|
35
|
+
# Load for server
|
36
|
+
(modified + added).each do |file|
|
37
|
+
load File.expand_path("#{Dir.pwd}/#{file}")
|
38
|
+
end
|
39
|
+
# Rebuild for browser
|
40
|
+
Thread.new { build }
|
41
|
+
end
|
42
|
+
@listener.start
|
43
|
+
at_exit { @listener.stop }
|
44
|
+
end
|
24
45
|
end
|
25
46
|
end
|
26
47
|
end
|
@@ -5,6 +5,8 @@ class Home
|
|
5
5
|
include Porous::Component
|
6
6
|
|
7
7
|
def route = '/'
|
8
|
+
def page_title = 'Porous Web | Home'
|
9
|
+
def page_description = 'Landing page generated by Porous'
|
8
10
|
|
9
11
|
# rubocop:disable Metrics, Layout/LineLength
|
10
12
|
def render
|
@@ -23,7 +25,7 @@ class Home
|
|
23
25
|
div class: 'flex flex-col items-start space-y-3 sm:space-x-4 sm:space-y-0 sm:items-center sm:flex-row' do
|
24
26
|
a href: 'https://github.com/exastencil/porous', target: '_blank', rel: 'noopener',
|
25
27
|
class: 'group relative inline-flex h-12 items-center justify-center overflow-hidden rounded-md bg-indigo-600 px-6 font-medium text-neutral-200 transition hover:scale-110' do
|
26
|
-
span 'Get Started'
|
28
|
+
span 'Get Started 🧽'
|
27
29
|
div class: 'absolute inset-0 flex h-full w-full justify-center [transform:skew(-12deg)_translateX(-100%)] group-hover:duration-1000 group-hover:[transform:skew(-12deg)_translateX(100%)]' do
|
28
30
|
div class: 'relative h-full w-8 bg-white/20'
|
29
31
|
end
|
data/lib/porous/injection.rb
CHANGED
@@ -22,14 +22,12 @@ module Porous
|
|
22
22
|
|
23
23
|
def init_injections
|
24
24
|
@injections ||= {}
|
25
|
-
self.class.injections.each do |name,
|
26
|
-
unless
|
27
|
-
raise Error, "Invalid #{
|
25
|
+
self.class.injections.each do |name, klass|
|
26
|
+
unless klass.included_modules.include?(Porous::Injection)
|
27
|
+
raise Error, "Invalid #{klass} class, should mixin Porous::Injection"
|
28
28
|
end
|
29
29
|
|
30
|
-
@injections[name] =
|
31
|
-
.new
|
32
|
-
.with_root_component(@root_component)
|
30
|
+
@injections[name] = klass.new.with_root_component(@root_component)
|
33
31
|
end
|
34
32
|
@injections.each_value do |instance|
|
35
33
|
instance.inject
|
data/lib/porous/page.rb
CHANGED
data/lib/porous/router.rb
CHANGED
@@ -100,11 +100,11 @@ module Porous
|
|
100
100
|
end
|
101
101
|
|
102
102
|
def query
|
103
|
-
@props ? @props[:query] : ''
|
103
|
+
@props ? @props[:query] : ''
|
104
104
|
end
|
105
105
|
|
106
106
|
def path
|
107
|
-
@props ? @props[:path] : '/'
|
107
|
+
@props ? @props[:path] : '/'
|
108
108
|
end
|
109
109
|
|
110
110
|
def current_url?(name)
|
data/lib/porous/server.rb
CHANGED
@@ -1,69 +1,42 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Porous
|
4
|
-
class Application
|
5
|
-
include Porous::Component
|
6
|
-
|
7
|
-
def render
|
8
|
-
html do
|
9
|
-
head do
|
10
|
-
title do
|
11
|
-
text props[:title]
|
12
|
-
end
|
13
|
-
meta charset: 'UTF-8'
|
14
|
-
meta name: 'viewport', content: 'width=device-width, initial-scale=1.0'
|
15
|
-
script src: 'https://cdn.tailwindcss.com'
|
16
|
-
end
|
17
|
-
|
18
|
-
body class: 'bg-gray-50 dark:bg-gray-900' do
|
19
|
-
component Porous::Router, props: { path: props[:path], query: props[:query] }
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
4
|
class Server
|
26
|
-
MONITORING = %w[components pages].freeze
|
27
|
-
|
28
5
|
def initialize(*_args)
|
29
|
-
@queue = Queue.new
|
30
|
-
start_live_reload
|
31
6
|
setup_rack_app
|
32
7
|
end
|
33
8
|
|
34
|
-
|
35
|
-
MONITORING.each { |path| FileUtils.mkdir_p path }
|
36
|
-
opts = {
|
37
|
-
only: /\.rb$/,
|
38
|
-
relative: true
|
39
|
-
}
|
40
|
-
@listener = Listen.to(*MONITORING, opts) do |modified, added, _removed|
|
41
|
-
(modified + added).each do |file|
|
42
|
-
load File.expand_path("#{Dir.pwd}/#{file}")
|
43
|
-
end
|
44
|
-
setup_rack_app
|
45
|
-
end
|
46
|
-
@listener.start
|
47
|
-
at_exit { @listener.stop }
|
48
|
-
end
|
49
|
-
|
9
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
50
10
|
def setup_rack_app
|
51
11
|
@rack = Rack::Builder.new do
|
12
|
+
use Rack::ContentLength
|
52
13
|
use Rack::Static, urls: ['/static']
|
14
|
+
use Rack::CommonLogger
|
15
|
+
use Rack::ShowExceptions
|
16
|
+
use Rack::Lint
|
17
|
+
use Rack::TempfileReaper
|
18
|
+
|
53
19
|
run do |env|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
20
|
+
router = Porous::Router.new path: env['PATH_INFO'], query: env['QUERY_STRING']
|
21
|
+
route = router.find_route
|
22
|
+
page = route[:component].new(route[:params])
|
23
|
+
|
24
|
+
[200, { 'content-type' => 'text/html' }, [
|
25
|
+
Porous::Application.new(
|
26
|
+
title: page.page_title,
|
27
|
+
description: page.page_description,
|
28
|
+
path: env['PATH_INFO'],
|
29
|
+
query: env['QUERY_STRING']
|
30
|
+
).to_s
|
31
|
+
]]
|
58
32
|
rescue Porous::InvalidRouteError => e
|
59
|
-
[404, { 'content-type' => 'text/plain' },
|
60
|
-
["404 Page not found\n", e.message]]
|
33
|
+
[404, { 'content-type' => 'text/plain' }, ["404 Page not found\n", e.message]]
|
61
34
|
rescue Porous::Error => e
|
62
|
-
[500, { 'content-type' => 'text/plain' },
|
63
|
-
["500 Internal Server Error\n", e.message]]
|
35
|
+
[500, { 'content-type' => 'text/plain' }, ["500 Internal Server Error\n", e.message]]
|
64
36
|
end
|
65
37
|
end
|
66
38
|
end
|
39
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
67
40
|
|
68
41
|
def call(*args)
|
69
42
|
@rack.call(*args)
|
data/lib/porous/version.rb
CHANGED
data/lib/porous.rb
CHANGED
@@ -2,9 +2,11 @@
|
|
2
2
|
|
3
3
|
require 'opal'
|
4
4
|
require 'opal-browser'
|
5
|
+
require 'opal-virtual-dom'
|
6
|
+
|
5
7
|
Opal.append_path File.expand_path('../opal', __dir__)
|
8
|
+
Opal.append_path File.expand_path(Dir.pwd)
|
6
9
|
|
7
|
-
require 'opal-virtual-dom'
|
8
10
|
require 'listen'
|
9
11
|
|
10
12
|
require 'porous/version'
|
@@ -24,7 +26,8 @@ Dir.glob(File.join('{components,pages}', '**', '*.rb')).each do |relative_path|
|
|
24
26
|
require File.expand_path("#{Dir.pwd}/#{relative_path}")
|
25
27
|
end
|
26
28
|
|
27
|
-
require 'porous/
|
29
|
+
require 'porous/application'
|
30
|
+
require 'porous/server' unless RUBY_ENGINE == 'opal'
|
28
31
|
|
29
32
|
module Porous
|
30
33
|
class Error < StandardError; end
|
data/lib/virtual_dom/dom.rb
CHANGED
@@ -55,10 +55,10 @@ module VirtualDOM
|
|
55
55
|
|
56
56
|
class_params = @__last_virtual_node__.params.delete(:className)
|
57
57
|
method_params = if klass.end_with?('!')
|
58
|
-
{ id:
|
58
|
+
{ id: klass[0..-2],
|
59
59
|
class: merge_string(class_params, params[:class]) }
|
60
60
|
else
|
61
|
-
{ class: merge_string(class_params, params[:class], klass.gsub('_', '-').gsub('--', '_')) }
|
61
|
+
{ class: merge_string(class_params, params[:class], klass.to_s.gsub('_', '-').gsub('--', '_')) }
|
62
62
|
end
|
63
63
|
params = @__last_virtual_node__.params.merge(params).merge(method_params)
|
64
64
|
process_tag(@__last_virtual_node__.name, params, block, children)
|
@@ -7,10 +7,10 @@ module Porous
|
|
7
7
|
new.mount_to(element)
|
8
8
|
end
|
9
9
|
|
10
|
-
def inject(
|
11
|
-
method_name = opts[:as] ||
|
10
|
+
def inject(klass, opts = {})
|
11
|
+
method_name = opts[:as] || klass.to_s.downcase
|
12
12
|
@injections ||= {}
|
13
|
-
@injections[method_name] =
|
13
|
+
@injections[method_name] = klass
|
14
14
|
end
|
15
15
|
|
16
16
|
def injections
|
data/opal/porous/component.rb
CHANGED
data/opal/porous/injection.rb
CHANGED
data/opal/porous/page.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Porous
|
4
|
+
module Page
|
5
|
+
# Define the route according to the Router::Routes rules
|
6
|
+
def route!
|
7
|
+
path = route
|
8
|
+
@route ||= Routes.new.tap do |routes|
|
9
|
+
routes.route path, to: self.class
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def page_title = 'Porous Web'
|
14
|
+
def page_description = nil
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Porous
|
4
|
+
# rubocop:disable Metrics/ClassLength
|
5
|
+
class Router
|
6
|
+
include Porous::Component
|
7
|
+
|
8
|
+
attr_reader :params
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@routes = Routes.new
|
12
|
+
# Extract the routes from all Pages
|
13
|
+
# Object.descendants.each { |klass| puts klass if klass.is_a?(Class) }
|
14
|
+
Object.descendants.select { |c| c.is_a?(Class) && c.included_modules.include?(Porous::Page) }.each do |klass|
|
15
|
+
# puts "Included in: #{klass}"
|
16
|
+
# next if klass.to_s.start_with? '#<Class:' # skip singleton classes
|
17
|
+
|
18
|
+
@routes.combine klass.new.route!
|
19
|
+
end
|
20
|
+
|
21
|
+
raise Error, 'No Porous::Page components found!' if @routes.routes.empty?
|
22
|
+
|
23
|
+
find_route
|
24
|
+
parse_url_params
|
25
|
+
add_listeners
|
26
|
+
end
|
27
|
+
|
28
|
+
# Handle anchor tags with router (requires router to be injected)
|
29
|
+
Component.module_eval do
|
30
|
+
unless respond_to?(:__a)
|
31
|
+
alias_method :__a, :a
|
32
|
+
define_method(:a) do |params = {}, &block|
|
33
|
+
if params['href'].include?('//') || params['target'] == '_blank'
|
34
|
+
# Retain behaviour
|
35
|
+
else
|
36
|
+
href = params['href']
|
37
|
+
params[:onclick] = lambda { |e|
|
38
|
+
e.prevent
|
39
|
+
raise Error, 'No router to handle navigation. Did you `inject Porous::Router`' unless router
|
40
|
+
|
41
|
+
router.go_to(href)
|
42
|
+
}
|
43
|
+
end
|
44
|
+
__a(params, &block)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_listeners
|
50
|
+
return unless Browser::History.supported?
|
51
|
+
|
52
|
+
$window.on(:popstate) do
|
53
|
+
find_route
|
54
|
+
parse_url_params
|
55
|
+
render!
|
56
|
+
end
|
57
|
+
$window.on(:hashchange) do
|
58
|
+
find_route
|
59
|
+
parse_url_params
|
60
|
+
render!
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def route(*params, &block)
|
65
|
+
@routes.route(*params, &block)
|
66
|
+
end
|
67
|
+
|
68
|
+
def find_route
|
69
|
+
@routes.routes.each do |route|
|
70
|
+
next unless path.match(route[:regex])
|
71
|
+
return go_to(url_for(route[:redirect_to])) if route[:redirect_to]
|
72
|
+
|
73
|
+
return @route = route
|
74
|
+
end
|
75
|
+
raise Error, "Can't find route for url"
|
76
|
+
end
|
77
|
+
|
78
|
+
def find_component(route)
|
79
|
+
call_on_enter_callback(route)
|
80
|
+
@component_props = route[:component_props]
|
81
|
+
route[:component]
|
82
|
+
end
|
83
|
+
|
84
|
+
def render
|
85
|
+
component find_component(@route), props: @component_props if @route
|
86
|
+
end
|
87
|
+
|
88
|
+
def call_on_enter_callback(route)
|
89
|
+
return unless route[:on_enter]
|
90
|
+
|
91
|
+
return unless route[:on_enter].respond_to?(:call)
|
92
|
+
|
93
|
+
route[:on_enter].call
|
94
|
+
end
|
95
|
+
|
96
|
+
def go_to(path)
|
97
|
+
$window.history.push path
|
98
|
+
find_route
|
99
|
+
parse_url_params
|
100
|
+
render!
|
101
|
+
false
|
102
|
+
end
|
103
|
+
|
104
|
+
def parse_url_params
|
105
|
+
@params = component_url_params
|
106
|
+
return if query.empty?
|
107
|
+
|
108
|
+
query[1..].split('&').each do |param|
|
109
|
+
key, value = param.split('=')
|
110
|
+
@params[Browser.decode_uri_component(key)] = Browser.decode_uri_component(value)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def component_url_params
|
115
|
+
@route[:params].zip(path.match(@route[:regex])[1..]).to_h
|
116
|
+
end
|
117
|
+
|
118
|
+
def url_for(name, params = nil)
|
119
|
+
route = @routes.routes.find do |r|
|
120
|
+
case name
|
121
|
+
when String
|
122
|
+
r[:name] == name || r[:path] == name
|
123
|
+
when Object
|
124
|
+
r[:component] == name
|
125
|
+
else
|
126
|
+
false
|
127
|
+
end
|
128
|
+
end
|
129
|
+
route ? url_with_params(route, params) : raise(Error, "Route '#{name}' not found.")
|
130
|
+
end
|
131
|
+
|
132
|
+
def query
|
133
|
+
$window.location.query
|
134
|
+
end
|
135
|
+
|
136
|
+
def path
|
137
|
+
$window.location.path
|
138
|
+
end
|
139
|
+
|
140
|
+
def current_url?(name)
|
141
|
+
path == url_for(name, params)
|
142
|
+
end
|
143
|
+
|
144
|
+
def url_with_params(route, params)
|
145
|
+
path = route[:path]
|
146
|
+
params&.each do |key, value|
|
147
|
+
path = path.gsub(":#{key}", value.to_s)
|
148
|
+
end
|
149
|
+
path
|
150
|
+
end
|
151
|
+
end
|
152
|
+
# rubocop:enable Metrics/ClassLength
|
153
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Porous
|
4
|
+
class Routes
|
5
|
+
attr_reader :routes
|
6
|
+
|
7
|
+
def initialize(parent = nil)
|
8
|
+
@parent = parent
|
9
|
+
@routes = []
|
10
|
+
end
|
11
|
+
|
12
|
+
# rubocop:disable Metrics/AbcSize
|
13
|
+
def route(*params, &block)
|
14
|
+
path = params.first.gsub(%r{^/}, '')
|
15
|
+
path = @parent ? "#{@parent}/#{path}" : "/#{path}"
|
16
|
+
|
17
|
+
add_subroutes(path, &block) if block_given?
|
18
|
+
|
19
|
+
if params.last[:redirect_to]
|
20
|
+
add_redirect(path, params.last[:redirect_to])
|
21
|
+
else
|
22
|
+
add_route(params.last[:as], path, params.last[:to], params.last[:props], params.last[:on_enter])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
# rubocop:enable Metrics/AbcSize
|
26
|
+
|
27
|
+
def validate_component(component)
|
28
|
+
raise Error, 'Component not exists' unless component
|
29
|
+
|
30
|
+
return if component.include?(Porous::Component)
|
31
|
+
|
32
|
+
raise Error,
|
33
|
+
"Invalid #{component} class, should mixin Porous::Component"
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_redirect(path, redirect_to)
|
37
|
+
@routes << {
|
38
|
+
path: path,
|
39
|
+
redirect_to: redirect_to
|
40
|
+
}.merge(build_params_and_regex(path))
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_route(name, path, component, component_props, on_enter)
|
44
|
+
validate_component(component)
|
45
|
+
@routes << {
|
46
|
+
path: path,
|
47
|
+
component: component,
|
48
|
+
component_props: component_props,
|
49
|
+
on_enter: on_enter,
|
50
|
+
name: name || component.to_s.gsub(/(.)([A-Z])/, '\1_\2').downcase
|
51
|
+
}.merge(build_params_and_regex(path))
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_subroutes(path, &block)
|
55
|
+
subroutes = Routes.new(path)
|
56
|
+
subroutes.instance_exec(&block)
|
57
|
+
@routes += subroutes.routes
|
58
|
+
end
|
59
|
+
|
60
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
61
|
+
def build_params_and_regex(path)
|
62
|
+
regex = ['^']
|
63
|
+
params = []
|
64
|
+
parts = path.split('/')
|
65
|
+
regex << '\/' if parts.empty?
|
66
|
+
parts.each do |part|
|
67
|
+
next if part.empty?
|
68
|
+
|
69
|
+
regex << '\/'
|
70
|
+
case part[0]
|
71
|
+
when ':'
|
72
|
+
params << part[1..]
|
73
|
+
regex << '([^\/]+)'
|
74
|
+
when '*'
|
75
|
+
params << part[1..]
|
76
|
+
regex << '(.*)'
|
77
|
+
break
|
78
|
+
else
|
79
|
+
regex << part
|
80
|
+
end
|
81
|
+
end
|
82
|
+
regex << '$'
|
83
|
+
{
|
84
|
+
regex: Regexp.new(regex.join),
|
85
|
+
params: params
|
86
|
+
}
|
87
|
+
end
|
88
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
89
|
+
|
90
|
+
def combine(other)
|
91
|
+
@routes += other.routes
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/opal/porous.rb
CHANGED
@@ -10,3 +10,32 @@ require 'console'
|
|
10
10
|
|
11
11
|
require 'virtual_dom'
|
12
12
|
require 'virtual_dom/support/browser'
|
13
|
+
|
14
|
+
VirtualDOM::DOM::HTML_TAGS = %w[a abbr address area article aside audio b base bdi bdo big blockquote body br
|
15
|
+
button canvas caption cite code col colgroup data datalist dd del details dfn
|
16
|
+
dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5
|
17
|
+
h6 head header hr html i iframe img input ins kbd keygen label legend li link
|
18
|
+
main map mark menu menuitem meta meter nav noscript object ol optgroup option
|
19
|
+
output p param picture pre progress q rp rt ruby s samp script section select
|
20
|
+
small source span strong style sub summary sup table tbody td textarea tfoot th
|
21
|
+
thead time title tr track u ul var video wbr svg path].freeze
|
22
|
+
|
23
|
+
require 'porous/injection'
|
24
|
+
require 'porous/component/class_methods'
|
25
|
+
require 'porous/component/render'
|
26
|
+
require 'porous/component/virtual'
|
27
|
+
require 'porous/component'
|
28
|
+
require 'porous/page'
|
29
|
+
|
30
|
+
require 'porous/routes'
|
31
|
+
require 'porous/router'
|
32
|
+
require 'porous/application'
|
33
|
+
|
34
|
+
module Porous
|
35
|
+
class Error < StandardError; end
|
36
|
+
class InvalidRouteError < Error; end
|
37
|
+
end
|
38
|
+
|
39
|
+
$document.ready do
|
40
|
+
Porous::Application.mount_to($document.body)
|
41
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: porous
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Exa Stencil
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-02-
|
11
|
+
date: 2024-02-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: listen
|
@@ -99,6 +99,7 @@ files:
|
|
99
99
|
- Rakefile
|
100
100
|
- exe/porous
|
101
101
|
- lib/porous.rb
|
102
|
+
- lib/porous/application.rb
|
102
103
|
- lib/porous/cli.rb
|
103
104
|
- lib/porous/cli/build.rb
|
104
105
|
- lib/porous/cli/new.rb
|
@@ -112,6 +113,7 @@ files:
|
|
112
113
|
- lib/porous/component/render.rb
|
113
114
|
- lib/porous/component/virtual.rb
|
114
115
|
- lib/porous/injection.rb
|
116
|
+
- lib/porous/logger.rb
|
115
117
|
- lib/porous/page.rb
|
116
118
|
- lib/porous/router.rb
|
117
119
|
- lib/porous/routes.rb
|
@@ -120,12 +122,15 @@ files:
|
|
120
122
|
- lib/virtual_dom/dom.rb
|
121
123
|
- lib/virtual_dom/virtual_node.rb
|
122
124
|
- opal/porous.rb
|
123
|
-
- opal/porous/
|
125
|
+
- opal/porous/application.rb
|
124
126
|
- opal/porous/component.rb
|
125
127
|
- opal/porous/component/class_methods.rb
|
126
128
|
- opal/porous/component/render.rb
|
127
129
|
- opal/porous/component/virtual.rb
|
128
130
|
- opal/porous/injection.rb
|
131
|
+
- opal/porous/page.rb
|
132
|
+
- opal/porous/router.rb
|
133
|
+
- opal/porous/routes.rb
|
129
134
|
- sig/porous.rbs
|
130
135
|
homepage: https://github.com/exastencil/porous
|
131
136
|
licenses:
|
data/opal/porous/browser.rb
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Porous
|
4
|
-
module Browser
|
5
|
-
module_function
|
6
|
-
|
7
|
-
Window = JS.global
|
8
|
-
Document = Window.JS[:document]
|
9
|
-
AddEventListener = Window.JS[:addEventListener]
|
10
|
-
|
11
|
-
if Native(Window.JS[:requestAnimationFrame])
|
12
|
-
def animation_frame(&block)
|
13
|
-
Window.JS.requestAnimationFrame(block)
|
14
|
-
end
|
15
|
-
else
|
16
|
-
def animation_frame(&block)
|
17
|
-
block.call
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
def ready?(&block)
|
22
|
-
AddEventListener.call('load', block)
|
23
|
-
end
|
24
|
-
|
25
|
-
def body
|
26
|
-
Document.JS[:body]
|
27
|
-
end
|
28
|
-
|
29
|
-
def append_child(node, new_node)
|
30
|
-
node = node.to_n unless native?(node)
|
31
|
-
new_node = new_node.to_n unless native?(new_node)
|
32
|
-
node.JS.appendChild(new_node)
|
33
|
-
end
|
34
|
-
|
35
|
-
def query_element(css)
|
36
|
-
Document.JS.querySelector(css)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|