porous 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/porous/server.rb CHANGED
@@ -1,44 +1,42 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Porous
2
4
  class Server
3
- class Router
4
- include Porous::Router
5
+ def initialize(*_args)
6
+ setup_rack_app
5
7
  end
6
8
 
7
- class Application
8
- include Porous::Component
9
-
10
- def render
11
- html do
12
- head do
13
- title do
14
- text props[:title]
15
- end
16
- meta charset: 'UTF-8'
17
- meta name: 'viewport', content: 'width=device-width, initial-scale=1.0'
18
- script src: 'https://cdn.tailwindcss.com'
19
- end
20
-
21
- body class: 'bg-gray-50 dark:bg-gray-900' do
22
- component Router, props: { path: props[:path], query: props[:query] }
23
- end
24
- end
25
- end
26
- end
27
-
28
- def initialize(*args, &block)
9
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
10
+ def setup_rack_app
29
11
  @rack = Rack::Builder.new do
12
+ use Rack::ContentLength
30
13
  use Rack::Static, urls: ['/static']
14
+ use Rack::CommonLogger
15
+ use Rack::ShowExceptions
16
+ use Rack::Lint
17
+ use Rack::TempfileReaper
18
+
31
19
  run do |env|
32
- [
33
- 200,
34
- { 'content-type' => 'text/html' },
35
- [
36
- Application.new(title: 'Porous Web', path: env['PATH_INFO'], query: env['QUERY_STRING']).to_s
37
- ]
38
- ]
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
+ ]]
32
+ rescue Porous::InvalidRouteError => e
33
+ [404, { 'content-type' => 'text/plain' }, ["404 Page not found\n", e.message]]
34
+ rescue Porous::Error => e
35
+ [500, { 'content-type' => 'text/plain' }, ["500 Internal Server Error\n", e.message]]
39
36
  end
40
37
  end
41
38
  end
39
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
42
40
 
43
41
  def call(*args)
44
42
  @rack.call(*args)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Porous
4
- VERSION = "0.1.0"
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
- Opal.append_path File.expand_path('../../opal', __FILE__)
6
-
7
5
  require 'opal-virtual-dom'
6
+
7
+ Opal.append_path File.expand_path('../opal', __dir__)
8
+ Opal.append_path File.expand_path(Dir.pwd)
9
+
8
10
  require 'listen'
9
11
 
10
12
  require 'porous/version'
@@ -24,7 +26,10 @@ 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
33
+ class Error < StandardError; end
34
+ class InvalidRouteError < Error; end
30
35
  end
@@ -1,15 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module VirtualDOM
2
4
  module DOM
3
- HTML_TAGS = %w(a abbr address area article aside audio b base bdi bdo big blockquote body br
5
+ HTML_TAGS = %w[a abbr address area article aside audio b base bdi bdo big blockquote body br
4
6
  button canvas caption cite code col colgroup data datalist dd del details dfn
5
7
  dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5
6
8
  h6 head header hr html i iframe img input ins kbd keygen label legend li link
7
9
  main map mark menu menuitem meta meter nav noscript object ol optgroup option
8
10
  output p param picture pre progress q rp rt ruby s samp script section select
9
11
  small source span strong style sub summary sup table tbody td textarea tfoot th
10
- thead time title tr track u ul var video wbr)
12
+ thead time title tr track u ul var video wbr].freeze
11
13
 
12
- SVG_TAGS = %w(svg path)
14
+ SVG_TAGS = %w[svg path].freeze
13
15
 
14
16
  (HTML_TAGS + SVG_TAGS).each do |tag|
15
17
  define_method tag do |params = {}, &block|
@@ -38,7 +40,8 @@ module VirtualDOM
38
40
  self
39
41
  end
40
42
 
41
- def method_missing(clazz, params = {}, &block)
43
+ # rubocop:disable Style/MissingRespondToMissing, Metrics/MethodLength, Metrics/AbcSize
44
+ def method_missing(klass, params = {}, &block)
42
45
  return unless @__last_virtual_node__
43
46
  return unless @__virtual_nodes__
44
47
 
@@ -51,28 +54,29 @@ module VirtualDOM
51
54
  end
52
55
 
53
56
  class_params = @__last_virtual_node__.params.delete(:className)
