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.
- 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