porous 0.1.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.
@@ -0,0 +1,86 @@
1
+ module Porous
2
+ class Routes
3
+ attr_reader :routes
4
+
5
+ def initialize(parent = nil)
6
+ @parent = parent
7
+ @routes = []
8
+ end
9
+
10
+ def route(*params, &block)
11
+ path = params.first.gsub(/^\//, '')
12
+ path = @parent ? "#{@parent}/#{path}" : "/#{path}"
13
+
14
+ add_subroutes(path, &block) if block_given?
15
+
16
+ if params.last[:redirect_to]
17
+ add_redirect(path, params.last[:redirect_to])
18
+ else
19
+ add_route(params.last[:as], path, params.last[:to], params.last[:props], params.last[:on_enter])
20
+ end
21
+ end
22
+
23
+ def validate_component(component)
24
+ raise Error, 'Component not exists' unless component
25
+
26
+ raise Error,
27
+ "Invalid #{component} class, should mixin Porous::Component" unless component.include?(Porous::Component)
28
+ end
29
+
30
+ def add_redirect(path, redirect_to)
31
+ @routes << {
32
+ path: path,
33
+ redirect_to: redirect_to
34
+ }.merge(build_params_and_regex(path))
35
+ end
36
+
37
+ def add_route(name, path, component, component_props, on_enter)
38
+ validate_component(component)
39
+ @routes << {
40
+ path: path,
41
+ component: component,
42
+ component_props: component_props,
43
+ on_enter: on_enter,
44
+ name: name || component.to_s.gsub(/(.)([A-Z])/, '\1_\2').downcase
45
+ }.merge(build_params_and_regex(path))
46
+ end
47
+
48
+ def add_subroutes(path, &block)
49
+ subroutes = Routes.new(path)
50
+ subroutes.instance_exec(&block)
51
+ @routes += subroutes.routes
52
+ end
53
+
54
+ def build_params_and_regex(path)
55
+ regex = ['^']
56
+ params = []
57
+ parts = path.split('/')
58
+ regex << '\/' if parts.empty?
59
+ parts.each do |part|
60
+ next if part.empty?
61
+
62
+ regex << '\/'
63
+ case part[0]
64
+ when ':'
65
+ params << part[1..-1]
66
+ regex << '([^\/]+)'
67
+ when '*'
68
+ params << part[1..-1]
69
+ regex << '(.*)'
70
+ break
71
+ else
72
+ regex << part
73
+ end
74
+ end
75
+ regex << '$'
76
+ {
77
+ regex: Regexp.new(regex.join),
78
+ params: params
79
+ }
80
+ end
81
+
82
+ def combine(other)
83
+ @routes += other.routes
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,47 @@
1
+ module Porous
2
+ class Server
3
+ class Router
4
+ include Porous::Router
5
+ end
6
+
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)
29
+ @rack = Rack::Builder.new do
30
+ use Rack::Static, urls: ['/static']
31
+ 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
+ ]
39
+ end
40
+ end
41
+ end
42
+
43
+ def call(*args)
44
+ @rack.call(*args)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Porous
4
+ VERSION = "0.1.0"
5
+ end
data/lib/porous.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'opal'
4
+ require 'opal-browser'
5
+ Opal.append_path File.expand_path('../../opal', __FILE__)
6
+
7
+ require 'opal-virtual-dom'
8
+ require 'listen'
9
+
10
+ require 'porous/version'
11
+
12
+ require 'porous/injection'
13
+ require 'virtual_dom/virtual_node'
14
+ require 'virtual_dom/dom'
15
+ require 'porous/component/class_methods'
16
+ require 'porous/component/render'
17
+ require 'porous/component/virtual'
18
+ require 'porous/component'
19
+ require 'porous/page'
20
+ require 'porous/routes'
21
+ require 'porous/router'
22
+
23
+ Dir.glob(File.join('{components,pages}', '**', '*.rb')).each do |relative_path|
24
+ require File.expand_path("#{Dir.pwd}/#{relative_path}")
25
+ end
26
+
27
+ require 'porous/server'
28
+
29
+ module Porous
30
+ end
@@ -0,0 +1,113 @@
1
+ module VirtualDOM
2
+ module DOM
3
+ HTML_TAGS = %w(a abbr address area article aside audio b base bdi bdo big blockquote body br
4
+ button canvas caption cite code col colgroup data datalist dd del details dfn
5
+ dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5
6
+ h6 head header hr html i iframe img input ins kbd keygen label legend li link
7
+ main map mark menu menuitem meta meter nav noscript object ol optgroup option
8
+ output p param picture pre progress q rp rt ruby s samp script section select
9
+ 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)
11
+
12
+ SVG_TAGS = %w(svg path)
13
+
14
+ (HTML_TAGS + SVG_TAGS).each do |tag|
15
+ define_method tag do |params = {}, &block|
16
+ if params.is_a?(String)
17
+ process_tag(tag, {}, block, params)
18
+ elsif params.is_a?(Hash)
19
+ process_tag(tag, params, block)
20
+ end
21
+ end
22
+ end
23
+
24
+ def process_tag(tag, params, block, children = [])
25
+ @__virtual_nodes__ ||= []
26
+ if block
27
+ current = @__virtual_nodes__
28
+ @__virtual_nodes__ = []
29
+ result = block.call || children
30
+ vnode = VirtualNode.new(tag, process_params(params),
31
+ @__virtual_nodes__.count.zero? ? result : @__virtual_nodes__)
32
+ @__virtual_nodes__ = current
33
+ else
34
+ vnode = VirtualNode.new(tag, process_params(params), children)
35
+ end
36
+ @__last_virtual_node__ = vnode
37
+ @__virtual_nodes__ << @__last_virtual_node__
38
+ self
39
+ end
40
+
41
+ def method_missing(clazz, params = {}, &block)
42
+ return unless @__last_virtual_node__
43
+ return unless @__virtual_nodes__
44
+
45
+ @__virtual_nodes__.pop
46
+ children = []
47
+
48
+ if params.is_a?(String)
49
+ children = [params]
50
+ params = {}
51
+ end
52
+
53
+ class_params = @__last_virtual_node__.params.delete(:className)
54
+ method_params = if clazz.end_with?('!')
55
+ { id: clazz[0..-2],
56
+ class: merge_string(class_params, params[:class]) }
57
+ else
58
+ { class: merge_string(class_params, params[:class], clazz.gsub('_', '-').gsub('--', '_')) }
59
+ end
60
+ params = @__last_virtual_node__.params.merge(params).merge(method_params)
61
+ process_tag(@__last_virtual_node__.name, params, block, children)
62
+ end
63
+
64
+ def merge_string(*params)
65
+ arr = []
66
+ params.each do |string|
67
+ next unless string
68
+
69
+ arr << string.split(' ')
70
+ end
71
+ arr.join(' ')
72
+ end
73
+
74
+ def process_params(params)
75
+ params.dup.each do |k, v|
76
+ case k
77
+ when 'for'
78
+ params['htmlFor'] = params.delete('for')
79
+ when 'class'
80
+ params['className'] = params.delete('class')
81
+ when 'data'
82
+ params['dataset'] = params.delete('data')
83
+ when 'default'
84
+ params['defaultValue'] = params.delete('default')
85
+ when /^on/
86
+ # Events are ignored server-side
87
+ params.delete k
88
+ end
89
+ end
90
+ params
91
+ end
92
+
93
+ def text(string)
94
+ @__virtual_nodes__ << string.to_s
95
+ end
96
+
97
+ def to_vnode
98
+ if @__virtual_nodes__.one?
99
+ @__virtual_nodes__.first
100
+ else
101
+ VirtualNode.new('div', {}, @__virtual_nodes__)
102
+ end
103
+ end
104
+
105
+ def class_names(hash)
106
+ class_names = []
107
+ hash.each do |key, value|
108
+ class_names << key if value
109
+ end
110
+ class_names.join(' ')
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,30 @@
1
+ module VirtualDOM
2
+ class VirtualNode
3
+ attr_reader :name, :params, :children
4
+
5
+ def initialize(name, params = {}, children = [])
6
+ @name = name
7
+ @params = params
8
+ @children = children
9
+ end
10
+
11
+ def to_s
12
+ "<#{@name}#{to_s_params}>#{to_s_children}</#{@name}>"
13
+ end
14
+
15
+ def to_s_params
16
+ return unless @params.any?
17
+
18
+ ' ' + @params.map { |k, v| "#{k}=\"#{v}\"" }.join(' ')
19
+ end
20
+
21
+ def to_s_children
22
+ return @children if @children.is_a?(String)
23
+ return unless @children.any?
24
+
25
+ @children
26
+ .map(&:to_s)
27
+ .join
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,37 @@
1
+ module Porous
2
+ module Browser
3
+ module_function
4
+
5
+ Window = JS.global
6
+ Document = Window.JS[:document]
7
+ AddEventListener = Window.JS[:addEventListener]
8
+
9
+ if Native(Window.JS[:requestAnimationFrame])
10
+ def animation_frame(&block)
11
+ Window.JS.requestAnimationFrame(block)
12
+ end
13
+ else
14
+ def animation_frame(&block)
15
+ block.call
16
+ end
17
+ end
18
+
19
+ def ready?(&block)
20
+ AddEventListener.call('load', block)
21
+ end
22
+
23
+ def body
24
+ Document.JS[:body]
25
+ end
26
+
27
+ def append_child(node, new_node)
28
+ node = node.to_n unless native?(node)
29
+ new_node = new_node.to_n unless native?(new_node)
30
+ node.JS.appendChild(new_node)
31
+ end
32
+
33
+ def query_element(css)
34
+ Document.JS.querySelector(css)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,19 @@
1
+ module Porous
2
+ module Component
3
+ module ClassMethods
4
+ def mount_to(element)
5
+ new.mount_to(element)
6
+ end
7
+
8
+ def inject(clazz, opts = {})
9
+ method_name = opts[:as] || clazz.to_s.downcase
10
+ @injections ||= {}
11
+ @injections[method_name] = clazz
12
+ end
13
+
14
+ def injections
15
+ @injections || {}
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ odule Porous
2
+ module Component
3
+ module Render
4
+ def render
5
+ raise Error, "Implement #render in #{self.class} component"
6
+ end
7
+
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
15
+
16
+ def before_render; end;
17
+
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
27
+ end
@@ -0,0 +1,37 @@
1
+ module Porous
2
+ module Component
3
+ module Virtual
4
+ # Memoizes instances of sub-components for re-rendering
5
+ def cache_component(component, &block)
6
+ @cache_component ||= {}
7
+ @cache_component_counter ||= 0
8
+ @cache_component_counter += 1
9
+ cache_key = "#{component}-#{@cache_component_counter}"
10
+ @cache_component[cache_key] || @cache_component[cache_key] = block.call
11
+ end
12
+
13
+ # Used to render nested components
14
+ def component(comp, opts = {})
15
+ raise Error, "Component is nil in #{self.class} class" if comp.nil?
16
+
17
+ @__virtual_nodes__ ||= []
18
+ @__virtual_nodes__ << cache_component(comp) do
19
+ comp = (comp.is_a?(Class) ? comp.new : comp)
20
+ .with_root_component(@root_component)
21
+ .inject
22
+ comp.init
23
+ comp
24
+ end.with_props(opts[:props] || {}).render_virtual_dom
25
+ self
26
+ end
27
+
28
+ def hook(mthd)
29
+ VirtualDOM::Hook.method(method(mthd))
30
+ end
31
+
32
+ def unhook(mthd)
33
+ VirtualDOM::UnHook.method(method(mthd))
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,31 @@
1
+ module Porous
2
+ module Component
3
+ include VirtualDOM::DOM
4
+ include Virtual
5
+ include Render
6
+ include Injection
7
+
8
+ def self.included(base)
9
+ base.extend Porous::Component::ClassMethods
10
+ end
11
+
12
+ def mount_to(element)
13
+ raise Error, "Can't mount #{self.class}, target element not found!" unless element
14
+
15
+ @root_component = self
16
+ init_injections
17
+ inject
18
+ @virtual_dom = render_virtual_dom
19
+ @root_node = VirtualDOM.create @virtual_dom
20
+ Browser.append_child element, @root_node
21
+ self
22
+ end
23
+
24
+ attr_reader :props
25
+
26
+ def with_props(props)
27
+ @props = props
28
+ self
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,44 @@
1
+ module Porous
2
+ module Injection
3
+ def init; end
4
+
5
+ def with_root_component(component)
6
+ @root_component = component
7
+ self
8
+ end
9
+
10
+ def inject
11
+ @root_component.injections.each do |name, instance|
12
+ define_singleton_method(name) do
13
+ instance
14
+ end
15
+ end
16
+ self
17
+ end
18
+
19
+ attr_reader :injections
20
+
21
+ def init_injections
22
+ @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"
30
+ end
31
+ end
32
+ @injections.each do |key, instance|
33
+ instance.inject
34
+ instance.init
35
+ end
36
+ end
37
+
38
+ def render!
39
+ Browser.animation_frame do
40
+ @root_component.render_if_root
41
+ end
42
+ end
43
+ end
44
+ end
data/opal/porous.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'opal'
2
+ require 'native'
3
+ require 'promise'
4
+ require 'browser/setup/full'
5
+
6
+ require 'js'
7
+ require 'console'
8
+
9
+ require 'virtual_dom'
10
+ require 'virtual_dom/support/browser'
data/sig/porous.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Porous
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end