54
- method_params = if clazz.end_with?('!')
55
- { id: clazz[0..-2],
57
+ method_params = if klass.end_with?('!')
58
+ { id: klass[0..-2],
56
59
  class: merge_string(class_params, params[:class]) }
57
60
  else
58
- { class: merge_string(class_params, params[:class], clazz.gsub('_', '-').gsub('--', '_')) }
61
+ { class: merge_string(class_params, params[:class], klass.to_s.gsub('_', '-').gsub('--', '_')) }
59
62
  end
60
63
  params = @__last_virtual_node__.params.merge(params).merge(method_params)
61
64
  process_tag(@__last_virtual_node__.name, params, block, children)
62
65
  end
66
+ # rubocop:enable Style/MissingRespondToMissing, Metrics/MethodLength, Metrics/AbcSize
63
67
 
64
68
  def merge_string(*params)
65
69
  arr = []
66
70
  params.each do |string|
67
71
  next unless string
68
72
 
69
- arr << string.split(' ')
73
+ arr << string.split
70
74
  end
71
75
  arr.join(' ')
72
76
  end
73
77
 
74
78
  def process_params(params)
75
- params.dup.each do |k, v|
79
+ params.dup.each_key do |k|
76
80
  case k
77
81
  when 'for'
78
82
  params['htmlFor'] = params.delete('for')
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module VirtualDOM
2
4
  class VirtualNode
3
5
  attr_reader :name, :params, :children
@@ -15,7 +17,7 @@ module VirtualDOM
15
17
  def to_s_params
16
18
  return unless @params.any?
17
19
 
18
- ' ' + @params.map { |k, v| "#{k}=\"#{v}\"" }.join(' ')
20
+ " #{@params.map { |k, v| "#{k}=\"#{v}\"" }.join(" ")}"
19
21
  end
20
22
 
21
23
  def to_s_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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Porous
2
4
  module Component
3
5
  module ClassMethods
@@ -5,10 +7,10 @@ module Porous
5
7
  new.mount_to(element)
6
8
  end
7
9
 
8
- def inject(clazz, opts = {})
9
- method_name = opts[:as] || clazz.to_s.downcase
10
+ def inject(klass, opts = {})
11
+ method_name = opts[:as] || klass.to_s.downcase
10
12
  @injections ||= {}
11
- @injections[method_name] = clazz
13
+ @injections[method_name] = klass
12
14
  end
13
15
 
14
16
  def injections
@@ -1,27 +1,30 @@
1
- odule Porous
2
- module Component
3
- module Render
4
- def render
5
- raise Error, "Implement #render in #{self.class} component"
6
- end
1
+ # frozen_string_literal: true
7
2
 
8
- def render_if_root
9
- return unless @virtual_dom && @root_node
10
- new_virtual_dom = render_virtual_dom
11
- diff = VirtualDOM.diff @virtual_dom, new_virtual_dom
12
- VirtualDOM.patch @root_node, diff
13
- @virtual_dom = new_virtual_dom
14
- end
3
+ module Porous
4
+ module Component
5
+ module Render
6
+ def render
7
+ raise Error, "Implement #render in #{self.class} component"
8
+ end
15
9
 
16
- def before_render; end;
10
+ def render_if_root
11
+ return unless @virtual_dom && @root_node
17
12
 
18
- def render_virtual_dom
19
- before_render
20
- @cache_component_counter = 0
21
- @__virtual_nodes__ = []
22
- render
23
- to_vnode
24
- end
25
- end
26
- end
13
+ new_virtual_dom = render_virtual_dom
14
+ diff = VirtualDOM.diff @virtual_dom, new_virtual_dom
15
+ VirtualDOM.patch @root_node, diff
16
+ @virtual_dom = new_virtual_dom
17
+ end
18
+
19
+ def before_render; end
20
+
21
+ def render_virtual_dom
22
+ before_render
23
+ @cache_component_counter = 0
24
+ @__virtual_nodes__ = []
25
+ render
26
+ to_vnode
27
+ end
28
+ end
29
+ end
27
30
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Porous
2
4
  module Component
3
5
  module Virtual
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Porous
2
4
  module Component
3
5
  include VirtualDOM::DOM
@@ -17,7 +19,7 @@ module Porous
17
19
  inject
18
20
  @virtual_dom = render_virtual_dom
19
21
  @root_node = VirtualDOM.create @virtual_dom
20
- Browser.append_child element, @root_node
22
+ element.replace_with @root_node
21
23
  self
22
24
  end
23
25
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Porous
2
4
  module Injection
3
5
  def init; end
@@ -20,23 +22,25 @@ module Porous
20
22
 
21
23
  def init_injections
22
24
  @injections ||= {}
23
- self.class.injections.each do |name, clazz|
24
- if clazz.included_modules.include?(Porous::Injection)
25
- @injections[name] = clazz
26
- .new
27
- .with_root_component(@root_component)
28
- else
29
- 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"
30
28
  end
29
+
30
+ @injections[name] = klass.new.with_root_component(@root_component)
31
31
  end
32
- @injections.each do |key, instance|
32
+ @injections.each_value do |instance|
33
33
  instance.inject
34
34
  instance.init
35
35
  end
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'opal'
2
4
  require 'native'
3
5
  require 'promise'
@@ -8,3 +10,32 @@ require 'console'
8
10
 
9
11
  require 'virtual_dom'
10
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