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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b55c1eaf688f84af5c848e212ab0f5686ca6d6f270a0ef68d35846824af0d716
4
- data.tar.gz: 4ca350f60c41896911dba97cd62ec05437ede55ae11d0a6c1fd0e3a68a09281e
3
+ metadata.gz: 605209154f6a44bc2c7a84934bf661de4c721a2a5ed5f3b9c21556064ac1672e
4
+ data.tar.gz: 27eb154e0be5f0b524e723355c1544541b26207f36e49eb372cea7d06e24cdd5
5
5
  SHA512:
6
- metadata.gz: a434473bf2ee80768ed88e729ecec2287af5a2596521e4fdb1857e82cff861eaeeba84ac9be5b6afba41340f6de543a7c57e1382e4a6d6e19e85960f0e6ea0c6
7
- data.tar.gz: 5ddf2319739891025fac39dfc34d50a379f2403139f6171d95d2488eabf621b22dacad1266c9151af35242dff02210aef15ccc350d3b9f1c782c0c2d585f7a8b
6
+ metadata.gz: f4244bbc93b9c53b55e486d52420653ce8ffd5ee5e8d642b91e25aafc439ea2c48b5622c84167083b93c05ef424f5ea2b5964b7a4696665e92ffab68d13a43b9
7
+ data.tar.gz: 58e28a76335c29b19f57c8b68e9c1c6bd00136b753025b230bcabf4a041a872e0efe8a8ebf233a15c7249de018662f4de9c240b27c10701989faeb71231bc866
data/.rubocop.yml CHANGED
@@ -13,6 +13,9 @@ Style/StringLiteralsInInterpolation:
13
13
  Style/Documentation:
14
14
  Enabled: false
15
15
 
16
+ Style/GlobalVars:
17
+ Enabled: false
18
+
16
19
  Layout/LineLength:
17
20
  Max: 120
18
21
 
data/CHANGELOG.md CHANGED
@@ -1,15 +1,20 @@
1
1
  ## [Planned]
2
2
 
3
- - Server-side hot reloading
4
- - Dynamic page metadata
5
- - Client-side component rendering
6
- - Client-side routing
7
- - Websockets support
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 based to remove code duplication. Essentially any markup that has behaviour attached or would otherwise create code duplication should probably be in a Components.
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
- To start a new Porous project simply `gem install porous` using whichever Ruby environment you want to use (Ruby 3.0 minimum). Then change to that directory and run:
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 `loclahost:9292`. Now you can edit `pages/home.rb` or add more pages. Files you modify will be reloaded so you can simply refresh the page in your browser. Hot-reloading will be coming later once WebSockets support is implemented.
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
@@ -8,17 +8,51 @@ module Porous
8
8
 
9
9
  namespace :build
10
10
 
11
- desc 'build', 'Build static assets'
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
- # TODO: Use Opal::Builder to generate pages, components and entities into static/dist
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
@@ -16,11 +16,32 @@ module Porous
16
16
  method_option :host,
17
17
  aliases: :h,
18
18
  type: :string,
19
- default: '127.0.0.1',
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
- Rackup::Server.start environment: 'development', builder: 'run Porous::Server.new'
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
@@ -22,14 +22,12 @@ module Porous
22
22
 
23
23
  def init_injections
24
24
  @injections ||= {}
25
- self.class.injections.each do |name, clazz|
26
- unless clazz.included_modules.include?(Porous::Injection)
27
- raise Error, "Invalid #{clazz} class, should mixin Porous::Injection"
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] = clazz
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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Porous
4
+ class Logger < Rack::CommonLogger
5
+ def log(env, status, response_headers, began_at)
6
+ super unless env['PATH_INFO'].start_with? 'static'
7
+ end
8
+ end
9
+ end
data/lib/porous/page.rb CHANGED
@@ -9,5 +9,8 @@ module Porous
9
9
  routes.route path, to: self.class
10
10
  end
11
11
  end
12
+
13
+ def page_title = 'Porous Web'
14
+ def page_description = nil
12
15
  end
13
16
  end
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] : '' # Browser.query
103
+ @props ? @props[:query] : ''
104
104
  end
105
105
 
106
106
  def path
107
- @props ? @props[:path] : '/' # @props[:path] # Browser.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
- def start_live_reload
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
- # Build a router to check for a valid route
55
- Porous::Router.new path: env['PATH_INFO'], query: env['QUERY_STRING']
56
- [200, { 'content-type' => 'text/html' },
57
- [Application.new(title: 'Porous Web', path: env['PATH_INFO'], query: env['QUERY_STRING']).to_s]]
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Porous
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.0'
5
5
  end
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/server'
29
+ require 'porous/application'
30
+ require 'porous/server' unless RUBY_ENGINE == 'opal'
28
31
 
29
32
  module Porous
30
33
  class Error < StandardError; end
@@ -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: clazz[0..-2],
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)
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Porous
4
+ class Application
5
+ include Porous::Component
6
+
7
+ inject Porous::Router, as: :router
8
+
9
+ def render
10
+ body class: 'bg-gray-50 dark:bg-gray-900' do
11
+ component router
12
+ end
13
+ end
14
+ end
15
+ end
@@ -7,10 +7,10 @@ module Porous
7
7
  new.mount_to(element)
8
8
  end
9
9
 
10
- def inject(clazz, opts = {})
11
- method_name = opts[:as] || clazz.to_s.downcase
10
+ def inject(klass, opts = {})
11
+ method_name = opts[:as] || klass.to_s.downcase
12
12
  @injections ||= {}
13
- @injections[method_name] = clazz
13
+ @injections[method_name] = klass
14
14
  end
15
15
 
16
16
  def injections
@@ -19,7 +19,7 @@ module Porous
19
19
  inject
20
20
  @virtual_dom = render_virtual_dom
21
21
  @root_node = VirtualDOM.create @virtual_dom
22
- Browser.append_child element, @root_node
22
+ element.replace_with @root_node
23
23
  self
24
24
  end
25
25
 
@@ -36,7 +36,11 @@ module Porous
36
36
  end
37
37
 
38
38
  def render!
39
- Browser.animation_frame do
39
+ if Browser::AnimationFrame.supported?
40
+ animation_frame do
41
+ @root_component.render_if_root
42
+ end
43
+ else
40
44
  @root_component.render_if_root
41
45
  end
42
46
  end
@@ -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.1.1
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-17 00:00:00.000000000 Z
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/browser.rb
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:
@@ -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