component_embedded_ruby 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 834b63b9cc7bf826b998ff5397391e477001e549da31d058243745db63d633e4
4
+ data.tar.gz: 6742bc330ce2dd5b9fdce7325759338bc966cc016955ca2156ee44e2187e1187
5
+ SHA512:
6
+ metadata.gz: 24c07b6614384d476903ab99d892373625cf017b3428c6a5ba2b00a328f07b0dd614ce178235e23d4a333662379719acdef3dc5f28be74eb0d1e68f4144e4cf8
7
+ data.tar.gz: 181d8e02bc4746860064231b6e19cc834c1b550c0c686aca1f12703bda0894030d9e1c444c2b104edec7e364606a675b8746e789331e52789c807ec2c812a3f7
@@ -0,0 +1,20 @@
1
+ name: Ruby
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ build:
7
+
8
+ runs-on: ubuntu-latest
9
+
10
+ steps:
11
+ - uses: actions/checkout@v1
12
+ - name: Set up Ruby 2.6
13
+ uses: actions/setup-ruby@v1
14
+ with:
15
+ ruby-version: 2.6.x
16
+ - name: Build and test with Rake
17
+ run: |
18
+ gem install bundler
19
+ bundle install --jobs 4 --retry 3
20
+ bundle exec rake
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ test/test_application/tmp
10
+ test/test_application/log
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.0
7
+ before_install: gem install bundler -v 1.17.2
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ gem "rails", github: "rails/rails"
6
+
7
+ gemspec
@@ -0,0 +1,160 @@
1
+ GIT
2
+ remote: https://github.com/rails/rails
3
+ revision: 7e2d8434ed42d6fbf7f4743d033d50ad92ebe4da
4
+ specs:
5
+ actioncable (6.1.0.alpha)
6
+ actionpack (= 6.1.0.alpha)
7
+ activesupport (= 6.1.0.alpha)
8
+ nio4r (~> 2.0)
9
+ websocket-driver (>= 0.6.1)
10
+ actionmailbox (6.1.0.alpha)
11
+ actionpack (= 6.1.0.alpha)
12
+ activejob (= 6.1.0.alpha)
13
+ activerecord (= 6.1.0.alpha)
14
+ activestorage (= 6.1.0.alpha)
15
+ activesupport (= 6.1.0.alpha)
16
+ mail (>= 2.7.1)
17
+ actionmailer (6.1.0.alpha)
18
+ actionpack (= 6.1.0.alpha)
19
+ actionview (= 6.1.0.alpha)
20
+ activejob (= 6.1.0.alpha)
21
+ activesupport (= 6.1.0.alpha)
22
+ mail (~> 2.5, >= 2.5.4)
23
+ rails-dom-testing (~> 2.0)
24
+ actionpack (6.1.0.alpha)
25
+ actionview (= 6.1.0.alpha)
26
+ activesupport (= 6.1.0.alpha)
27
+ rack (~> 2.0, >= 2.0.9)
28
+ rack-test (>= 0.6.3)
29
+ rails-dom-testing (~> 2.0)
30
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
31
+ actiontext (6.1.0.alpha)
32
+ actionpack (= 6.1.0.alpha)
33
+ activerecord (= 6.1.0.alpha)
34
+ activestorage (= 6.1.0.alpha)
35
+ activesupport (= 6.1.0.alpha)
36
+ nokogiri (>= 1.8.5)
37
+ actionview (6.1.0.alpha)
38
+ activesupport (= 6.1.0.alpha)
39
+ builder (~> 3.1)
40
+ erubi (~> 1.4)
41
+ rails-dom-testing (~> 2.0)
42
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
43
+ activejob (6.1.0.alpha)
44
+ activesupport (= 6.1.0.alpha)
45
+ globalid (>= 0.3.6)
46
+ activemodel (6.1.0.alpha)
47
+ activesupport (= 6.1.0.alpha)
48
+ activerecord (6.1.0.alpha)
49
+ activemodel (= 6.1.0.alpha)
50
+ activesupport (= 6.1.0.alpha)
51
+ activestorage (6.1.0.alpha)
52
+ actionpack (= 6.1.0.alpha)
53
+ activejob (= 6.1.0.alpha)
54
+ activerecord (= 6.1.0.alpha)
55
+ activesupport (= 6.1.0.alpha)
56
+ marcel (~> 0.3.1)
57
+ mimemagic (~> 0.3.2)
58
+ activesupport (6.1.0.alpha)
59
+ concurrent-ruby (~> 1.0, >= 1.0.2)
60
+ i18n (>= 1.6, < 2)
61
+ minitest (>= 5.1)
62
+ tzinfo (~> 2.0)
63
+ zeitwerk (~> 2.3)
64
+ rails (6.1.0.alpha)
65
+ actioncable (= 6.1.0.alpha)
66
+ actionmailbox (= 6.1.0.alpha)
67
+ actionmailer (= 6.1.0.alpha)
68
+ actionpack (= 6.1.0.alpha)
69
+ actiontext (= 6.1.0.alpha)
70
+ actionview (= 6.1.0.alpha)
71
+ activejob (= 6.1.0.alpha)
72
+ activemodel (= 6.1.0.alpha)
73
+ activerecord (= 6.1.0.alpha)
74
+ activestorage (= 6.1.0.alpha)
75
+ activesupport (= 6.1.0.alpha)
76
+ bundler (>= 1.15.0)
77
+ railties (= 6.1.0.alpha)
78
+ sprockets-rails (>= 2.0.0)
79
+ railties (6.1.0.alpha)
80
+ actionpack (= 6.1.0.alpha)
81
+ activesupport (= 6.1.0.alpha)
82
+ method_source
83
+ rake (>= 0.8.7)
84
+ thor (~> 1.0)
85
+
86
+ PATH
87
+ remote: .
88
+ specs:
89
+ component_embedded_ruby (0.2.0)
90
+
91
+ GEM
92
+ remote: https://rubygems.org/
93
+ specs:
94
+ builder (3.2.4)
95
+ coderay (1.1.2)
96
+ concurrent-ruby (1.1.7)
97
+ crass (1.0.6)
98
+ erubi (1.9.0)
99
+ globalid (0.4.2)
100
+ activesupport (>= 4.2.0)
101
+ i18n (1.8.5)
102
+ concurrent-ruby (~> 1.0)
103
+ loofah (2.7.0)
104
+ crass (~> 1.0.2)
105
+ nokogiri (>= 1.5.9)
106
+ mail (2.7.1)
107
+ mini_mime (>= 0.1.1)
108
+ marcel (0.3.3)
109
+ mimemagic (~> 0.3.2)
110
+ method_source (0.9.2)
111
+ mimemagic (0.3.5)
112
+ mini_mime (1.0.2)
113
+ mini_portile2 (2.4.0)
114
+ minitest (5.11.3)
115
+ nio4r (2.5.4)
116
+ nokogiri (1.10.10)
117
+ mini_portile2 (~> 2.4.0)
118
+ pry (0.12.2)
119
+ coderay (~> 1.1.0)
120
+ method_source (~> 0.9.0)
121
+ rack (2.2.3)
122
+ rack-test (1.1.0)
123
+ rack (>= 1.0, < 3)
124
+ rails-dom-testing (2.0.3)
125
+ activesupport (>= 4.2.0)
126
+ nokogiri (>= 1.6)
127
+ rails-html-sanitizer (1.3.0)
128
+ loofah (~> 2.3)
129
+ rake (13.0.1)
130
+ sprockets (4.0.2)
131
+ concurrent-ruby (~> 1.0)
132
+ rack (> 1, < 3)
133
+ sprockets-rails (3.2.2)
134
+ actionpack (>= 4.0)
135
+ activesupport (>= 4.0)
136
+ sprockets (>= 3.0.0)
137
+ thor (1.0.1)
138
+ tzinfo (2.0.2)
139
+ concurrent-ruby (~> 1.0)
140
+ view_component (2.20.0)
141
+ activesupport (>= 5.0.0, < 7.0)
142
+ websocket-driver (0.7.3)
143
+ websocket-extensions (>= 0.1.0)
144
+ websocket-extensions (0.1.5)
145
+ zeitwerk (2.4.0)
146
+
147
+ PLATFORMS
148
+ ruby
149
+
150
+ DEPENDENCIES
151
+ bundler (~> 1.17)
152
+ component_embedded_ruby!
153
+ minitest (~> 5.0)
154
+ pry (~> 0.12.2)
155
+ rails!
156
+ rake (~> 13.0)
157
+ view_component (> 2.15)
158
+
159
+ BUNDLED WITH
160
+ 1.17.3
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Blake Williams
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.
@@ -0,0 +1,80 @@
1
+ # Component Embedded Ruby
2
+
3
+ Strict HTML templating with support for components.
4
+
5
+ ### Features:
6
+
7
+ * Strict HTML parsing. eg: matching end tags are enforced
8
+ * HTML attributes are either static, or dynamic. No more `class="hello <%=
9
+ extra_classes %>`, instead this logic should be pushed up to components.
10
+ * Component rendering has a single dependency, a `render` method being present
11
+ in the rendering context.
12
+ * Easy Rails integration by registering `crb` as a template handler.
13
+
14
+ ### Usage
15
+
16
+ Define a template:
17
+
18
+ ```ruby
19
+ <h1>
20
+ <Capitalization upcase={true}>hello world</Capitalization>
21
+ </h1>
22
+ ```
23
+
24
+ Define a component
25
+
26
+ ```ruby
27
+ class Capitalization
28
+ def initialize(upcase: false)
29
+ @upcase = upcase
30
+ end
31
+
32
+ def render_in(_view_context)
33
+ children = yield
34
+
35
+ if @upcase
36
+ children.upcase
37
+ else
38
+ children
39
+ end
40
+ end
41
+ end
42
+ ```
43
+
44
+ Render it
45
+
46
+ ```ruby
47
+ ComponentEmbeddedRuby.render(template_string)
48
+ ```
49
+
50
+ See results
51
+
52
+
53
+ ```html
54
+ <h1>HELLO WORLD</h1>
55
+ ```
56
+
57
+ If trying to render outside of a Rails environment, ensure that the binding
58
+ passed to the renderer has a top-level `render` method that can accept component
59
+ instances and convert them to strings.
60
+
61
+
62
+ e.g. the most basic example could look like this:
63
+
64
+ ```ruby
65
+ def render(renderable, &block)
66
+ # This assumes components being rendered utilize `to_s` to render their
67
+ # templates
68
+ renderable.to_s(&block)
69
+ end
70
+ ```
71
+
72
+ For more examples, check out the `ComponentEmbeddedRuby::Renderable` tests.
73
+
74
+ ## Contributing
75
+
76
+ Bug reports and pull requests are welcome on GitHub at https://github.com/BlakeWilliams/component_embedded_ruby.
77
+
78
+ ## License
79
+
80
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "component_embedded_ruby"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,42 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "component_embedded_ruby/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "component_embedded_ruby"
8
+ spec.version = ComponentEmbeddedRuby::VERSION
9
+ spec.authors = ["Blake Williams"]
10
+ spec.email = ["blake@blakewilliams.me"]
11
+
12
+ spec.summary = %q{HTML templates with embedded Ruby components}
13
+ spec.description = %q{HTML templates with embedded Ruby components}
14
+ spec.homepage = "https://github.com/blakewilliams/component_embedded_ruby"
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["homepage_uri"] = spec.homepage
21
+ spec.metadata["source_code_uri"] = "https://github.com/blakewilliams/component_embedded_ruby"
22
+ else
23
+ raise "RubyGems 2.0 or newer is required to protect against " \
24
+ "public gem pushes."
25
+ end
26
+
27
+ # Specify which files should be added to the gem when it is released.
28
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
29
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
30
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
31
+ end
32
+ spec.bindir = "exe"
33
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
34
+ spec.require_paths = ["lib"]
35
+
36
+ spec.add_development_dependency "bundler", "~> 1.17"
37
+ spec.add_development_dependency "rake", "~> 13.0"
38
+ spec.add_development_dependency "minitest", "~> 5.0"
39
+ spec.add_development_dependency "rails", "> 6.0"
40
+ spec.add_development_dependency "view_component", "> 2.15"
41
+ spec.add_development_dependency "pry", "~> 0.12.2"
42
+ end
@@ -0,0 +1,12 @@
1
+ require "component_embedded_ruby/version"
2
+ require "component_embedded_ruby/lexer"
3
+ require "component_embedded_ruby/parser"
4
+ require "component_embedded_ruby/eval"
5
+ require "component_embedded_ruby/node"
6
+ require "component_embedded_ruby/renderer"
7
+ require "component_embedded_ruby/template"
8
+ require "component_embedded_ruby/unexpected_token_error"
9
+
10
+ module ComponentEmbeddedRuby
11
+ class Error < StandardError; end
12
+ end
@@ -0,0 +1,18 @@
1
+ module ComponentEmbeddedRuby
2
+ class Eval
3
+ attr_reader :value, :output
4
+
5
+ def initialize(value, output: true)
6
+ @value = value
7
+ @output = output
8
+ end
9
+
10
+ def eval(binding)
11
+ binding.instance_eval(value)
12
+ end
13
+
14
+ def ==(other)
15
+ other.value == value
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,145 @@
1
+ require "component_embedded_ruby/lexer/input_reader"
2
+
3
+ module ComponentEmbeddedRuby
4
+ class Lexer
5
+ Token = Struct.new(:type, :value, :position)
6
+ Position = Struct.new(:line, :column)
7
+
8
+ def initialize(content)
9
+ @reader = InputReader.new(content)
10
+
11
+ @tokens = []
12
+ end
13
+
14
+ def lex
15
+ while !reader.eof?
16
+ char = reader.current_char
17
+
18
+ if char == "<"
19
+ add_token(:open_carrot, "<")
20
+ reader.next
21
+ elsif char == ">"
22
+ add_token(:close_carrot, ">")
23
+ reader.next
24
+ elsif char == "="
25
+ add_token(:equals, "=")
26
+ reader.next
27
+ elsif char == "\""
28
+ add_token(:string, read_quoted_string)
29
+ elsif char == "/"
30
+ add_token(:slash, "/")
31
+ reader.next
32
+ elsif char == "{"
33
+ if reader.peek == "-"
34
+ reader.next
35
+ add_token(:ruby_no_eval, read_ruby_string)
36
+ else
37
+ add_token(:ruby, read_ruby_string)
38
+ end
39
+ elsif is_letter?(char)
40
+ if @tokens[-1]&.type == :close_carrot
41
+ add_token(:string, read_body_string)
42
+ else
43
+ add_token(:identifier, read_string)
44
+ end
45
+ else
46
+ reader.next
47
+ end
48
+ end
49
+
50
+ @tokens
51
+ end
52
+
53
+ private
54
+
55
+ attr_reader :reader
56
+ attr_accessor :position
57
+
58
+ def add_token(type, value)
59
+ token = Token.new(type, value, Position.new(reader.current_line, reader.current_column))
60
+ @tokens << token
61
+ end
62
+
63
+ def read_string
64
+ string = ""
65
+
66
+ while is_letter?(reader.current_char) && !reader.eof?
67
+ string += reader.current_char
68
+ reader.next
69
+ end
70
+
71
+ string
72
+ end
73
+
74
+ def read_quoted_string
75
+ string = ""
76
+
77
+ # Get past initial "
78
+ reader.next
79
+
80
+ while !unescaped_quote?
81
+ raise "unterminated string" if reader.eof?
82
+ string += reader.current_char
83
+ reader.next
84
+ end
85
+
86
+ # Get past last "
87
+ reader.next
88
+
89
+ string
90
+ end
91
+
92
+ def read_body_string
93
+ string = ""
94
+
95
+ while reader.current_char != "<" && reader.current_char != "{"
96
+ raise "unterminated content" if reader.eof?
97
+
98
+ string += reader.current_char
99
+ reader.next
100
+ end
101
+
102
+ string
103
+ end
104
+
105
+ def read_ruby_string
106
+ inner_string_count = 0
107
+ inner_bracket_count = 0
108
+
109
+ string = ""
110
+
111
+ reader.next
112
+
113
+ previous_token = nil
114
+
115
+ loop do
116
+ break if inner_bracket_count == 0 && inner_string_count % 2 == 0 && reader.current_char == "}"
117
+ char = reader.current_char
118
+ string += char
119
+
120
+ # TODO handle " and ' separately
121
+ if inner_string_count % 2 == 0 && char == "{"
122
+ inner_bracket_count += 1
123
+ elsif inner_string_count % 2 == 0 && char == "}"
124
+ inner_bracket_count -= 1
125
+ elsif previous_token != "\\" && char == "\"" || char == "'"
126
+ inner_string_count -= 1
127
+ end
128
+
129
+ previous_token = char
130
+ reader.next
131
+ end
132
+
133
+ string
134
+ end
135
+
136
+ def unescaped_quote?
137
+ reader.current_char == "\"" && reader.peek_behind != "\\"
138
+ end
139
+
140
+ def is_letter?(char)
141
+ ascii = char.ord
142
+ (ascii >= 48 && ascii <= 57) || (ascii >= 65 && ascii <= 122) || ascii == 45 || ascii == 95 || ascii == 58
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,46 @@
1
+ module ComponentEmbeddedRuby
2
+ class Lexer
3
+ class InputReader
4
+ attr_reader :current_line, :current_column
5
+
6
+ def initialize(input)
7
+ @input = input.freeze
8
+ @position = 0
9
+
10
+ @current_line = 0
11
+ @current_column = 0
12
+ end
13
+
14
+ def eof?
15
+ @position == @input.length
16
+ end
17
+
18
+ def current_char
19
+ input[@position]
20
+ end
21
+
22
+ def peek
23
+ @input[@position + 1]
24
+ end
25
+
26
+ def peek_behind
27
+ @input[@position - 1]
28
+ end
29
+
30
+ def next
31
+ if current_char == "\n"
32
+ @current_line += 1
33
+ @current_column = 0
34
+ else
35
+ @current_column += 1
36
+ end
37
+
38
+ @position += 1
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :input
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,44 @@
1
+ module ComponentEmbeddedRuby
2
+ class Node
3
+ attr_reader :tag, :attributes, :children
4
+
5
+ def initialize(tag, attributes, children)
6
+ @tag = tag
7
+ @attributes = attributes
8
+ @children = children
9
+ end
10
+
11
+ def ==(other)
12
+ if other
13
+ other.tag == tag && other.attributes == attributes && other.children == children
14
+ else
15
+ false
16
+ end
17
+ end
18
+
19
+ def component_class
20
+ @_component_class = Object.const_get(tag)
21
+ end
22
+
23
+ # If the tag starts with a capital, we assume it's a component
24
+ def component?
25
+ @_component ||= tag && !!/[[:upper:]]/.match(tag[0])
26
+ end
27
+
28
+ def ruby?
29
+ children.is_a?(Eval)
30
+ end
31
+
32
+ def output_ruby?
33
+ ruby? && children.output
34
+ end
35
+
36
+ def text?
37
+ tag.nil? && !ruby?
38
+ end
39
+
40
+ def html?
41
+ !component? && tag
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,15 @@
1
+ require "component_embedded_ruby/parser/base"
2
+ require "component_embedded_ruby/parser/root_parser"
3
+ require "component_embedded_ruby/parser/attribute_parser"
4
+ require "component_embedded_ruby/parser/tag_parser"
5
+ require "component_embedded_ruby/parser/token_reader"
6
+
7
+ module ComponentEmbeddedRuby
8
+ module Parser
9
+ def self.parse(tokens)
10
+ @token_reader = TokenReader.new(tokens)
11
+
12
+ RootParser.new(@token_reader).call
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,50 @@
1
+ module ComponentEmbeddedRuby
2
+ module Parser
3
+ # Internal: Parses an HTML tag attributes into a hash of key values
4
+ #
5
+ # This class parses HTML attributes into a hash of key values, keys are
6
+ # always strings but since values can be dynamic, they will either be a
7
+ # string or an instance of `Eval`.
8
+ #
9
+ # Given how we parse these attributes, they are intentionally either a
10
+ # string or Ruby, not a combination of the two.
11
+ #
12
+ # Valid attributes may look like `id="document" class={my_classes}`
13
+ #
14
+ # The following is invalid `class="mb-0 {my_classes}"`
15
+ #
16
+ class AttributeParser < Base
17
+ def call
18
+ attributes = {}
19
+
20
+ while current_token.type != :close_carrot && current_token.type != :slash
21
+ attributes.merge!(parse_attribute)
22
+ end
23
+
24
+ attributes
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :token_reader
30
+
31
+ def parse_attribute
32
+ key = expect(:identifier).value
33
+ expect(:equals)
34
+ value = parse_value
35
+
36
+ { key => value }
37
+ end
38
+
39
+ def parse_value
40
+ value_token = expect_any(:string, :ruby)
41
+
42
+ if value_token.type == :string
43
+ value_token.value
44
+ else
45
+ Eval.new(value_token.value)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,45 @@
1
+ module ComponentEmbeddedRuby
2
+ module Parser
3
+ class Base
4
+ def initialize(token_reader)
5
+ @token_reader = token_reader
6
+ end
7
+
8
+ private
9
+
10
+ attr_reader :token_reader
11
+
12
+ def current_token
13
+ token_reader.current_token
14
+ end
15
+
16
+ def peek_token
17
+ token_reader.peek_token
18
+ end
19
+
20
+ def expect(type)
21
+ token = current_token
22
+
23
+ if token.type != type
24
+ raise UnexpectedTokenError.new(:string, current_token)
25
+ else
26
+ token_reader.next
27
+ end
28
+
29
+ token
30
+ end
31
+
32
+ def expect_any(*types)
33
+ token = current_token
34
+
35
+ if !types.include?(token.type)
36
+ raise UnexpectedTokenError.new(:string, token)
37
+ else
38
+ token_reader.next
39
+ end
40
+
41
+ token
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,51 @@
1
+ module ComponentEmbeddedRuby
2
+ module Parser
3
+ # Internal: Used for parsing multiple adjacent tag, string, and emedded
4
+ # ruby into an array.
5
+ #
6
+ # This parser is used to parse top-level documents and the children of
7
+ # `tag` nodes, which may have any combination of adjacent tag, string, and
8
+ # ruby nodes.
9
+ #
10
+ class RootParser < Base
11
+ def call
12
+ results = []
13
+
14
+ while current_token
15
+ if current_token.type == :open_carrot
16
+ # If we run into a </ we are likely at the end of parsing a tag so
17
+ # this should return and let the `TagParser` complete parsing
18
+ #
19
+ # e.g.
20
+ # 1. <h1>Hello</h1> would start a `TagParser` after reading <h1
21
+ # 2. `TagParser` reads <h1>, sees that it has
22
+ # children, and will use another instance of `RootParser` to reads its children
23
+ # 3. The new RootParser reads `Hello`, then runs into `</`, so it should return `["Hello"]`
24
+ # and allow the `TagParser` to finish reading `</h1>`
25
+ if peek_token.type == :slash
26
+ return results
27
+ else
28
+ results << TagParser.new(token_reader).call
29
+ end
30
+ elsif current_token.type == :string || current_token.type == :identifier
31
+ # If we're reading a string, or some other identifier that is on
32
+ # its own, we can skip instantiating a new parser and parse it directly ourselves
33
+ results << Node.new(nil, nil, current_token.value)
34
+ token_reader.next
35
+ elsif current_token.type == :ruby || current_token.type == :ruby_no_eval
36
+ # If we run into Ruby code that should be evaluated inside of the
37
+ # template, we want to create an `Eval`. The compliation step
38
+ # handles `Eval` objects specially since it's making template
39
+ # provided ruby code compatibile with the compiled template code.
40
+ value = Eval.new(current_token.value, output: current_token.type == :ruby)
41
+
42
+ results << Node.new(nil, nil, value)
43
+ token_reader.next
44
+ end
45
+ end
46
+
47
+ results
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,73 @@
1
+ module ComponentEmbeddedRuby
2
+ module Parser
3
+ # Internal: Parses an HTML tag into a Node object
4
+ #
5
+ # This class is responsible for parsing an HTML element into a node
6
+ # instance. There are three variations of tags:
7
+ #
8
+ # * A self-closing tag e.g. <hr/>
9
+ # * A tag with no children <img src="/favicon.png"></img>
10
+ # * A tag with children e.g. <b>bold text!</b>
11
+ #
12
+ # A tag with children will delegate back to the `RootParser` since child
13
+ # elements can be any combination of adjacent tags, strings, and ruby code.
14
+ #
15
+ class TagParser < Base
16
+ def call
17
+ # Expect opening carrot, e.g. < in <h1>
18
+ expect(:open_carrot)
19
+
20
+ # Expects an identifier, e.g. "h1"
21
+ tag = expect(:identifier).value
22
+
23
+ # Expects 0 or more attributes
24
+ # e.g. id="hello" in <h1 id="hello">
25
+ attributes = AttributeParser.new(token_reader).call
26
+
27
+ # Is this a self-closing element?
28
+ if current_token.type == :slash
29
+ expect(:slash)
30
+ expect(:close_carrot)
31
+
32
+ Node.new(tag, attributes, [])
33
+ else
34
+ expect(:close_carrot)
35
+
36
+ children = parse_children
37
+
38
+ expect(:open_carrot)
39
+ expect(:slash)
40
+ close_tag = expect(:identifier).value
41
+
42
+ if close_tag != tag
43
+ raise "Mismatched tags. expected #{tag}, got #{current_token.value}"
44
+ end
45
+
46
+ expect(:close_carrot)
47
+
48
+ Node.new(tag, attributes, children)
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ # If the next two elements are </, we can safely asume it's meant to
55
+ # close the current tag and lets us avoid having to attempt parsing
56
+ # children.
57
+ def has_children?
58
+ return true if current_token.type != :open_carrot
59
+ return true if peek_token&.type != :slash
60
+
61
+ false
62
+ end
63
+
64
+ def parse_children
65
+ if has_children?
66
+ RootParser.new(token_reader).call
67
+ else
68
+ []
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,22 @@
1
+ module ComponentEmbeddedRuby
2
+ module Parser
3
+ class TokenReader
4
+ def initialize(tokens)
5
+ @tokens = tokens
6
+ @position = 0
7
+ end
8
+
9
+ def current_token
10
+ @tokens[@position]
11
+ end
12
+
13
+ def peek_token
14
+ @tokens[@position + 1]
15
+ end
16
+
17
+ def next
18
+ @position += 1
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,85 @@
1
+ module ComponentEmbeddedRuby
2
+ class Renderer
3
+ def initialize(nodes, output_var_name: "__crb_out", skip_return: false)
4
+ @nodes = Array(nodes)
5
+ @functions = {}
6
+ @output_var_name = output_var_name
7
+ @skip_return = skip_return
8
+ end
9
+
10
+ def to_ruby
11
+ <<~EOF
12
+ #{output_var_name} = '';
13
+
14
+ #{nodes.map(&method(:render)).join("\n")}
15
+
16
+ #{output_var_name if !@skip_return};
17
+ EOF
18
+ end
19
+
20
+ private
21
+
22
+ def render(node)
23
+ if node.component?
24
+ <<~EOF
25
+ #{children_to_ruby(node)}
26
+ #{output_var_name}.<< render(#{node.component_class}.new(#{attributes_for_component(node).join(",")})) { |component|
27
+ __c_#{node.hash.to_s.gsub("-", "_")}
28
+ };
29
+ EOF
30
+ elsif node.ruby?
31
+ if node.output_ruby?
32
+ "#{output_var_name}.<< (#{node.children.value}).to_s;\n"
33
+ else
34
+ "#{node.children.value};\n"
35
+ end
36
+ elsif node.text?
37
+ "#{output_var_name}.<< \"#{node.children}\";\n"
38
+ elsif node.html?
39
+ <<~EOF
40
+ #{output_var_name}.<< \"<#{node.tag}\";
41
+ #{attributes_for_tag(node).join("\n")};
42
+ #{output_var_name}.<< \">\";
43
+ #{node.children.map(&method(:render)).join("\n")}
44
+ #{output_var_name}.<< \"</#{node.tag}>\";
45
+ EOF
46
+ end
47
+ end
48
+
49
+ attr_reader :output_var_name
50
+
51
+ def children_to_ruby(node)
52
+ self.class.new(
53
+ node.children,
54
+ output_var_name: "__c_#{node.hash.to_s.gsub("-", "_")}",
55
+ skip_return: true
56
+ ).to_ruby
57
+ end
58
+
59
+ def attributes_for_component(node)
60
+ node.attributes.map do |key, value|
61
+ if value.is_a?(Eval)
62
+ " #{key}: #{value.value}"
63
+ else
64
+ " #{key}: \"#{value}\""
65
+ end
66
+ end
67
+ end
68
+
69
+ def attributes_for_tag(node)
70
+ node.attributes.map do |key, value|
71
+ if value.is_a?(Eval)
72
+ <<~EOF
73
+ #{output_var_name}.<< " #{key}=\\"";
74
+ #{output_var_name}.<< (#{value.value}).to_s;
75
+ #{output_var_name}.<< "\\"";
76
+ EOF
77
+ else
78
+ %W(#{output_var_name}.<< "key="";\n)
79
+ end
80
+ end
81
+ end
82
+
83
+ attr_reader :nodes
84
+ end
85
+ end
@@ -0,0 +1,17 @@
1
+ module ComponentEmbeddedRuby
2
+ class Template
3
+ def initialize(template)
4
+ @template = template
5
+ end
6
+
7
+ def to_ruby
8
+ tokens = Lexer.new(@template).lex
9
+ nodes = Parser.parse(tokens)
10
+ Renderer.new(nodes).to_ruby
11
+ end
12
+
13
+ def to_s(binding: TOPLEVEL_BINDING)
14
+ eval(to_ruby, binding)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ module ComponentEmbeddedRuby
2
+ class UnexpectedTokenError < StandardError
3
+ attr_reader :expected, :got
4
+
5
+ def initialize(expected, got)
6
+ @expected = expected
7
+ @got = got
8
+ end
9
+
10
+ def message
11
+ "Unexpected token at column #{got.position}, got #{got.value}#{expected_message}."
12
+ end
13
+
14
+ private
15
+
16
+ def expected_message
17
+ if expected != nil
18
+ " but expected #{expected}"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ module ComponentEmbeddedRuby
2
+ VERSION = "0.2.0"
3
+ end
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: component_embedded_ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Blake Williams
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-10-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.17'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.17'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">"
60
+ - !ruby/object:Gem::Version
61
+ version: '6.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">"
67
+ - !ruby/object:Gem::Version
68
+ version: '6.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: view_component
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.15'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.15'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.12.2
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.12.2
97
+ description: HTML templates with embedded Ruby components
98
+ email:
99
+ - blake@blakewilliams.me
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".github/workflows/ruby.yml"
105
+ - ".gitignore"
106
+ - ".travis.yml"
107
+ - Gemfile
108
+ - Gemfile.lock
109
+ - LICENSE.txt
110
+ - README.md
111
+ - Rakefile
112
+ - bin/console
113
+ - bin/setup
114
+ - component_embedded_ruby.gemspec
115
+ - lib/component_embedded_ruby.rb
116
+ - lib/component_embedded_ruby/eval.rb
117
+ - lib/component_embedded_ruby/lexer.rb
118
+ - lib/component_embedded_ruby/lexer/input_reader.rb
119
+ - lib/component_embedded_ruby/node.rb
120
+ - lib/component_embedded_ruby/parser.rb
121
+ - lib/component_embedded_ruby/parser/attribute_parser.rb
122
+ - lib/component_embedded_ruby/parser/base.rb
123
+ - lib/component_embedded_ruby/parser/root_parser.rb
124
+ - lib/component_embedded_ruby/parser/tag_parser.rb
125
+ - lib/component_embedded_ruby/parser/token_reader.rb
126
+ - lib/component_embedded_ruby/renderer.rb
127
+ - lib/component_embedded_ruby/template.rb
128
+ - lib/component_embedded_ruby/unexpected_token_error.rb
129
+ - lib/component_embedded_ruby/version.rb
130
+ homepage: https://github.com/blakewilliams/component_embedded_ruby
131
+ licenses:
132
+ - MIT
133
+ metadata:
134
+ homepage_uri: https://github.com/blakewilliams/component_embedded_ruby
135
+ source_code_uri: https://github.com/blakewilliams/component_embedded_ruby
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ requirements: []
151
+ rubygems_version: 3.1.2
152
+ signing_key:
153
+ specification_version: 4
154
+ summary: HTML templates with embedded Ruby components
155
+ test_files: []