beryl 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b009e4e353f9efecf2cb768b1e71126f5367edaf534559c0c6ef1b9d02312d1
4
- data.tar.gz: 4e0fa2a870b6bec3f12f5b5d5e6b840571fff39e3181e8965f0c48e6d7032f4d
3
+ metadata.gz: 5b6146cdf28fe2e973b3d151fe7536034e11a3e1dec1639b7e139509ecd4e298
4
+ data.tar.gz: 5d7c85d49307cb8c427bf8f2fc593615613d6abc648d6e1dae6a2d081f46abe8
5
5
  SHA512:
6
- metadata.gz: a3834be4e596b8260b5a8d246b97e58ecfb771c74a8d0b5c6c49e8598a8cd9a5d483b7f1a5341e41a3238678c756f0ddbc23b1ae7cde8ea8af312102e214c60f
7
- data.tar.gz: 3c0ca7a52f50ba852be97f031e89cd80df1ccd056ac8e4af4b9cbcff71c86efb32fe28a8d8c5980cf2f04514f4c1db343893e3ec9d15413eb2328a44ad21fd9b
6
+ metadata.gz: 87de7c9e079ca95edf709051289c8c34a6d3f3314c46f9b877fadd5fa4fc199e783f00fd964b6d3fe39db2d48269eec0d3b14708108998fad8eba77dfc431ae5
7
+ data.tar.gz: 6aa0b0572132a48a1d7f175e386b6bc6b404a0efbbd953ea43d9b861e867d176dfc5effa950cb87b91e757f6c0144e5b7a80551a93616cce585e31c7d9ebe352
data/Gemfile CHANGED
@@ -3,4 +3,4 @@ source "https://rubygems.org"
3
3
  git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
4
 
5
5
  # Specify your gem's dependencies in beryl.gemspec
6
- gemspec
6
+ gemspec
data/Gemfile.lock CHANGED
@@ -1,3 +1,8 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ beryl (0.2.0)
5
+
1
6
  GEM
2
7
  remote: https://rubygems.org/
3
8
  specs:
@@ -5,6 +10,7 @@ GEM
5
10
  bowser (1.0.4)
6
11
  opal (>= 0.7.0, < 0.12.0)
7
12
  hike (1.2.3)
13
+ minitest (5.11.3)
8
14
  opal (0.11.3)
9
15
  ast (>= 2.3.0)
10
16
  hike (~> 1.2)
@@ -14,16 +20,21 @@ GEM
14
20
  ast (~> 2.2)
15
21
  puma (3.12.0)
16
22
  rack (2.0.5)
23
+ rake (10.5.0)
17
24
  sourcemap (0.1.1)
18
25
 
19
26
  PLATFORMS
20
27
  ruby
21
28
 
22
29
  DEPENDENCIES
23
- bowser!
24
- opal!
25
- puma!
26
- rack!
30
+ beryl!
31
+ bowser
32
+ bundler (~> 1.16)
33
+ minitest (~> 5.0)
34
+ opal
35
+ puma
36
+ rack
37
+ rake (~> 10.0)
27
38
 
28
39
  BUNDLED WITH
