porous 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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