porous 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +18 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +69 -0
- data/Rakefile +12 -0
- data/exe/porous +4 -0
- data/lib/porous/cli/build.rb +22 -0
- data/lib/porous/cli/new.rb +29 -0
- data/lib/porous/cli/server.rb +26 -0
- data/lib/porous/cli/template/.gitignore.tt +1 -0
- data/lib/porous/cli/template/README.md.tt +7 -0
- data/lib/porous/cli/template/pages/home.rb +48 -0
- data/lib/porous/cli/template/static/hero.png +0 -0
- data/lib/porous/cli.rb +10 -0
- data/lib/porous/component/class_methods.rb +15 -0
- data/lib/porous/component/render.rb +19 -0
- data/lib/porous/component/virtual.rb +18 -0
- data/lib/porous/component.rb +34 -0
- data/lib/porous/injection.rb +38 -0
- data/lib/porous/page.rb +11 -0
- data/lib/porous/router.rb +113 -0
- data/lib/porous/routes.rb +86 -0
- data/lib/porous/server.rb +47 -0
- data/lib/porous/version.rb +5 -0
- data/lib/porous.rb +30 -0
- data/lib/virtual_dom/dom.rb +113 -0
- data/lib/virtual_dom/virtual_node.rb +30 -0
- data/opal/porous/browser.rb +37 -0
- data/opal/porous/component/class_methods.rb +19 -0
- data/opal/porous/component/render.rb +27 -0
- data/opal/porous/component/virtual.rb +37 -0
- data/opal/porous/component.rb +31 -0
- data/opal/porous/injection.rb +44 -0
- data/opal/porous.rb +10 -0
- data/sig/porous.rbs +4 -0
- metadata +212 -0
@@ -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
|
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
data/sig/porous.rbs
ADDED