rerb 0.1.1
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 +14 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +57 -0
- data/Rakefile +14 -0
- data/Steepfile +20 -0
- data/docs/capabilities.md +42 -0
- data/exe/rerb +5 -0
- data/lib/rerb/cli.rb +59 -0
- data/lib/rerb/compiler.rb +200 -0
- data/lib/rerb/ir.rb +16 -0
- data/lib/rerb/templater.rb +77 -0
- data/lib/rerb/version.rb +5 -0
- data/lib/rerb.rb +18 -0
- data/sig/rerb/cli.rbs +5 -0
- data/sig/rerb/compiler.rbs +64 -0
- data/sig/rerb/ir.rbs +12 -0
- data/sig/rerb/templater.rbs +23 -0
- data/sig/rerb.rbs +15 -0
- metadata +71 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c5447c563b19fb93588b116db35b0135642fcb6e91143dee1a91ad3cbe89967c
|
4
|
+
data.tar.gz: b4897e456dd1f51669f3ded89ec2d1482adea5f4385667b32db638adc43fb8f7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 512e6903850669d93e0e820ae21132026863cfac1e7230d84c38fd1f5512cd0631d01f8ce755f7b64a62dccdb78c63d55f71e60db5a54f3b8d150449cbc1b1de
|
7
|
+
data.tar.gz: e8f81f838b1990df6fbcd27769075a8d09b50a6ae8d7519758d452801fd76696f20caa17ea4f6c8ac725a047c99237456500c432bb926a58771d490fb6a75991
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.2.2
|
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
6
|
+
|
7
|
+
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
8
|
+
|
9
|
+
## Our Standards
|
10
|
+
|
11
|
+
Examples of behavior that contributes to a positive environment for our community include:
|
12
|
+
|
13
|
+
* Demonstrating empathy and kindness toward other people
|
14
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
15
|
+
* Giving and gracefully accepting constructive feedback
|
16
|
+
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
17
|
+
* Focusing on what is best not just for us as individuals, but for the overall community
|
18
|
+
|
19
|
+
Examples of unacceptable behavior include:
|
20
|
+
|
21
|
+
* The use of sexualized language or imagery, and sexual attention or
|
22
|
+
advances of any kind
|
23
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
24
|
+
* Public or private harassment
|
25
|
+
* Publishing others' private information, such as a physical or email
|
26
|
+
address, without their explicit permission
|
27
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
28
|
+
professional setting
|
29
|
+
|
30
|
+
## Enforcement Responsibilities
|
31
|
+
|
32
|
+
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
33
|
+
|
34
|
+
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
35
|
+
|
36
|
+
## Scope
|
37
|
+
|
38
|
+
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
39
|
+
|
40
|
+
## Enforcement
|
41
|
+
|
42
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at castlehoneyjung@gmail.com. All complaints will be reviewed and investigated promptly and fairly.
|
43
|
+
|
44
|
+
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
45
|
+
|
46
|
+
## Enforcement Guidelines
|
47
|
+
|
48
|
+
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
49
|
+
|
50
|
+
### 1. Correction
|
51
|
+
|
52
|
+
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
53
|
+
|
54
|
+
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
55
|
+
|
56
|
+
### 2. Warning
|
57
|
+
|
58
|
+
**Community Impact**: A violation through a single incident or series of actions.
|
59
|
+
|
60
|
+
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
61
|
+
|
62
|
+
### 3. Temporary Ban
|
63
|
+
|
64
|
+
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
65
|
+
|
66
|
+
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
67
|
+
|
68
|
+
### 4. Permanent Ban
|
69
|
+
|
70
|
+
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
71
|
+
|
72
|
+
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
73
|
+
|
74
|
+
## Attribution
|
75
|
+
|
76
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
|
77
|
+
available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
78
|
+
|
79
|
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
80
|
+
|
81
|
+
[homepage]: https://www.contributor-covenant.org
|
82
|
+
|
83
|
+
For answers to common questions about this code of conduct, see the FAQ at
|
84
|
+
https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 Forthoney
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# RERB: WebAssembly Embedded Ruby
|
2
|
+
|
3
|
+
Use ERB/rhtml to build DOMs in [ruby.wasm](https://github.com/ruby/ruby.wasm).
|
4
|
+
|
5
|
+
RERB is an unopinionated tool for compiling ERB/rhtml into ruby.wasm DOM operations for building the DOM tree described in the source file. Specifically, it generates code which, when run on a Ruby VM on WASM, generate the desired DOM.
|
6
|
+
RERB is experimental and very young, so there are some usage caveats which you can read about [here](docs/capabilities.md).
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Install the gem and add to the application's Gemfile by executing:
|
11
|
+
|
12
|
+
$ bundle add RERB
|
13
|
+
|
14
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
15
|
+
|
16
|
+
$ gem install RERB
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
### For Developers
|
20
|
+
The CLI is the recommended way to use RERB in development. The simplest way to call rerb would be to just do
|
21
|
+
```bash
|
22
|
+
rerb your-file.erb
|
23
|
+
```
|
24
|
+
RERB will compile this erb file into a HTML file which internally uses ruby.wasm DOM operations to generate the DOM in the erb file. If you want to save this output to a file, you can simply use the shell `>` operator to write the output into a file.
|
25
|
+
```bash
|
26
|
+
rerb your-file.erb > your-file.html
|
27
|
+
```
|
28
|
+
`your-file.html` is a fully valid HTML file, and is ready to be rendered on the browser, assuming the erb code itself contains all the necessary logic.
|
29
|
+
|
30
|
+
There are other flags which help you customize some aesthetics of the generated code. For example,
|
31
|
+
```bash
|
32
|
+
rerb --template nil your-file.erb
|
33
|
+
```
|
34
|
+
will generate just the DOM operations without the HTML boilerplate. This would be useful if you already have a HTML file and are simply looking to extend.
|
35
|
+
|
36
|
+
### For Browsers
|
37
|
+
Running the code generated by RERB in a browser does not require RERB. It only needs `ruby.wasm` with the JS interop extension. This is easily available via CDN as shown [here](https://github.com/ruby/ruby.wasm/tree/main/packages/npm-packages/ruby-head-wasm-wasi).
|
38
|
+
Alternatively, you can use one of the templates which have all of this automatically set up.
|
39
|
+
|
40
|
+
## Development
|
41
|
+
|
42
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
43
|
+
|
44
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
45
|
+
|
46
|
+
## Contributing
|
47
|
+
|
48
|
+
Reading the [capabilities (and limitations)](docs/capabilities.md) is highly recommended. The current limitations of RERB listed in the document would be a great place to start looking for places to possibly contribute.
|
49
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/forthoney/rerb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/forthoney/rerb/blob/main/CODE_OF_CONDUCT.md).
|
50
|
+
|
51
|
+
## License
|
52
|
+
|
53
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
54
|
+
|
55
|
+
## Code of Conduct
|
56
|
+
|
57
|
+
Everyone interacting in the rerb project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/forthoney/rerb/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
7
|
+
|
8
|
+
require "rubocop/rake_task"
|
9
|
+
|
10
|
+
RuboCop::RakeTask.new do |task|
|
11
|
+
task.requires << "rubocop-rspec"
|
12
|
+
end
|
13
|
+
|
14
|
+
task default: [:spec, :rubocop]
|
data/Steepfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
D = Steep::Diagnostic
|
4
|
+
|
5
|
+
target :lib do
|
6
|
+
signature "sig"
|
7
|
+
|
8
|
+
check "lib" # Directory name
|
9
|
+
# library "better_html" # better_html does not have RBS yet
|
10
|
+
|
11
|
+
configure_code_diagnostics(D::Ruby.default) # `default` diagnostics setting (applies by default)
|
12
|
+
end
|
13
|
+
|
14
|
+
# target :test do
|
15
|
+
# signature "sig", "sig-private"
|
16
|
+
#
|
17
|
+
# check "test"
|
18
|
+
#
|
19
|
+
# # library "pathname" # Standard libraries
|
20
|
+
# end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# What does RERB want to be?
|
2
|
+
In short, RERB roughly mirrors how [React deals with JSX](https://facebook.github.io/jsx/).
|
3
|
+
Firstly, it borrows the principle that JSX is merely syntactic sugar over JS, and thus is not valid ECMAScript. JSX is instead precompiled by Babel into valid JS.
|
4
|
+
Secondly, JSX is only one part of React, and a part that is unrelated to how components are updated. JSX is just an easy way to describe a DOM in code.
|
5
|
+
|
6
|
+
The long answer is the following:
|
7
|
+
## RERB is a _developer_'s tool, not a client-side package
|
8
|
+
RERB is meant to be used by the developer to help them write code. It is not meant to be shipped to the client as an npm package.
|
9
|
+
There are a couple of reasons behind this.
|
10
|
+
1. Implementation Difficulty
|
11
|
+
|
12
|
+
It is currently very difficult to ship Gems with Ruby on WASM. This problem holds for almost all popular interpreted languages that have some WASM implementation at the moment (e.g. pip and python).
|
13
|
+
It may be possible in a year or two to include Gems written in pure Ruby with a RubyVM running on WASM.
|
14
|
+
Unfortunately, RERB uses [better-html](https://github.com/Shopify/better-html), a Gem with C extensions, and these Gems will likely take longer to be shippable.
|
15
|
+
|
16
|
+
2. Practical Considerations
|
17
|
+
|
18
|
+
Even if a means to ship RERB to the client is developed, we should stop and ask - _should we?_
|
19
|
+
Sending RERB over to the client would mean a larger download size for the client.
|
20
|
+
This may not seem like a huge issue, but considering the base Ruby Interpreter is already fairly large, it's good to save space where possible.
|
21
|
+
There have been precedents of adding libraries increasing the WASM download size significantly, namely in Blazor WebAssembly.
|
22
|
+
|
23
|
+
## RERB is an ERB Compiler, not a full UI Library/Framework
|
24
|
+
At this current stage, RERB is not concerned with efficient re-rendering algorithms, components, or other features that libraries like React have.
|
25
|
+
It is concerned with transforming easy-to-write ERB files into tedious DOM transformations.
|
26
|
+
Once RERB finishes compilation, how exactly the rendering works is in the developer's hands.
|
27
|
+
If you choose to rerender the entire page on every update, that is up to you.
|
28
|
+
If you choose a more fine grained, element-wise rerendering, that is also up to you.
|
29
|
+
If you choose to build a library that extends upon rerb's output and applies a generalizable rerendering technique, that is also entirely up to you.
|
30
|
+
This philosophy may change in the future, but RERB's current priority is in describing a DOM with erb, not DOM update algorithms.
|
31
|
+
|
32
|
+
# Where is RERB currently?
|
33
|
+
## Features
|
34
|
+
RERB supports the vast majority of HTML deemed valid by the better-html Gem. There are some small pieces which are unsupported, none of which are significant
|
35
|
+
- [ ] Strip from beginning or end of String via `<%-` and `-%>` (currently ignored entirely)
|
36
|
+
- [ ] Attribute values without quotation marks (currently undefined behavior)
|
37
|
+
|
38
|
+
## Rigidity
|
39
|
+
RERB is quite brittle at the moment, and there are ongoing efforts to help make it more "error tolerant".
|
40
|
+
- [ ] Attempting to commpile invalid HTML-ERB results in undefined behavior. It may error, or it may finish running and produce the wrong graph.
|
41
|
+
- [ ] Improve better-html's Parser's output type.
|
42
|
+
- [ ] More tests, particularly for the CLI.
|
data/exe/rerb
ADDED
data/lib/rerb/cli.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
|
5
|
+
require "rerb"
|
6
|
+
require "rerb/templater"
|
7
|
+
|
8
|
+
module RERB
|
9
|
+
# Command Line Interface for invoking RERB
|
10
|
+
module CLI
|
11
|
+
def parse(args)
|
12
|
+
parser = OptionParser.new do |o|
|
13
|
+
o.banner = "Usage: werb [options...] FILE"
|
14
|
+
o.on(
|
15
|
+
"--template TYPE",
|
16
|
+
["umd", "iife", "nil"],
|
17
|
+
"Specify which html template to use to wrap the generated code. " \
|
18
|
+
"Valid options are umd, iife, and nil.",
|
19
|
+
"nil will use no template and just output raw ruby.wasm code. " \
|
20
|
+
"Defaults to umd",
|
21
|
+
)
|
22
|
+
o.on(
|
23
|
+
"--root NAME",
|
24
|
+
"The html id of the root element to add all other DOM nodes to. " \
|
25
|
+
'Defaults to "root"',
|
26
|
+
)
|
27
|
+
o.on("-v", "--version", "Output version information and exit.")
|
28
|
+
end
|
29
|
+
opts = {
|
30
|
+
template: "umd",
|
31
|
+
document: "document",
|
32
|
+
root: "root",
|
33
|
+
el_prefix: "el",
|
34
|
+
}
|
35
|
+
parser.parse!(args, into: opts)
|
36
|
+
|
37
|
+
filename = args.shift
|
38
|
+
return puts parser.help if filename.nil?
|
39
|
+
|
40
|
+
input = File.read(filename)
|
41
|
+
case opts[:template]
|
42
|
+
when "umd"
|
43
|
+
res = UMDTemplater.new(filename, opts[:root])
|
44
|
+
.generate(input)
|
45
|
+
when "iife"
|
46
|
+
res = IIFETemplater.new(filename, opts[:root])
|
47
|
+
.generate(input)
|
48
|
+
when "nil"
|
49
|
+
res = Templater.new(filename, opts[:root])
|
50
|
+
.generate(input)
|
51
|
+
else
|
52
|
+
raise "Invalid template option. Choose between umd, iife, nil."
|
53
|
+
end
|
54
|
+
puts res
|
55
|
+
end
|
56
|
+
|
57
|
+
module_function :parse
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "better_html"
|
4
|
+
require "better_html/parser"
|
5
|
+
require "better_html/tree/tag"
|
6
|
+
|
7
|
+
require "rerb"
|
8
|
+
require "rerb/ir"
|
9
|
+
|
10
|
+
module RERB
|
11
|
+
# Compile ERB into ruby.wasm compatible code
|
12
|
+
class Compiler
|
13
|
+
SELF_CLOSING_TAGS = [
|
14
|
+
"area",
|
15
|
+
"base",
|
16
|
+
"br",
|
17
|
+
"col",
|
18
|
+
"embed",
|
19
|
+
"hr",
|
20
|
+
"img",
|
21
|
+
"input",
|
22
|
+
"link",
|
23
|
+
"meta",
|
24
|
+
"param",
|
25
|
+
"source",
|
26
|
+
"track",
|
27
|
+
"wbr",
|
28
|
+
].freeze
|
29
|
+
|
30
|
+
Frame = Data.define(:name, :elems) do
|
31
|
+
# Frame is initialized with an empty array for its elems
|
32
|
+
def initialize(name:, elems: [])
|
33
|
+
super(name:, elems:)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(source, viewmodel_name, root_elem_name = "root")
|
38
|
+
@counter = 0
|
39
|
+
@parser = create_parser(source)
|
40
|
+
@viewmodel_name = viewmodel_name
|
41
|
+
@name_hash = Hash.new { |h, k| h[k] = 0 }
|
42
|
+
@root_elem_name = root_elem_name
|
43
|
+
@frames = [Frame[root_elem_name]]
|
44
|
+
end
|
45
|
+
|
46
|
+
def compile
|
47
|
+
<<~RESULT.chomp
|
48
|
+
class #{@viewmodel_name}
|
49
|
+
def initialize
|
50
|
+
setup_dom
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def setup_dom
|
56
|
+
#{compile_body.gsub(/^/, " " * 2)}
|
57
|
+
end
|
58
|
+
|
59
|
+
def document
|
60
|
+
JS.global[:document]
|
61
|
+
end
|
62
|
+
|
63
|
+
def root
|
64
|
+
document.getElementById("#{@root_elem_name}")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
#{@viewmodel_name}.new
|
69
|
+
RESULT
|
70
|
+
end
|
71
|
+
|
72
|
+
def compile_body
|
73
|
+
dom_to_str(compile_ast(@parser.ast)).strip
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def create_parser(source)
|
79
|
+
buffer = Parser::Source::Buffer.new("(buffer)", source:)
|
80
|
+
BetterHtml::Parser.new(buffer)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Unfortunately, this very ugly pattern matching is the only way to
|
84
|
+
# pattern match the AST::Nodes from better-html
|
85
|
+
def compile_ast(node)
|
86
|
+
case node
|
87
|
+
in nil | [:quote, *]
|
88
|
+
IR::Ignore[]
|
89
|
+
|
90
|
+
in String
|
91
|
+
node.strip.empty? ? IR::Ignore[] : IR::Content[node.strip]
|
92
|
+
|
93
|
+
in [:erb, nil, start_trim, code, end_trim] # ERB statement
|
94
|
+
IR::RubyStatement[dom_to_str(compile_ast(code)).strip.to_s]
|
95
|
+
|
96
|
+
in [:erb, _ind, start_trim, code, end_trim] # ERB expression
|
97
|
+
IR::RubyExpr[dom_to_str(compile_ast(code)).strip.to_s]
|
98
|
+
|
99
|
+
in [:tag, nil, tag_name, tag_attr, _end_solidus] # Opening tag
|
100
|
+
tag_type = dom_to_str(compile_ast(tag_name))
|
101
|
+
el_name = generate_el_name(tag_type)
|
102
|
+
@frames << Frame[el_name]
|
103
|
+
attrs = dom_to_str(compile_ast(tag_attr))
|
104
|
+
|
105
|
+
create = IR::Create[el_name, "#{el_name} = document.createElement('#{tag_type}')\n#{attrs}"]
|
106
|
+
return create unless SELF_CLOSING_TAGS.include?(tag_type)
|
107
|
+
|
108
|
+
current_frame.elems << create
|
109
|
+
IR::Content[collect_frame(@frames.pop)]
|
110
|
+
|
111
|
+
in [:tag, _start_solidus, _tag_name, _tag_attr, _end_solidus] # Closing tag
|
112
|
+
IR::Content[collect_frame(@frames.pop)]
|
113
|
+
|
114
|
+
in [:attribute, attr_name, _eql_token, attr_value] # Attribute
|
115
|
+
name = dom_to_str(compile_ast(attr_name))
|
116
|
+
if name[0...2] == "on" # Event
|
117
|
+
value = dom_to_str(compile_ast(attr_value), interpolate: false)
|
118
|
+
IR::Content[%(#{current_frame.name}.addEventListener("#{name[2...]}", #{value})\n)]
|
119
|
+
elsif attr_value.nil? # Boolean attribute
|
120
|
+
IR::Content[%(#{current_frame.name}.setAttribute("#{name}", true)\n)]
|
121
|
+
else
|
122
|
+
value = dom_to_str(compile_ast(attr_value), interpolate: true)
|
123
|
+
IR::Content[%(#{current_frame.name}.setAttribute("#{name}", "#{value}")\n)]
|
124
|
+
end
|
125
|
+
|
126
|
+
in [:attribute_value, _start_quote, value, _end_quote]
|
127
|
+
compile_ast(value)
|
128
|
+
|
129
|
+
in [:code, code]
|
130
|
+
IR::Content["#{code.strip}\n"]
|
131
|
+
|
132
|
+
in [:text, *children]
|
133
|
+
IR::Content[join_text_children(children)]
|
134
|
+
|
135
|
+
in [:document, *] | [:tag_attributes, *]
|
136
|
+
IR::Content[collect_children(node.children, interpolate: false)]
|
137
|
+
|
138
|
+
in [:attribute_name, *] | [:tag_name, *]
|
139
|
+
IR::Content[collect_children(node.children, interpolate: true)]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def join_text_children(children)
|
144
|
+
f_name = current_frame.name
|
145
|
+
children.compact.map do |c|
|
146
|
+
case compile_ast(c)
|
147
|
+
in IR::RubyStatement(content)
|
148
|
+
"#{content}\n"
|
149
|
+
in IR::Ignore
|
150
|
+
""
|
151
|
+
in IR::Content(content)
|
152
|
+
%(#{f_name}.appendChild(document.createTextNode("#{content}"))\n)
|
153
|
+
in IR::RubyExpr(content)
|
154
|
+
%(#{f_name}.appendChild(document.createTextNode("\#{#{content}}"))\n)
|
155
|
+
end
|
156
|
+
end.join
|
157
|
+
end
|
158
|
+
|
159
|
+
def dom_to_str(elem, interpolate: false)
|
160
|
+
case elem
|
161
|
+
in IR::Create(el_name, content)
|
162
|
+
content.to_s + "#{current_frame.name}.appendChild(#{el_name})\n"
|
163
|
+
in IR::Content(content)
|
164
|
+
content.to_s
|
165
|
+
in IR::RubyStatement(content)
|
166
|
+
content.to_s
|
167
|
+
in IR::RubyExpr(content)
|
168
|
+
interpolate ? "\#{#{content}}" : content.to_s
|
169
|
+
in IR::Ignore
|
170
|
+
""
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def collect_frame(frame, interpolate: false)
|
175
|
+
frame.elems.map { |e| dom_to_str(e, interpolate:) }.join
|
176
|
+
end
|
177
|
+
|
178
|
+
def collect_children(children, interpolate: false)
|
179
|
+
@frames << Frame[current_frame.name]
|
180
|
+
|
181
|
+
children.compact.each do |n|
|
182
|
+
# compile_ast must be evaluated BEFORE current_frame because current_frame
|
183
|
+
# must be reflective of the current frame after whatever mutations compile_ast did
|
184
|
+
compiled = compile_ast(n)
|
185
|
+
current_frame.elems << compiled
|
186
|
+
end
|
187
|
+
|
188
|
+
collect_frame(@frames.pop, interpolate:)
|
189
|
+
end
|
190
|
+
|
191
|
+
def current_frame
|
192
|
+
@frames.last or raise EmptyFrameError
|
193
|
+
end
|
194
|
+
|
195
|
+
def generate_el_name(tag_type)
|
196
|
+
@name_hash[tag_type] += 1
|
197
|
+
"@#{tag_type}_#{@name_hash[tag_type]}"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
data/lib/rerb/ir.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RERB
|
4
|
+
# Intermediate Representation Nodes
|
5
|
+
module IR
|
6
|
+
# Generic HTML Content
|
7
|
+
Content = Data.define(:content)
|
8
|
+
# Ruby expression. Analogous to <%= %>
|
9
|
+
RubyExpr = Data.define(:content)
|
10
|
+
# Ruby statement. Analogous to <% %>
|
11
|
+
RubyStatement = Data.define(:content)
|
12
|
+
# Create DOM Node
|
13
|
+
Create = Data.define(:el_name, :content)
|
14
|
+
Ignore = Data.define
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "erb"
|
4
|
+
|
5
|
+
require "rerb"
|
6
|
+
require "rerb/compiler"
|
7
|
+
|
8
|
+
module RERB
|
9
|
+
class Templater
|
10
|
+
TEMPLATE = "<%= content %>"
|
11
|
+
|
12
|
+
def initialize(filename, root_name)
|
13
|
+
@viewmodel_name = File.basename(filename, ".*").split("_").map(&:capitalize).join
|
14
|
+
@root_name = root_name
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate(input)
|
18
|
+
content = Compiler.new(input, @viewmodel_name, @root_name)
|
19
|
+
.compile
|
20
|
+
rhtml = ERB.new(self.class::TEMPLATE)
|
21
|
+
rhtml.result(binding)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class IIFETemplater < Templater
|
26
|
+
TEMPLATE = <<~TMPL.chomp
|
27
|
+
<html>
|
28
|
+
<head>
|
29
|
+
<script src="https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@2.1.0/dist/browser.script.iife.js"></script>
|
30
|
+
<script type="text/ruby">
|
31
|
+
require 'js'
|
32
|
+
|
33
|
+
<%= content.gsub(/^(?!$)/, ' ' * 3) %>
|
34
|
+
</script>
|
35
|
+
</head>
|
36
|
+
<body>
|
37
|
+
<div id="<%= @root_name %>"></div>
|
38
|
+
</body>
|
39
|
+
</html>
|
40
|
+
TMPL
|
41
|
+
end
|
42
|
+
|
43
|
+
class UMDTemplater < Templater
|
44
|
+
TEMPLATE = <<~TMPL.chomp
|
45
|
+
<html>
|
46
|
+
<script src="https://cdn.jsdelivr.net/npm/@ruby/wasm-wasi@latest/dist/browser.umd.js"></script>
|
47
|
+
<script>
|
48
|
+
const { DefaultRubyVM } = window["ruby-wasm-wasi"];
|
49
|
+
const main = async () => {
|
50
|
+
// Fetch and instantiate WebAssembly binary
|
51
|
+
const response = await fetch(
|
52
|
+
// Tips: Replace the binary with debug info if you want symbolicated stack trace.
|
53
|
+
// (only nightly release for now)
|
54
|
+
// "https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@next/dist/ruby.debug+stdlib.wasm"
|
55
|
+
"https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@latest/dist/ruby+stdlib.wasm"
|
56
|
+
);
|
57
|
+
const buffer = await response.arrayBuffer();
|
58
|
+
const module = await WebAssembly.compile(buffer);
|
59
|
+
const { vm } = await DefaultRubyVM(module);
|
60
|
+
|
61
|
+
vm.printVersion();
|
62
|
+
vm.eval(`
|
63
|
+
require 'js'
|
64
|
+
|
65
|
+
<%= content.gsub(/^(?!$)/, ' ' * 4) %>
|
66
|
+
`);
|
67
|
+
};
|
68
|
+
|
69
|
+
main();
|
70
|
+
</script>
|
71
|
+
<body>
|
72
|
+
<div id="<%= @root_name %>"></div>
|
73
|
+
</body>
|
74
|
+
</html>
|
75
|
+
TMPL
|
76
|
+
end
|
77
|
+
end
|
data/lib/rerb/version.rb
ADDED
data/lib/rerb.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "rerb/version"
|
4
|
+
require "rerb/compiler"
|
5
|
+
require "rerb/templater"
|
6
|
+
require "rerb/cli"
|
7
|
+
require "rerb/ir"
|
8
|
+
|
9
|
+
module RERB
|
10
|
+
class Error < StandardError; end
|
11
|
+
|
12
|
+
class EmptyFrameError < Error
|
13
|
+
def initialize(message = "Frames list is empty")
|
14
|
+
super
|
15
|
+
@message = message
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/sig/rerb/cli.rbs
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# Classes
|
2
|
+
module BetterHtml
|
3
|
+
class Parser
|
4
|
+
def initialize: (untyped buffer) -> void
|
5
|
+
def ast: -> AST::Node
|
6
|
+
end
|
7
|
+
|
8
|
+
module AST
|
9
|
+
class Node
|
10
|
+
attr_reader children: Array[Node | String]
|
11
|
+
attr_reader type: Symbol
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module RERB
|
17
|
+
# Compile ERB into ruby.wasm compatible code
|
18
|
+
class Compiler
|
19
|
+
class Frame < Data
|
20
|
+
attr_reader name: String
|
21
|
+
attr_reader elems: Array[IR::node]
|
22
|
+
|
23
|
+
def initialize: (String, ?Array[IR::node]) -> void
|
24
|
+
end
|
25
|
+
|
26
|
+
@counter: Integer
|
27
|
+
|
28
|
+
@parser: BetterHtml::Parser
|
29
|
+
|
30
|
+
@viewmodel_name: String
|
31
|
+
|
32
|
+
@name_hash: Hash[String, Integer]
|
33
|
+
|
34
|
+
@root_elem_name: String
|
35
|
+
|
36
|
+
@frames: Array[Frame]
|
37
|
+
|
38
|
+
def initialize: (String source,
|
39
|
+
String viewmodel_name,
|
40
|
+
?String root_elem_name) -> void
|
41
|
+
|
42
|
+
def compile: () -> String
|
43
|
+
|
44
|
+
def compile_body: () -> String
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def create_parser: (String source) -> BetterHtml::Parser
|
49
|
+
|
50
|
+
def compile_ast: (BetterHtml::AST::Node | String node) -> IR::node
|
51
|
+
|
52
|
+
def join_text_children: (Array[IR::node | nil] children) -> String
|
53
|
+
|
54
|
+
def dom_to_str: (IR::node elem, ?interpolate: bool) -> String
|
55
|
+
|
56
|
+
def collect_frame: (Frame frame, ?interpolate: bool) -> untyped
|
57
|
+
|
58
|
+
def collect_children: (Array[BetterHtml::AST::Node] children, ?interpolate: bool) -> untyped
|
59
|
+
|
60
|
+
def current_frame: () -> Frame
|
61
|
+
|
62
|
+
def generate_el_name: (String) -> String
|
63
|
+
end
|
64
|
+
end
|
data/sig/rerb/ir.rbs
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
module RERB
|
2
|
+
# Intermediate Representation Nodes
|
3
|
+
module IR
|
4
|
+
class Content < Data end
|
5
|
+
class RubyExpr < Data end
|
6
|
+
class RubyStatement < Data end
|
7
|
+
class Create < Data end
|
8
|
+
class Ignore < Data end
|
9
|
+
|
10
|
+
type node = Content | RubyExpr | RubyStatement | Create | Ignore
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module RERB
|
2
|
+
class Templater
|
3
|
+
@viewmodel_name: String
|
4
|
+
|
5
|
+
@root_name: String
|
6
|
+
|
7
|
+
@el_name_prefix: String
|
8
|
+
|
9
|
+
TEMPLATE: String
|
10
|
+
|
11
|
+
def initialize: (String filename, String root_name) -> void
|
12
|
+
|
13
|
+
def generate: (String input) -> String
|
14
|
+
end
|
15
|
+
|
16
|
+
class IIFETemplater < Templater
|
17
|
+
TEMPLATE: String
|
18
|
+
end
|
19
|
+
|
20
|
+
class UMDTemplater < Templater
|
21
|
+
TEMPLATE: String
|
22
|
+
end
|
23
|
+
end
|
data/sig/rerb.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rerb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Forthoney
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-11-15 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: RERB is an unopinionated tool for compiling ERB/rhtml into ruby.wasm
|
14
|
+
DOM operations. It generates code which, when run on a Ruby VM on WASM, generate
|
15
|
+
the desired DOM.
|
16
|
+
email:
|
17
|
+
- castlehoneyjung@gmail.com
|
18
|
+
executables:
|
19
|
+
- rerb
|
20
|
+
extensions: []
|
21
|
+
extra_rdoc_files: []
|
22
|
+
files:
|
23
|
+
- ".rspec"
|
24
|
+
- ".rubocop.yml"
|
25
|
+
- ".ruby-version"
|
26
|
+
- CHANGELOG.md
|
27
|
+
- CODE_OF_CONDUCT.md
|
28
|
+
- LICENSE.txt
|
29
|
+
- README.md
|
30
|
+
- Rakefile
|
31
|
+
- Steepfile
|
32
|
+
- docs/capabilities.md
|
33
|
+
- exe/rerb
|
34
|
+
- lib/rerb.rb
|
35
|
+
- lib/rerb/cli.rb
|
36
|
+
- lib/rerb/compiler.rb
|
37
|
+
- lib/rerb/ir.rb
|
38
|
+
- lib/rerb/templater.rb
|
39
|
+
- lib/rerb/version.rb
|
40
|
+
- sig/rerb.rbs
|
41
|
+
- sig/rerb/cli.rbs
|
42
|
+
- sig/rerb/compiler.rbs
|
43
|
+
- sig/rerb/ir.rbs
|
44
|
+
- sig/rerb/templater.rbs
|
45
|
+
homepage: https://github.com/forthoney/werb
|
46
|
+
licenses:
|
47
|
+
- MIT
|
48
|
+
metadata:
|
49
|
+
homepage_uri: https://github.com/forthoney/werb
|
50
|
+
source_code_uri: https://github.com/forthoney/rerb
|
51
|
+
changelog_uri: https://github.com/Forthoney/rerb/blob/main/CHANGELOG.md
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '3.2'
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubygems_version: 3.4.21
|
68
|
+
signing_key:
|
69
|
+
specification_version: 4
|
70
|
+
summary: Build a DOM for ruby.wasm with ERB
|
71
|
+
test_files: []
|