29
- 1.16.4
40
+ 1.16.5
data/Rakefile CHANGED
@@ -1,27 +1 @@
1
- require 'opal'
2
- require 'rake/testtask'
3
- require 'bowser'
4
- require 'bundler/gem_tasks'
5
-
6
- desc 'Build the app to build/app.js'
7
- task :build do
8
- Opal.append_path 'app'
9
- Opal.append_path 'lib'
10
- Dir.mkdir('build') unless File.exist?('build')
11
- File.binwrite 'build/app.js', Opal::Builder.build('frontend_app').to_s
12
- end
13
-
14
- desc 'Build and run the app'
15
- task :run do
16
- Rake::Task['build'].invoke
17
- sh 'bundle exec rackup --port 3000 --host 0.0.0.0'
18
- end
19
-
20
- desc 'Test the app'
21
- Rake::TestTask.new(:test) do |t|
22
- t.libs << 'test'
23
- t.libs << 'lib'
24
- t.test_files = FileList['test/**/*_test.rb']
25
- end
26
-
27
- task :default => :test
1
+ load './lib/beryl/Rakefile'
data/Rakefile-template ADDED
@@ -0,0 +1,5 @@
1
+ require 'beryl'
2
+
3
+ spec = Gem::Specification.find_by_name 'beryl'
4
+ rakefile = "#{spec.gem_dir}/lib/beryl/Rakefile"
5
+ load rakefile
data/app/frontend.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'beryl/frontend'
2
+ require 'view'
3
+
4
+ Beryl::Frontend.new(View.new).run
@@ -0,0 +1,5 @@
1
+ {
2
+ content: 'here we will load something',
3
+ counter: 0,
4
+ route: nil
5
+ }
data/app/something.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'beryl/widget'
2
+
3
+ class Something < Beryl::Widget
4
+ def render(state)
5
+ column :fill_width, :fill_height do
6
+ text 'Bart', height: 100, width: 300
7
+ text 'Abc', proportional_height: 2
8
+ text 'Karol', :fill_height
9
+ text 'Xyz', proportional_height: 3
10
+ end
11
+ end
12
+ end
data/app/view.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'beryl/view'
2
+ require 'something'
3
+
4
+ class View < Beryl::View
5
+ def render
6
+ Something.new.render(state)
7
+ end
8
+ end
data/beryl.gemspec CHANGED
@@ -1,38 +1,33 @@
1
-
2
- lib = File.expand_path("../lib", __FILE__)
1
+ lib = File.expand_path('../lib', __FILE__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "beryl/version"
3
+ require 'beryl/version'
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = "beryl"
6
+ spec.name = 'beryl'
8
7
  spec.version = Beryl::VERSION
9
- spec.authors = ["Bart Blast"]
10
- spec.email = ["bart@bartblast.com"]
8
+ spec.authors = ['Bart Blast']
9
+ spec.email = ['bart@bartblast.com']
11
10
 
12
11
  spec.summary = %q{Web framework}
13
12
  spec.description = %q{Web framework}
14
- spec.homepage = "https://github.com/bartblast/beryl"
15
- spec.license = "MIT"
16
-
17
- # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
- # to allow pushing to a single host or delete this section to allow pushing to any host.
19
- # if spec.respond_to?(:metadata)
20
- # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
21
- # else
22
- # raise "RubyGems 2.0 or newer is required to protect against " \
23
- # "public gem pushes."
24
- # end
13
+ spec.homepage = 'https://github.com/bartblast/beryl'
14
+ spec.license = 'MIT'
25
15
 
26
16
  # Specify which files should be added to the gem when it is released.
27
17
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
28
18
  spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
29
19
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
30
20
  end
31
- spec.bindir = "exe"
21
+ spec.bindir = 'exe'
32
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
33
- spec.require_paths = ["lib"]
23
+ spec.require_paths = ['lib']
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.16'
26
+ spec.add_development_dependency 'rake', '~> 10.0'
27
+ spec.add_development_dependency 'minitest', '~> 5.0'
34
28
 
35
- spec.add_development_dependency "bundler", "~> 1.16"
36
- spec.add_development_dependency "rake", "~> 10.0"
37
- spec.add_development_dependency "minitest", "~> 5.0"
38
- end
29
+ spec.add_development_dependency 'bowser'
30
+ spec.add_development_dependency 'opal'
31
+ spec.add_development_dependency 'puma'
32
+ spec.add_development_dependency 'rack'
33
+ end
data/config.ru CHANGED
@@ -1,5 +1,5 @@
1
+ require 'beryl/backend'
1
2
  require 'rack'
2
- require_relative 'lib/app'
3
3
 
4
4
  use Rack::Static, :urls => ['/build']
5
- run App.new
5
+ run Beryl::Backend.new
@@ -0,0 +1,28 @@
1
+ require 'bowser'
2
+ require 'opal'
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ desc 'Build the app to build/app.js'
7
+ task :compile do
8
+ Opal.append_path 'app'
9
+ Opal.append_path 'lib'
10
+ Dir.mkdir('build') unless File.exist?('build')
11
+ File.binwrite 'build/app.js', Opal::Builder.build('frontend').to_s
12
+ File.binwrite 'build/style.css', File.read("#{File.dirname(__FILE__)}/style.css")
13
+ end
14
+
15
+ desc 'Build and run the app'
16
+ task :run do
17
+ Rake::Task['compile'].invoke
18
+ sh 'bundle exec rackup --port 3000 --host 0.0.0.0'
19
+ end
20
+
21
+ desc 'Test the app'
22
+ Rake::TestTask.new(:test) do |t|
23
+ t.libs << 'test'
24
+ t.libs << 'lib'
25
+ t.test_files = FileList['test/**/*_test.rb']
26
+ end
27
+
28
+ task :default => :test
@@ -0,0 +1,44 @@
1
+ require 'command_handler'
2
+ require 'json'
3
+ require 'serializer'
4
+
5
+ module Beryl
6
+ class Backend
7
+ def call(env)
8
+ req = Rack::Request.new(env)
9
+ case req.path_info
10
+ when '/command'
11
+ [200, { 'Content-Type' => 'application/json; charset=utf-8' }, [handle_command(req)]]
12
+ else
13
+ [200, { 'Content-Type' => 'text/html; charset=utf-8' }, [response]]
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def handle_command(req)
20
+ json = JSON.parse(req.body.read)
21
+ result = CommandHandler.new.handle(json['type'].to_sym, json['payload'])
22
+ Serializer.serialize(result)
23
+ end
24
+
25
+ def hydrate_state
26
+ Serializer.serialize(eval(File.read('app/initial_state.rb'))).gsub('"', '&quot;')
27
+ end
28
+
29
+ def response
30
+ <<~HEREDOC
31
+ <!DOCTYPE html>
32
+ <html>
33
+ <head>
34
+ <script src="build/app.js"></script>
35
+ <link rel="stylesheet" type="text/css" href="build/style.css">
36
+ </head>
37
+ <body>
38
+ <div id="beryl" data-beryl="#{hydrate_state}" class="bg-color-255-255-255-255 font-color-0-0-0-255 font-size-20 font-open-sanshelveticaverdanasans-serif s e ui s e"></div>
39
+ </body>
40
+ </html>
41
+ HEREDOC
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ require 'json'
2
+
3
+ module Beryl
4
+ module Deserializer
5
+ extend self
6
+
7
+ def deserialize(item, json = false)
8
+ item = JSON.parse(item) unless json
9
+ case item['class']
10
+ when 'Hash'
11
+ item['value'].each_with_object({}) do |(key, value), result|
12
+ result[key.to_sym] = deserialize(value, true)
13
+ end
14
+ when 'Integer'
15
+ item['value'].to_i
16
+ when 'String'
17
+ item['value']
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ require 'opal'
2
+ require 'native'
3
+ require 'beryl/deserializer'
4
+ require 'beryl/runtime'
5
+
6
+ module Beryl
7
+ class Frontend
8
+ def initialize(view)
9
+ @view = view
10
+ end
11
+
12
+ def onload(&block)
13
+ `window.onload = block;`
14
+ end
15
+
16
+ def run
17
+ onload do
18
+ document = Native(`window.document`)
19
+ root = document.getElementById('beryl')
20
+ serialized_state = root.getAttribute('data-beryl').gsub('&quot;', '"')
21
+ state = Beryl::Deserializer.deserialize(serialized_state)
22
+ Beryl::Runtime.new(root, state, @view).run
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,65 @@
1
+ require 'renderer'
2
+ require 'task'
3
+ require 'bowser/http'
4
+ require 'serializer'
5
+
6
+ module Beryl
7
+ class Runtime
8
+ def initialize(root, state, view)
9
+ @messages = []
10
+ @root = root
11
+ @state = state
12
+ @view = view
13
+ end
14
+
15
+ def push(message)
16
+ @messages << message
17
+ end
18
+
19
+ def render
20
+ @view.state = @state
21
+ virtual_dom = VirtualDOM.new(@view.render)
22
+ Renderer.new.render(self, virtual_dom.dom.first, @root)
23
+ end
24
+
25
+ def process
26
+ while @messages.any?
27
+ message = @messages.shift
28
+ result = transition(message.first, message.last)
29
+ @state = result.is_a?(Array) ? result.first : result
30
+ command = result.is_a?(Array) ? result[1] : nil
31
+ run_command(result[1], result[2]) if command
32
+ render
33
+ end
34
+ end
35
+
36
+ def run
37
+ process
38
+ render
39
+ end
40
+
41
+ def run_command(type, payload)
42
+ Task.new do
43
+ Bowser::HTTP.fetch('/command', method: :post, data: { type: type, payload: Serializer.serialize(payload) })
44
+ .then(&:json) # JSONify the response
45
+ .then { |response| puts response }
46
+ .catch { |exception| warn exception.message }
47
+ end
48
+ end
49
+
50
+ def transition(type, payload)
51
+ case type
52
+
53
+ when :IncrementClicked
54
+ @state.merge(counter: @state[:counter] + 1)
55
+
56
+ when :LoadClicked
57
+ [@state, :FetchData, key_1: 1, key_2: 2]
58
+
59
+ when :LoadSuccess
60
+ @state.merge(content: payload[:data])
61
+
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,89 @@
1
+ @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {.s.r > .s { flex-basis: auto !important; } .s.r > .s.ctr { flex-basis: auto !important; }}
2
+
3
+ /* General Input Reset */
4
+ input[type=range] {
5
+ -webkit-appearance: none; /* Hides the slider so that custom slider can be made */
6
+ /* width: 100%; Specific width is required for Firefox. */
7
+ background: transparent; /* Otherwise white in Chrome */
8
+ position:absolute;
9
+ left:0;
10
+ top:0;
11
+ z-index:10;
12
+ width: 100%;
13
+ outline: dashed 1px;
14
+ height: 100%;
15
+ opacity: 0;
16
+ }
17
+
18
+ /* Hide all syling for track */
19
+ input[type=range]::-moz-range-track {
20
+ background: transparent;
21
+ cursor: pointer;
22
+ }
23
+ input[type=range]::-ms-track {
24
+ background: transparent;
25
+ cursor: pointer;
26
+ }
27
+ input[type=range]::-webkit-slider-runnable-track {
28
+ background: transparent;
29
+ cursor: pointer;
30
+ }
31
+
32
+ /* Thumbs */
33
+ input[type=range]::-webkit-slider-thumb {
34
+ -webkit-appearance: none;
35
+ opacity: 0.5;
36
+ width: 80px;
37
+ height: 80px;
38
+ background-color: black;
39
+ border:none;
40
+ border-radius: 5px;
41
+ }
42
+ input[type=range]::-moz-range-thumb {
43
+ opacity: 0.5;
44
+ width: 80px;
45
+ height: 80px;
46
+ background-color: black;
47
+ border:none;
48
+ border-radius: 5px;
49
+ }
50
+ input[type=range]::-ms-thumb {
51
+ opacity: 0.5;
52
+ width: 80px;
53
+ height: 80px;
54
+ background-color: black;
55
+ border:none;
56
+ border-radius: 5px;
57
+ }
58
+ input[type=range][orient=vertical]{
59
+ writing-mode: bt-lr; /* IE */
60
+ -webkit-appearance: slider-vertical; /* WebKit */
61
+ }
62
+
63
+ .explain {
64
+ border: 6px solid rgb(174, 121, 15) !important;
65
+ }
66
+ .explain > .s {
67
+ border: 4px dashed rgb(0, 151, 167) !important;
68
+ }
69
+
70
+ .ctr {
71
+ border: none !important;
72
+ }
73
+ .explain > .ctr > .s {
74
+ border: 4px dashed rgb(0, 151, 167) !important;
75
+ }
76
+
77
+ html,body{height:100%;padding:0;margin:0;}.s.e.ic{display:block;}.s:focus{outline:none;}.ui{width:100%;height:auto;min-height:100%;z-index:0;}.ui.s.hf{height:100%;}.ui.s.hf > .hf{height:100%;}.ui > .fr.s{position:fixed;}.s{position:relative;border:none;flex-shrink:0;display:flex;flex-direction:row;flex-basis:auto;resize:none;box-sizing:border-box;margin:0;padding:0;border-width:0;border-style:solid;font-size:inherit;color:inherit;font-family:inherit;line-height:1;font-weight:inherit;text-decoration:none;font-style:inherit;}.s.wrp{flex-wrap:wrap;}.s.notxt{-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;}.s.cptr{cursor:pointer;}.s.ctxt{cursor:text;}.s.ppe{pointer-events:none !important;}.s.cpe{pointer-events:auto !important;}.s.clr{opacity:0;}.s.oq{opacity:1;}.s.hvclr:hover{opacity:0;}.s.hvoq:hover{opacity:1;}.s.fcsclr:focus{opacity:0;}.s.fcsoq:focus{opacity:1;}.s.atvclr:active{opacity:0;}.s.atvoq:active{opacity:1;}.s.ts{transition:transform 160ms, opacity 160ms, filter 160ms, background-color 160ms, color 160ms, font-size 160ms;}.s.sb{overflow:auto;flex-shrink:1;}.s.sbx{overflow-x:auto;}.s.sbx.r{flex-shrink:1;}.s.sby{overflow-y:auto;}.s.sby.c{flex-shrink:1;}.s.sby.e{flex-shrink:1;}.s.cp{overflow:hidden;}.s.cpx{overflow-x:hidden;}.s.cpy{overflow-y:hidden;}.s.wc{width:auto;}.s.bn{border-width:0;}.s.bd{border-style:dashed;}.s.bdt{border-style:dotted;}.s.bs{border-style:solid;}.s.t{white-space:pre;display:inline-block;}.s.it{line-height:1.05;}.s.e{display:flex;flex-direction:column;white-space:pre;}.s.e.hbh{z-index:0;}.s.e.hbh > .bh{z-index:-1;}.s.e.sbt > .t.hf{flex-grow:0;}.s.e.sbt > .t.wf{align-self:auto !important;}.s.e > .hc{height:auto;}.s.e > .hf{flex-grow:100000;}.s.e > .wf{width:100%;}.s.e > .wc{align-self:flex-start;}.s.e.ct{justify-content:flex-start;}.s.e > .s.at{margin-bottom:auto !important;margin-top:0 !important;}.s.e.cb{justify-content:flex-end;}.s.e > .s.ab{margin-top:auto !important;margin-bottom:0 !important;}.s.e.cr{align-items:flex-end;}.s.e > .s.ar{align-self:flex-end;}.s.e.cl{align-items:flex-start;}.s.e > .s.al{align-self:flex-start;}.s.e.ccx{align-items:center;}.s.e > .s.cx{align-self:center;}.s.e.ccy > .s{margin-top:auto;margin-bottom:auto;}.s.e > .s.cy{margin-top:auto !important;margin-bottom:auto !important;}.s.r{display:flex;flex-direction:row;}.s.r > .s{flex-basis:0%;}.s.r > .s.we{flex-basis:auto;}.s.r > .hf{align-self:stretch !important;}.s.r > .hfp{align-self:stretch !important;}.s.r > .wf{flex-grow:100000;}.s.r > .ctr{flex-grow:0;flex-basis:auto;align-self:stretch;}.s.r > u:first-of-type.acr{flex-grow:1;}.s.r > s:first-of-type.accx{flex-grow:1;}.s.r > s:first-of-type.accx > .cx{margin-left:auto !important;}.s.r > s:last-of-type.accx{flex-grow:1;}.s.r > s:last-of-type.accx > .cx{margin-right:auto !important;}.s.r > s:only-of-type.accx{flex-grow:1;}.s.r > s:only-of-type.accx > .cy{margin-top:auto !important;margin-bottom:auto !important;}.s.r > s:last-of-type.accx ~ u{flex-grow:0;}.s.r > u:first-of-type.acr ~ s.accx{flex-grow:0;}.s.r.ct{align-items:flex-start;}.s.r > .s.at{align-self:flex-start;}.s.r.cb{align-items:flex-end;}.s.r > .s.ab{align-self:flex-end;}.s.r.cr{justify-content:flex-end;}.s.r.cl{justify-content:flex-start;}.s.r.ccx{justify-content:center;}.s.r.ccy{align-items:center;}.s.r > .s.cy{align-self:center;}.s.r.sev{justify-content:space-between;}.s.c{display:flex;flex-direction:column;}.s.c > .hf{flex-grow:100000;}.s.c > .wf{width:100%;}.s.c > .wfp{width:100%;}.s.c > .wc{align-self:flex-start;}.s.c > u:first-of-type.acb{flex-grow:1;}.s.c > s:first-of-type.accy{flex-grow:1;}.s.c > s:first-of-type.accy > .cy{margin-top:auto !important;margin-bottom:0 !important;}.s.c > s:last-of-type.accy{flex-grow:1;}.s.c > s:last-of-type.accy > .cy{margin-bottom:auto !important;margin-top:0 !important;}.s.c > s:only-of-type.accy{flex-grow:1;}.s.c > s:only-of-type.accy > .cy{margin-top:auto !important;margin-bottom:auto !important;}.s.c > s:last-of-type.accy ~ u{flex-grow:0;}.s.c > u:first-of-type.acb ~ s.accy{flex-grow:0;}.s.c.ct{justify-content:flex-start;}.s.c > .s.at{margin-bottom:auto;}.s.c.cb{justify-content:flex-end;}.s.c > .s.ab{margin-top:auto;}.s.c.cr{align-items:flex-end;}.s.c > .s.ar{align-self:flex-end;}.s.c.cl{align-items:flex-start;}.s.c > .s.al{align-self:flex-start;}.s.c.ccx{align-items:center;}.s.c > .s.cx{align-self:center;}.s.c.ccy{justify-content:center;}.s.c > .ctr{flex-grow:0;flex-basis:auto;width:100%;align-self:stretch !important;}.s.c.sev{justify-content:space-between;}.s.g{display:-ms-grid;}.s.g > .gp > .s{width:100%;}@supports (display:grid) {.s.g{display:grid;
78
+ }}.s.g > .s.at{justify-content:flex-start;}.s.g > .s.ab{justify-content:flex-end;}.s.g > .s.ar{align-items:flex-end;}.s.g > .s.al{align-items:flex-start;}.s.g > .s.cx{align-items:center;}.s.g > .s.cy{justify-content:center;}.s.pg{display:block;}.s.pg > .s:first-child{margin:0 !important;}.s.pg > .s.al:first-child + .s{margin:0 !important;}.s.pg > .s.ar:first-child + .s{margin:0 !important;}.s.pg > .s.ar{float:right;}.s.pg > .s.ar::after{content:"";display:table;clear:both;}.s.pg > .s.al{float:left;}.s.pg > .s.al::after{content:"";display:table;clear:both;}.s.iml{white-space:pre-wrap;}.s.p{display:block;white-space:normal;}.s.p.hbh{z-index:0;}.s.p.hbh > .bh{z-index:-1;}.s.p > .t{display:inline;white-space:normal;}.s.p > .e{display:inline;white-space:normal;}.s.p > .e.fr{display:flex;}.s.p > .e.bh{display:flex;}.s.p > .e.a{display:flex;}.s.p > .e.b{display:flex;}.s.p > .e.or{display:flex;}.s.p > .e.ol{display:flex;}.s.p > .e > .t{display:inline;white-space:normal;}.s.p > .r{display:inline-flex;}.s.p > .c{display:inline-flex;}.s.p > .g{display:inline-grid;}.s.p > .s.ar{float:right;}.s.p > .s.al{float:left;}.s.hidden{display:none;}.s.a{position:absolute;bottom:100%;left:0;width:100%;z-index:10;margin:0 !important;pointer-events:none;}.s.a > .hf{height:auto;}.s.a > .wf{width:100%;}.s.a > .s{pointer-events:auto;}.s.b{position:absolute;bottom:0;left:0;height:0;width:100%;z-index:10;margin:0 !important;pointer-events:auto;}.s.b > .hf{height:auto;}.s.or{position:absolute;left:100%;top:0;height:100%;margin:0 !important;z-index:10;pointer-events:auto;}.s.ol{position:absolute;right:100%;top:0;height:100%;margin:0 !important;z-index:10;pointer-events:auto;}.s.fr{position:absolute;width:100%;height:100%;left:0;top:0;margin:0 !important;z-index:10;pointer-events:none;}.s.fr > .s{pointer-events:auto;}.s.bh{position:absolute;width:100%;height:100%;left:0;top:0;margin:0 !important;z-index:0;pointer-events:none;}.s.bh > .s{pointer-events:auto;}.s.w1{font-weight:100;}.s.w2{font-weight:200;}.s.w3{font-weight:300;}.s.w4{font-weight:400;}.s.w5{font-weight:500;}.s.w6{font-weight:600;}.s.w7{font-weight:700;}.s.w8{font-weight:800;}.s.w9{font-weight:900;}.s.i{font-style:italic;}.s.sk{text-decoration:line-through;}.s.u{text-decoration:underline;text-decoration-skip-ink:auto;text-decoration-skip:ink;}.s.u.sk{text-decoration:line-through underline;text-decoration-skip-ink:auto;text-decoration-skip:ink;}.s.tun{font-style:normal;}.s.tj{text-align:justify;}.s.tja{text-align:justify-all;}.s.tc{text-align:center;}.s.tr{text-align:right;}.s.tl{text-align:left;}.s.modal{position:fixed;left:0;top:0;width:100%;height:100%;pointer-events:none;}.border-0{border-width:0px;}.border-1{border-width:1px;}.border-2{border-width:2px;}.border-3{border-width:3px;}.border-4{border-width:4px;}.border-5{border-width:5px;}.border-6{border-width:6px;}.font-size-8{font-size:8px;}.font-size-9{font-size:9px;}.font-size-10{font-size:10px;}.font-size-11{font-size:11px;}.font-size-12{font-size:12px;}.font-size-13{font-size:13px;}.font-size-14{font-size:14px;}.font-size-15{font-size:15px;}.font-size-16{font-size:16px;}.font-size-17{font-size:17px;}.font-size-18{font-size:18px;}.font-size-19{font-size:19px;}.font-size-20{font-size:20px;}.font-size-21{font-size:21px;}.font-size-22{font-size:22px;}.font-size-23{font-size:23px;}.font-size-24{font-size:24px;}.font-size-25{font-size:25px;}.font-size-26{font-size:26px;}.font-size-27{font-size:27px;}.font-size-28{font-size:28px;}.font-size-29{font-size:29px;}.font-size-30{font-size:30px;}.font-size-31{font-size:31px;}.font-size-32{font-size:32px;}.p-0{padding:0px;}.p-1{padding:1px;}.p-2{padding:2px;}.p-3{padding:3px;}.p-4{padding:4px;}.p-5{padding:5px;}.p-6{padding:6px;}.p-7{padding:7px;}.p-8{padding:8px;}.p-9{padding:9px;}.p-10{padding:10px;}.p-11{padding:11px;}.p-12{padding:12px;}.p-13{padding:13px;}.p-14{padding:14px;}.p-15{padding:15px;}.p-16{padding:16px;}.p-17{padding:17px;}.p-18{padding:18px;}.p-19{padding:19px;}.p-20{padding:20px;}.p-21{padding:21px;}.p-22{padding:22px;}.p-23{padding:23px;}.p-24{padding:24px;}
79
+
80
+ .font-open-sanshelveticaverdanasans-serif{
81
+ font-family: "Open Sans", "Helvetica", "Verdana", sans-serif;
82
+ }.font-color-0-0-0-255{
83
+ color: rgba(0,0,0,1);
84
+ }.bg-color-255-255-255-255{
85
+ background-color: rgba(255,255,255,1);
86
+ }.s:focus .focusable, .s.focusable:focus{
87
+ box-shadow: 0px 0px 3px 3px rgba(155,203,255,1);
88
+ outline: none;
89
+ }
@@ -0,0 +1,55 @@
1
+ module Beryl
2
+ module Utils
3
+ extend self
4
+
5
+ def constantize(camel_cased_word)
6
+ names = camel_cased_word.split('::'.freeze)
7
+
8
+ # Trigger a built-in NameError exception including the ill-formed constant in the message.
9
+ Object.const_get(camel_cased_word) if names.empty?
10
+
11
+ # Remove the first blank element in case of '::ClassName' notation.
12
+ names.shift if names.size > 1 && names.first.empty?
13
+
14
+ names.inject(Object) do |constant, name|
15
+ if constant == Object
16
+ constant.const_get(name)
17
+ else
18
+ candidate = constant.const_get(name)
19
+ next candidate if constant.const_defined?(name, false)
20
+ next candidate unless Object.const_defined?(name)
21
+
22
+ # Go down the ancestors to check if it is owned directly. The check
23
+ # stops when we reach Object or the end of ancestors tree.
24
+ constant = constant.ancestors.inject(constant) do |const, ancestor|
25
+ break const if ancestor == Object
26
+ break ancestor if ancestor.const_defined?(name, false)
27
+ const
28
+ end
29
+
30
+ # owner is in Object, so raise
31
+ constant.const_get(name, false)
32
+ end
33
+ end
34
+ end
35
+
36
+ def deep_symbolize_keys(hash)
37
+ deep_transform_keys_in_object(hash) { |key| key.to_sym rescue key }
38
+ end
39
+
40
+ private
41
+
42
+ def deep_transform_keys_in_object(object, &block)
43
+ case object
44
+ when Hash
45
+ object.each_with_object({}) do |(key, value), result|
46
+ result[yield(key)] = deep_transform_keys_in_object(value, &block)
47
+ end
48
+ when Array
49
+ object.map {|e| deep_transform_keys_in_object(e, &block) }
50
+ else
51
+ object
52
+ end
53
+ end
54
+ end
55
+ end
data/lib/beryl/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Beryl
2
- VERSION = "0.1.0"
2
+ VERSION = '0.2.0'
3
3
  end
data/lib/beryl/view.rb ADDED
@@ -0,0 +1,35 @@
1
+ require 'beryl/virtual_dom'
2
+
3
+ module Beryl
4
+ class View
5
+ attr_accessor :state
6
+
7
+ def div(props = {}, &children)
8
+ node('div', props, children ? children.call : [])
9
+ end
10
+
11
+ def input(props = {}, &children)
12
+ node('input', props, children ? children.call : [])
13
+ end
14
+
15
+ def link(text, props = {}, &children)
16
+ node('a', props, [text(text)])
17
+ end
18
+
19
+ def node(type, props = {}, children)
20
+ {
21
+ type: type,
22
+ props: props,
23
+ children: children
24
+ }
25
+ end
26
+
27
+ def span(props = {}, &children)
28
+ node('span', props, children ? children.call : [])
29
+ end
30
+
31
+ def text(value, props = {}, &children)
32
+ node('text', props.merge(nodeValue: value), children ? children.call : [])
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,144 @@
1
+ module Beryl
2
+ class VirtualDOM
3
+ attr_reader :dom
4
+
5
+ def initialize(layout)
6
+ @layout = layout
7
+ @dom = convert(layout)
8
+ end
9
+
10
+ private
11
+
12
+ def convert(layout)
13
+ layout.each_with_object([]) do |element, dom|
14
+ case element[:type]
15
+ when :column
16
+ width = width(element[:props])
17
+ height = height(element[:props])
18
+ klass = "#{height[:class]} s c #{width[:class]} ct cl"
19
+ style = "#{width[:style]}#{height[:style]}"
20
+ props = { class: klass, style: style }
21
+ dom << node('div', props, element[:children] ? convert(element[:children]) : [])
22
+ when :row
23
+ width = width(element[:props])
24
+ height = height(element[:props])
25
+ klass = "#{height[:class]} s r #{width[:class]} cl ccy"
26
+ style = "#{width[:style]}#{height[:style]}"
27
+ props = { class: klass, style: style }
28
+ dom << node('div', props, element[:children] ? convert(element[:children]) : [])
29
+ when :text
30
+ width = width(element[:props])
31
+ height = height(element[:props])
32
+ klass = "#{height[:class]} s e #{width[:class]}"
33
+ style = "#{width[:style]}#{height[:style]}"
34
+ props = { class: klass, style: style }
35
+ dom << node('div', props, [node('text', { nodeValue: element[:value] })])
36
+ end
37
+ end
38
+ end
39
+
40
+ def height(props)
41
+ type = height_type(props)
42
+ {
43
+ type: type,
44
+ class: height_class(type),
45
+ style: height_style(props, type)
46
+ }
47
+ end
48
+
49
+ def width(props)
50
+ type = width_type(props)
51
+ {
52
+ type: type,
53
+ class: width_class(type),
54
+ style: width_style(props, type)
55
+ }
56
+ end
57
+
58
+ def width_type(props)
59
+ props = [props] unless props.is_a?(Array)
60
+ return :content unless props
61
+ return :fill if props.include?(:fill_width)
62
+ hash = props.select { |p| p.is_a?(Hash) }.first
63
+ return :content unless hash
64
+ return :fixed if hash[:width].is_a?(Integer)
65
+ return :proportional if hash[:proportional_width].is_a?(Integer)
66
+ :content
67
+ end
68
+
69
+ def height_type(props)
70
+ props = [props] unless props.is_a?(Array)
71
+ return :content unless props
72
+ return :fill if props.include?(:fill_height)
73
+ hash = props.select { |p| p.is_a?(Hash) }.first
74
+ return :content unless hash
75
+ return :fixed if hash[:height].is_a?(Integer)
76
+ return :proportional if hash[:proportional_height].is_a?(Integer)
77
+ :content
78
+ end
79
+
80
+ def width_class(type)
81
+ case type
82
+ when :content
83
+ 'wc'
84
+ when :fill
85
+ 'wf'
86
+ when :fixed
87
+ ''
88
+ when :proportional
89
+ 'wfp'
90
+ end
91
+ end
92
+
93
+ def height_class(type)
94
+ case type
95
+ when :content
96
+ 'hc'
97
+ when :fill
98
+ 'hf'
99
+ when :fixed
100
+ ''
101
+ when :proportional
102
+ 'hfp'
103
+ end
104
+ end
105
+
106
+ def width_style(props, type)
107
+ case type
108
+ when :content
109
+ ''
110
+ when :fill
111
+ ''
112
+ when :fixed
113
+ width = props.select { |p| p.is_a?(Hash) }.first[:width]
114
+ "width: #{width}px;"
115
+ when :proportional
116
+ portion = props.select { |p| p.is_a?(Hash) }.first[:proportional_width]
117
+ "flex-grow: #{100000 * portion};"
118
+ end
119
+ end
120
+
121
+ def height_style(props, type)
122
+ case type
123
+ when :content
124
+ ''
125
+ when :fill
126
+ ''
127
+ when :fixed
128
+ height = props.select { |p| p.is_a?(Hash) }.first[:height]
129
+ "height: #{height}px;"
130
+ when :proportional
131
+ portion = props.select { |p| p.is_a?(Hash) }.first[:proportional_height]
132
+ "flex-grow: #{100000 * portion};"
133
+ end
134
+ end
135
+
136
+ def node(type, props = {}, children = [])
137
+ {
138
+ type: type,
139
+ props: props,
140
+ children: children
141
+ }
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,50 @@
1
+ require 'beryl/utils'
2
+
3
+ module Beryl
4
+ class Widget
5
+ attr_reader :children
6
+
7
+ def initialize
8
+ @children = []
9
+ end
10
+
11
+ def build(type, *args, &block)
12
+ element = Widget.new
13
+ element.instance_eval(&block)
14
+ {
15
+ type: type,
16
+ props: args,
17
+ children: element.children
18
+ }
19
+ end
20
+
21
+ def column(*args, &block)
22
+ @children << build(:column, *args, &block)
23
+ @children
24
+ end
25
+
26
+ def method_missing(name, *args, &block)
27
+ constantized = Beryl::Utils.constantize(name.to_s)
28
+ child = args.any? ? constantized.new.render(*args) : constantized.new.render
29
+ raise SyntaxError.new("Widget #{name} should return only one element (use row or column)") if child.is_a?(Array) && child.size > 1
30
+ @children += child
31
+ child
32
+ rescue NoMethodError
33
+ raise NameError.new("There is no such widget: #{name}")
34
+ end
35
+
36
+ def row(*args, &block)
37
+ @children << build(:row, *args, &block)
38
+ @children
39
+ end
40
+
41
+ def text(string, *props)
42
+ @children << {
43
+ type: :text,
44
+ value: string,
45
+ props: props
46
+ }
47
+ @children
48
+ end
49
+ end
50
+ end
@@ -1,27 +1,27 @@
1
1
  require 'native'
2
- require 'event_loop'
2
+ require 'beryl/runtime'
3
3
 
4
- class VirtualDOM
5
- def render(event_loop, element, parentDom, replace = true)
4
+ class Renderer
5
+ def render(runtime, element, parentDom, replace = true)
6
6
  document = Native(`window.document`)
7
7
  dom = element[:type] == 'text' ? document.createTextNode('') : document.createElement(element[:type])
8
8
 
9
- add_event_listeners(element, dom, event_loop)
9
+ add_event_listeners(element, dom, runtime)
10
10
  set_attributes(element, dom)
11
11
 
12
12
  childElements = element[:children] || [];
13
- childElements.each { |child| render(event_loop, child, dom, false) }
13
+ childElements.each { |child| render(runtime, child, dom, false) }
14
14
 
15
15
  update_dom(parentDom, dom, replace)
16
16
  end
17
17
 
18
18
  private
19
19
 
20
- def add_event_listeners(element, dom, event_loop)
21
- listener_props = element[:props].select { |key, _value| listener?(key) }
22
- listener_props.each do |key, value|
20
+ def add_event_listeners(element, dom, runtime)
21
+ listeners = element[:props].select { |key, _value| listener?(key) }
22
+ listeners.each do |key, value|
23
23
  event_type = key.downcase[2..-1]
24
- dom.addEventListener(event_type, lambda { event_loop.push(value); event_loop.process })
24
+ dom.addEventListener(event_type, lambda { runtime.push(value); runtime.process })
25
25
  end
26
26
  end
27
27
 
@@ -30,8 +30,8 @@ class VirtualDOM
30
30
  end
31
31
 
32
32
  def set_attributes(element, dom)
33
- attribute_props = element[:props].reject { |key, _value| listener?(key) }
34
- attribute_props.each { |key, value| dom[key] = value }
33
+ attributes = element[:props].reject { |key, _value| listener?(key) }
34
+ attributes.each { |key, value| key != :class ? dom[key] = value : dom.className = value }
35
35
  end
36
36
 
37
37
  def update_dom(parent_dom, dom, replace)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: beryl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Blast
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-09-23 00:00:00.000000000 Z
11
+ date: 2018-09-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,6 +52,62 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bowser
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: opal
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: puma
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rack
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
55
111
  description: Web framework
56
112
  email:
57
113
  - bart@bartblast.com
@@ -66,20 +122,31 @@ files:
66
122
  - LICENSE.txt
67
123
  - README.md
68
124
  - Rakefile
69
- - app-Gemfile
70
- - app/frontend_app.rb
125
+ - Rakefile-template
126
+ - app/frontend.rb
127
+ - app/initial_state.rb
128
+ - app/something.rb
129
+ - app/view.rb
71
130
  - beryl.gemspec
72
131
  - bin/console
73
132
  - bin/setup
74
133
  - config.ru
75
- - lib/app.rb
76
134
  - lib/beryl.rb
135
+ - lib/beryl/Rakefile
136
+ - lib/beryl/backend.rb
137
+ - lib/beryl/deserializer.rb
138
+ - lib/beryl/frontend.rb
139
+ - lib/beryl/runtime.rb
140
+ - lib/beryl/style.css
141
+ - lib/beryl/utils.rb
77
142
  - lib/beryl/version.rb
143
+ - lib/beryl/view.rb
144
+ - lib/beryl/virtual_dom.rb
145
+ - lib/beryl/widget.rb
78
146
  - lib/command_handler.rb
79
- - lib/event_loop.rb
147
+ - lib/renderer.rb
80
148
  - lib/serializer.rb
81
149
  - lib/task.rb
82
- - lib/virtual_dom.rb
83
150
  homepage: https://github.com/bartblast/beryl
84
151
  licenses:
85
152
  - MIT
data/app-Gemfile DELETED
@@ -1,6 +0,0 @@
1
- source 'https://rubygems.org' do
2
- gem 'bowser'
3
- gem 'opal'
4
- gem 'puma'
5
- gem 'rack'
6
- end
data/app/frontend_app.rb DELETED
@@ -1,73 +0,0 @@
1
- require 'opal'
2
- require 'native'
3
- require 'event_loop'
4
- require 'serializer'
5
- require 'virtual_dom'
6
-
7
- puts 'Wow, running opal!'
8
-
9
- def div(props = {}, &children)
10
- node('div', props, children ? children.call : [])
11
- end
12
-
13
- def input(props = {}, &children)
14
- node('input', props, children ? children.call : [])
15
- end
16
-
17
- def link(text, props = {}, &children)
18
- node('a', props, [text(text)])
19
- end
20
-
21
- def node(type, props = {}, children)
22
- {
23
- type: type,
24
- props: props,
25
- children: children
26
- }
27
- end
28
-
29
- def span(props = {}, &children)
30
- node('span', props, children ? children.call : [])
31
- end
32
-
33
- def text(value, props = {}, &children)
34
- node('text', props.merge(nodeValue: value), children ? children.call : [])
35
- end
36
-
37
- def onload(&block)
38
- `window.onload = block;`
39
- end
40
-
41
- def element(state)
42
- div(id: 'container') {[
43
- input(value: 'foo', type: 'text'),
44
- span() {[
45
- text(' Foo ' + state[:counter].to_s + ' ')
46
- ]},
47
- link(' Increment ', onClick: [:IncrementClicked, key_1: 1, key_2: 2]),
48
- link(' Load ', onClick: [:LoadClicked, key_1: 1, key_2: 2]),
49
- div {[
50
- text(state[:content])
51
- ]}
52
- ]}
53
- end
54
-
55
- class Interval
56
- def initialize(time = 0, &block)
57
- @interval = `setInterval(function(){#{block.call}}, time)`
58
- end
59
-
60
- def stop
61
- `clearInterval(#@interval)`
62
- end
63
- end
64
-
65
- onload do
66
- document = Native(`window.document`)
67
- parentDom = document.getElementById('root')
68
-
69
- state = { counter: 0, content: 'here we will load something' }
70
- event_loop = EventLoop.new(parentDom, state)
71
- event_loop.process
72
- event_loop.render
73
- end
data/lib/app.rb DELETED
@@ -1,33 +0,0 @@
1
- require 'json'
2
- require_relative 'command_handler'
3
- require_relative 'serializer'
4
-
5
- class App
6
- HTML = <<~HEREDOC
7
- <!DOCTYPE html>
8
- <html>
9
- <head>
10
- <script src='build/app.js'></script>
11
- </head>
12
- <body>
13
- <div id="root"></div>
14
- </body>
15
- </html>
16
- HEREDOC
17
-
18
- def call (env)
19
- req = Rack::Request.new(env)
20
- case req.path_info
21
- when '/rock/command'
22
- [200, { 'Content-Type' => 'application/json; charset=utf-8' }, [run_command(req)]]
23
- else
24
- [200, { 'Content-Type' => 'text/html; charset=utf-8' }, [HTML]]
25
- end
26
- end
27
-
28
- def run_command(req)
29
- json = JSON.parse(req.body.read)
30
- result = CommandHandler.new.handle(json['type'].to_sym, json['payload'])
31
- Serializer.serialize(result)
32
- end
33
- end
data/lib/event_loop.rb DELETED
@@ -1,53 +0,0 @@
1
- require 'virtual_dom'
2
- require 'task'
3
- require 'bowser/http'
4
- require 'serializer'
5
-
6
- class EventLoop
7
- def initialize(root, state)
8
- @messages = []
9
- @root = root
10
- @state = state
11
- end
12
-
13
- def push(message)
14
- puts 'adding message...'
15
- @messages << message
16
- end
17
-
18
- def render
19
- VirtualDOM.new.render(self, element(@state), @root)
20
- end
21
-
22
- def process
23
- while @messages.any?
24
- message = @messages.shift
25
- result = transition(message.first, message.last)
26
- @state = result.is_a?(Array) ? result.first : result
27
- command = result.is_a?(Array) ? result[1] : nil
28
- run_command(result[1], result[2]) if command
29
- render
30
- end
31
- end
32
-
33
- def run_command(type, payload)
34
- puts 'running command'
35
- Task.new do
36
- Bowser::HTTP.fetch('/rock/command', method: :post, data: { type: type, payload: Serializer.serialize(payload) })
37
- .then(&:json) # JSONify the response
38
- .then { |response| puts response }
39
- .catch { |exception| warn exception.message }
40
- end
41
- end
42
-
43
- def transition(type, payload)
44
- case type
45
- when :IncrementClicked
46
- @state.merge(counter: @state[:counter] + 1)
47
- when :LoadClicked
48
- [@state, :FetchData, key_1: 1, key_2: 2]
49
- when :LoadSuccess
50
- @state.merge(content: payload[:data])
51
- end
52
- end
53
- end