rbexy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/.travis.yml +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Dockerfile +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +213 -0
- data/Guardfile +70 -0
- data/LICENSE.txt +21 -0
- data/README.md +452 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docker-compose.yml +15 -0
- data/example.rb +113 -0
- data/lib/rbexy.rb +35 -0
- data/lib/rbexy/component.rb +100 -0
- data/lib/rbexy/component_providers/rbexy_provider.rb +23 -0
- data/lib/rbexy/component_providers/view_component_provider.rb +23 -0
- data/lib/rbexy/component_tag_builder.rb +19 -0
- data/lib/rbexy/configuration.rb +15 -0
- data/lib/rbexy/hash_mash.rb +15 -0
- data/lib/rbexy/lexer.rb +279 -0
- data/lib/rbexy/nodes.rb +142 -0
- data/lib/rbexy/output_buffer.rb +8 -0
- data/lib/rbexy/parser.rb +204 -0
- data/lib/rbexy/rails.rb +8 -0
- data/lib/rbexy/rails/engine.rb +25 -0
- data/lib/rbexy/rails/template_handler.rb +9 -0
- data/lib/rbexy/runtime.rb +33 -0
- data/lib/rbexy/version.rb +3 -0
- data/lib/rbexy/view_context_helper.rb +11 -0
- data/rbexy.gemspec +40 -0
- metadata +269 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "rbexy"
|
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__)
|
data/bin/setup
ADDED
data/docker-compose.yml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
version: '3'
|
2
|
+
|
3
|
+
volumes:
|
4
|
+
bundle:
|
5
|
+
|
6
|
+
services:
|
7
|
+
rbexy:
|
8
|
+
build: .
|
9
|
+
volumes:
|
10
|
+
- .:/app
|
11
|
+
- bundle:/usr/local/bundle
|
12
|
+
- $HOME/.ssh:/root/.ssh:ro
|
13
|
+
- $HOME/.gitconfig:/root/.gitconfig:ro
|
14
|
+
- $HOME/.gem/credentials:/root/.gem/credentials
|
15
|
+
working_dir: /app
|
data/example.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
require "bundler"
|
2
|
+
Bundler.require
|
3
|
+
|
4
|
+
require "active_support/all"
|
5
|
+
require "action_view/helpers"
|
6
|
+
require "action_view/context"
|
7
|
+
require "action_view/buffers"
|
8
|
+
|
9
|
+
template_string = <<-RBX
|
10
|
+
# A comment here
|
11
|
+
<div>
|
12
|
+
<h1 {**{ class: "myClass" }} {**splat_attrs}>Hello world</h1>
|
13
|
+
<div {**{ class: "myClass" }}></div>
|
14
|
+
Some words
|
15
|
+
# A comment here
|
16
|
+
# A comment here
|
17
|
+
<p>Lorem ipsum</p>
|
18
|
+
<input type="submit" value={@ivar_val} disabled />
|
19
|
+
{true && <p>Is true</p>}
|
20
|
+
{false && <p>Is false</p>}
|
21
|
+
{true ? <p {**{ class: "myClass" }}>Ternary is {'true'.upcase}</p> : <p>Ternary is false</p>}
|
22
|
+
<Button prop1="val1" prop2={true && "val2"} multi-word-prop="value">the content</Button>
|
23
|
+
<Forms.TextField label={->(n) { <label id={n}>Something</label> }} note={<p>the note</p>} />
|
24
|
+
<ul>
|
25
|
+
# A comment here
|
26
|
+
{["hi", "there", "nick"].map { |val| <li>{val}</li> }}
|
27
|
+
</ul>
|
28
|
+
<p
|
29
|
+
class="something">Text</p>
|
30
|
+
<input
|
31
|
+
class="foobar"
|
32
|
+
/>
|
33
|
+
<div
|
34
|
+
with="lots"
|
35
|
+
of="attributes"
|
36
|
+
>
|
37
|
+
Content
|
38
|
+
</div>
|
39
|
+
# comment
|
40
|
+
</div>
|
41
|
+
# comment
|
42
|
+
RBX
|
43
|
+
|
44
|
+
module Components
|
45
|
+
class ButtonComponent
|
46
|
+
def initialize(prop1:, prop2:, multi_word_prop:)
|
47
|
+
@prop1 = prop1
|
48
|
+
@prop2 = prop2
|
49
|
+
@multi_word_prop = multi_word_prop
|
50
|
+
end
|
51
|
+
|
52
|
+
def render
|
53
|
+
# Render it yourself, call one of Rails view helpers (link_to,
|
54
|
+
# content_tag, etc), or use a template file. Be sure to render
|
55
|
+
# children by yielding to the given block.
|
56
|
+
"<button class=\"#{[@prop1, @prop2, @multi_word_prop].join("-")}\">#{yield}</button>"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
module Forms
|
61
|
+
class TextFieldComponent
|
62
|
+
def initialize(label:, note:, **attrs)
|
63
|
+
@label = label
|
64
|
+
@note = note
|
65
|
+
end
|
66
|
+
|
67
|
+
def render
|
68
|
+
"#{@label.call(2)} <input type=\"text\" />#{@note}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class ComponentProvider
|
75
|
+
def match?(name)
|
76
|
+
find(name) != nil
|
77
|
+
end
|
78
|
+
|
79
|
+
def render(context, name, **attrs, &block)
|
80
|
+
props = attrs.transform_keys { |k| ActiveSupport::Inflector.underscore(k.to_s).to_sym }
|
81
|
+
find(name).new(**props).render(&block)
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def find(name)
|
87
|
+
ActiveSupport::Inflector.constantize("Components::#{name}Component")
|
88
|
+
rescue NameError => e
|
89
|
+
raise e unless e.message =~ /constant/
|
90
|
+
nil
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class MyRuntime < Rbexy::Runtime
|
95
|
+
def initialize(component_provider)
|
96
|
+
super(component_provider)
|
97
|
+
@ivar_val = "ivar value"
|
98
|
+
end
|
99
|
+
|
100
|
+
def splat_attrs
|
101
|
+
{
|
102
|
+
key1: "val1",
|
103
|
+
key2: "val2"
|
104
|
+
}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
puts "=============== Compiled ruby code ==============="
|
109
|
+
code = Rbexy.compile(template_string)
|
110
|
+
puts code
|
111
|
+
|
112
|
+
puts "=============== Result of eval ==============="
|
113
|
+
puts MyRuntime.new(ComponentProvider.new).evaluate(code)
|
data/lib/rbexy.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require "rbexy/version"
|
2
|
+
|
3
|
+
module Rbexy
|
4
|
+
autoload :Lexer, "rbexy/lexer"
|
5
|
+
autoload :Parser, "rbexy/parser"
|
6
|
+
autoload :Nodes, "rbexy/nodes"
|
7
|
+
autoload :Runtime, "rbexy/runtime"
|
8
|
+
autoload :HashMash, "rbexy/hash_mash"
|
9
|
+
autoload :OutputBuffer, "rbexy/output_buffer"
|
10
|
+
autoload :ComponentTagBuilder, "rbexy/component_tag_builder"
|
11
|
+
autoload :ViewContextHelper, "rbexy/view_context_helper"
|
12
|
+
autoload :Configuration, "rbexy/configuration"
|
13
|
+
|
14
|
+
ContextNotFound = Class.new(StandardError)
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def configure
|
18
|
+
yield(configuration)
|
19
|
+
end
|
20
|
+
|
21
|
+
def configuration
|
22
|
+
@configuration ||= Configuration.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def compile(template_string)
|
26
|
+
tokens = Rbexy::Lexer.new(template_string).tokenize
|
27
|
+
template = Rbexy::Parser.new(tokens).parse
|
28
|
+
template.compile
|
29
|
+
end
|
30
|
+
|
31
|
+
def evaluate(template_string, runtime)
|
32
|
+
runtime.evaluate compile(template_string)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require "action_view"
|
2
|
+
|
3
|
+
module Rbexy
|
4
|
+
class Component < ActionView::Base
|
5
|
+
class LookupContext < ActionView::LookupContext
|
6
|
+
def self.details_hash(context)
|
7
|
+
context.registered_details.each_with_object({}) do |key, details_hash|
|
8
|
+
value = key == :locale ? [context.locale] : context.send(key)
|
9
|
+
details_hash[key] = value
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# We override any calls to args_for_lookup and set partial=false so that
|
14
|
+
# the lookup context doesn't automatically add a `_` prefix to the
|
15
|
+
# template path, since we're using the Rails partial-rendering
|
16
|
+
# functionality but don't want our templates prefixed with a `_`
|
17
|
+
def args_for_lookup(name, prefixes, partial, keys, details_options)
|
18
|
+
super(name, prefixes, false, keys, details_options)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(view_context, **props)
|
23
|
+
super(
|
24
|
+
view_context.lookup_context,
|
25
|
+
view_context.assigns,
|
26
|
+
view_context.controller
|
27
|
+
)
|
28
|
+
|
29
|
+
@view_context = view_context
|
30
|
+
|
31
|
+
setup(**props)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Override in your subclass to handle props, setup your component, etc.
|
35
|
+
# You can also implement `initialize` but you just need to remember to
|
36
|
+
# call super(view_context).
|
37
|
+
def setup(**props); end
|
38
|
+
|
39
|
+
def render(&block)
|
40
|
+
@content = nil
|
41
|
+
@content_block = block_given? ? block : nil
|
42
|
+
call
|
43
|
+
end
|
44
|
+
|
45
|
+
def call
|
46
|
+
old_lookup_context = view_renderer.lookup_context
|
47
|
+
view_renderer.lookup_context = build_lookup_context(old_lookup_context)
|
48
|
+
view_renderer.render(self, partial: component_name, &nil)
|
49
|
+
ensure
|
50
|
+
view_renderer.lookup_context = old_lookup_context
|
51
|
+
end
|
52
|
+
|
53
|
+
def content
|
54
|
+
@content ||= content_block ? view_context.capture(self, &content_block) : ""
|
55
|
+
end
|
56
|
+
|
57
|
+
def create_context(name, value)
|
58
|
+
rbexy_context.last[name] = value
|
59
|
+
end
|
60
|
+
|
61
|
+
def use_context(name)
|
62
|
+
index = rbexy_context.rindex { |c| c.has_key?(name) }
|
63
|
+
index ?
|
64
|
+
rbexy_context[index][name] :
|
65
|
+
raise(ContextNotFound, "no parent context `#{name}`")
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
attr_reader :view_context, :content_block
|
71
|
+
|
72
|
+
def build_lookup_context(existing_context)
|
73
|
+
paths = existing_context.view_paths.dup.unshift(
|
74
|
+
*Rbexy.configuration.template_paths.map { |p| ActionView::OptimizedFileSystemResolver.new(p) }
|
75
|
+
)
|
76
|
+
|
77
|
+
LookupContext.new(
|
78
|
+
paths,
|
79
|
+
LookupContext.details_hash(existing_context),
|
80
|
+
Rbexy.configuration.template_prefixes
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
def view_renderer
|
85
|
+
view_context.view_renderer
|
86
|
+
end
|
87
|
+
|
88
|
+
def component_name
|
89
|
+
self.class.name.underscore
|
90
|
+
end
|
91
|
+
|
92
|
+
def method_missing(meth, *args, &block)
|
93
|
+
if view_context.respond_to?(meth)
|
94
|
+
view_context.send(meth, *args, &block)
|
95
|
+
else
|
96
|
+
super
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Rbexy
|
2
|
+
module ComponentProviders
|
3
|
+
class RbexyProvider
|
4
|
+
def match?(name)
|
5
|
+
name =~ /^[A-Z]/ && find(name) != nil
|
6
|
+
end
|
7
|
+
|
8
|
+
def render(context, name, **attrs, &block)
|
9
|
+
props = attrs.transform_keys { |k| ActiveSupport::Inflector.underscore(k.to_s).to_sym }
|
10
|
+
find(name).new(context, **props).render(&block)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def find(name)
|
16
|
+
ActiveSupport::Inflector.constantize("#{name}Component")
|
17
|
+
rescue NameError => e
|
18
|
+
raise e unless e.message =~ /wrong constant name/ || e.message =~ /uninitialized constant/
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Rbexy
|
2
|
+
module ComponentProviders
|
3
|
+
class ViewComponentProvider
|
4
|
+
def match?(name)
|
5
|
+
name =~ /^[A-Z]/ && find(name) != nil
|
6
|
+
end
|
7
|
+
|
8
|
+
def render(context, name, **attrs, &block)
|
9
|
+
props = attrs.transform_keys { |k| ActiveSupport::Inflector.underscore(k.to_s).to_sym }
|
10
|
+
find(name).new(**props).render_in(context, &block)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def find(name)
|
16
|
+
ActiveSupport::Inflector.constantize("#{name}Component")
|
17
|
+
rescue NameError => e
|
18
|
+
raise e unless e.message =~ /wrong constant name/ || e.message =~ /uninitialized constant/
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Rbexy
|
2
|
+
class ComponentTagBuilder < ActionView::Helpers::TagHelper::TagBuilder
|
3
|
+
attr_reader :component_provider
|
4
|
+
|
5
|
+
def initialize(context, component_provider)
|
6
|
+
super(context)
|
7
|
+
@component_provider = component_provider
|
8
|
+
end
|
9
|
+
|
10
|
+
def method_missing(called, *args, **attrs, &block)
|
11
|
+
component_name = called.to_s.gsub("__", "::")
|
12
|
+
if component_provider.match?(component_name)
|
13
|
+
component_provider.render(@view_context, component_name, **attrs, &block)
|
14
|
+
else
|
15
|
+
super
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Rbexy
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :component_provider
|
4
|
+
attr_accessor :template_paths
|
5
|
+
attr_accessor :template_prefixes
|
6
|
+
|
7
|
+
def template_paths
|
8
|
+
@template_paths ||= []
|
9
|
+
end
|
10
|
+
|
11
|
+
def template_prefixes
|
12
|
+
@template_prefixes ||= []
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/rbexy/lexer.rb
ADDED
@@ -0,0 +1,279 @@
|
|
1
|
+
module Rbexy
|
2
|
+
class Lexer
|
3
|
+
class SyntaxError < StandardError
|
4
|
+
def initialize(lexer)
|
5
|
+
super(
|
6
|
+
"Invalid syntax: `#{lexer.scanner.peek(20)}`\n" +
|
7
|
+
"Stack: #{lexer.stack}\n" +
|
8
|
+
"Tokens: #{lexer.tokens}"
|
9
|
+
)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
Patterns = HashMash.new(
|
14
|
+
open_expression: /{/,
|
15
|
+
close_expression: /}/,
|
16
|
+
expression_content: /[^}{"'<]+/,
|
17
|
+
open_tag_def: /<(?!\/)/,
|
18
|
+
open_tag_end: /<\//,
|
19
|
+
close_tag: /\s*\/?>/,
|
20
|
+
close_self_closing_tag: /\s*\/>/,
|
21
|
+
tag_name: /\/?[A-Za-z0-9\-_.]+/,
|
22
|
+
text_content: /[^<{#]+/,
|
23
|
+
comment: /^\p{Blank}*#.*(\n|\z)/,
|
24
|
+
whitespace: /\s+/,
|
25
|
+
attr: /[A-Za-z0-9\-_\.]+/,
|
26
|
+
open_attr_splat: /{\*\*/,
|
27
|
+
attr_assignment: /=/,
|
28
|
+
double_quote: /"/,
|
29
|
+
single_quote: /'/,
|
30
|
+
double_quoted_text_content: /[^"]+/,
|
31
|
+
single_quoted_text_content: /[^']+/,
|
32
|
+
expression_internal_tag_prefixes: /(\s+(&&|\?|:|do|do\s*\|[^\|]+\||{|{\s*\|[^\|]+\|)\s+\z|\A\s*\z)/,
|
33
|
+
declaration: /<![^>]*>/
|
34
|
+
)
|
35
|
+
|
36
|
+
attr_reader :stack, :tokens, :scanner, :curr_expr_quote_levels
|
37
|
+
attr_accessor :curr_expr_bracket_levels, :curr_expr, :curr_default_text,
|
38
|
+
:curr_quoted_text
|
39
|
+
|
40
|
+
def initialize(code)
|
41
|
+
@stack = [:default]
|
42
|
+
@curr_expr_bracket_levels = 0
|
43
|
+
@curr_expr_quote_levels = { single: 0, double: 0 }
|
44
|
+
@curr_expr = ""
|
45
|
+
@curr_default_text = ""
|
46
|
+
@curr_quoted_text = ""
|
47
|
+
@tokens = []
|
48
|
+
@scanner = StringScanner.new(code)
|
49
|
+
end
|
50
|
+
|
51
|
+
def tokenize
|
52
|
+
until scanner.eos?
|
53
|
+
case stack.last
|
54
|
+
when :default
|
55
|
+
if scanner.scan(Patterns.declaration)
|
56
|
+
tokens << [:DECLARATION, scanner.matched]
|
57
|
+
elsif scanner.scan(Patterns.open_tag_def)
|
58
|
+
open_tag_def
|
59
|
+
elsif scanner.scan(Patterns.open_expression)
|
60
|
+
open_expression
|
61
|
+
elsif scanner.scan(Patterns.comment)
|
62
|
+
tokens << [:SILENT_NEWLINE]
|
63
|
+
elsif scanner.check(Patterns.text_content)
|
64
|
+
stack.push(:default_text)
|
65
|
+
else
|
66
|
+
raise SyntaxError, self
|
67
|
+
end
|
68
|
+
when :tag
|
69
|
+
if scanner.scan(Patterns.open_tag_def)
|
70
|
+
open_tag_def
|
71
|
+
elsif scanner.scan(Patterns.open_tag_end)
|
72
|
+
tokens << [:OPEN_TAG_END]
|
73
|
+
stack.push(:tag_end)
|
74
|
+
elsif scanner.scan(Patterns.open_expression)
|
75
|
+
open_expression
|
76
|
+
elsif scanner.scan(Patterns.comment)
|
77
|
+
tokens << [:SILENT_NEWLINE]
|
78
|
+
elsif scanner.check(Patterns.text_content)
|
79
|
+
stack.push(:default_text)
|
80
|
+
else
|
81
|
+
raise SyntaxError, self
|
82
|
+
end
|
83
|
+
when :default_text
|
84
|
+
if scanner.scan(Patterns.text_content)
|
85
|
+
self.curr_default_text += scanner.matched
|
86
|
+
if scanner.matched.end_with?('\\') && scanner.peek(1) == "{"
|
87
|
+
self.curr_default_text += scanner.getch
|
88
|
+
elsif scanner.matched.end_with?('\\') && scanner.peek(1) == "#"
|
89
|
+
self.curr_default_text += scanner.getch
|
90
|
+
else
|
91
|
+
if scanner.peek(1) == "#"
|
92
|
+
# If the next token is a comment, trim trailing whitespace from
|
93
|
+
# the text value so we don't add to the indentation of the next
|
94
|
+
# value that is output after the comment
|
95
|
+
self.curr_default_text = curr_default_text.gsub(/^\p{Blank}*\z/, "")
|
96
|
+
end
|
97
|
+
tokens << [:TEXT, curr_default_text]
|
98
|
+
self.curr_default_text = ""
|
99
|
+
stack.pop
|
100
|
+
end
|
101
|
+
else
|
102
|
+
raise SyntaxError, self
|
103
|
+
end
|
104
|
+
when :expression
|
105
|
+
if scanner.scan(Patterns.close_expression)
|
106
|
+
tokens << [:EXPRESSION_BODY, curr_expr]
|
107
|
+
tokens << [:CLOSE_EXPRESSION]
|
108
|
+
self.curr_expr = ""
|
109
|
+
stack.pop
|
110
|
+
elsif scanner.scan(Patterns.open_expression)
|
111
|
+
expression_inner_bracket
|
112
|
+
elsif scanner.scan(Patterns.double_quote)
|
113
|
+
expression_inner_double_quote
|
114
|
+
elsif scanner.scan(Patterns.single_quote)
|
115
|
+
expression_inner_single_quote
|
116
|
+
elsif scanner.scan(Patterns.open_tag_def)
|
117
|
+
potential_expression_inner_tag
|
118
|
+
elsif scanner.scan(Patterns.expression_content)
|
119
|
+
self.curr_expr += scanner.matched
|
120
|
+
else
|
121
|
+
raise SyntaxError, self
|
122
|
+
end
|
123
|
+
when :expression_inner_bracket
|
124
|
+
if scanner.scan(Patterns.close_expression)
|
125
|
+
self.curr_expr += scanner.matched
|
126
|
+
stack.pop
|
127
|
+
elsif scanner.scan(Patterns.open_expression)
|
128
|
+
expression_inner_bracket
|
129
|
+
elsif scanner.scan(Patterns.double_quote)
|
130
|
+
expression_inner_double_quote
|
131
|
+
elsif scanner.scan(Patterns.single_quote)
|
132
|
+
expression_inner_single_quote
|
133
|
+
elsif scanner.scan(Patterns.open_tag_def)
|
134
|
+
potential_expression_inner_tag
|
135
|
+
elsif scanner.scan(Patterns.expression_content)
|
136
|
+
self.curr_expr += scanner.matched
|
137
|
+
else
|
138
|
+
raise SyntaxError, self
|
139
|
+
end
|
140
|
+
when :expression_inner_double_quote
|
141
|
+
if scanner.check(Patterns.double_quote)
|
142
|
+
expression_quoted_string_content
|
143
|
+
elsif scanner.scan(Patterns.double_quoted_text_content)
|
144
|
+
self.curr_expr += scanner.matched
|
145
|
+
else
|
146
|
+
raise SyntaxError, self
|
147
|
+
end
|
148
|
+
when :expression_inner_single_quote
|
149
|
+
if scanner.check(Patterns.single_quote)
|
150
|
+
expression_quoted_string_content
|
151
|
+
elsif scanner.scan(Patterns.single_quoted_text_content)
|
152
|
+
self.curr_expr += scanner.matched
|
153
|
+
else
|
154
|
+
raise SyntaxError, self
|
155
|
+
end
|
156
|
+
when :tag_def
|
157
|
+
if scanner.scan(Patterns.close_self_closing_tag)
|
158
|
+
tokens << [:CLOSE_TAG_DEF]
|
159
|
+
tokens << [:OPEN_TAG_END]
|
160
|
+
tokens << [:CLOSE_TAG_END]
|
161
|
+
stack.pop(2)
|
162
|
+
elsif scanner.scan(Patterns.close_tag)
|
163
|
+
tokens << [:CLOSE_TAG_DEF]
|
164
|
+
stack.pop
|
165
|
+
elsif scanner.scan(Patterns.tag_name)
|
166
|
+
tokens << [:TAG_NAME, scanner.matched]
|
167
|
+
elsif scanner.scan(Patterns.whitespace)
|
168
|
+
scanner.matched.count("\n").times { tokens << [:SILENT_NEWLINE] }
|
169
|
+
tokens << [:OPEN_ATTRS]
|
170
|
+
stack.push(:tag_attrs)
|
171
|
+
else
|
172
|
+
raise SyntaxError, self
|
173
|
+
end
|
174
|
+
when :tag_end
|
175
|
+
if scanner.scan(Patterns.close_tag)
|
176
|
+
tokens << [:CLOSE_TAG_END]
|
177
|
+
stack.pop(2)
|
178
|
+
elsif scanner.scan(Patterns.tag_name)
|
179
|
+
tokens << [:TAG_NAME, scanner.matched]
|
180
|
+
else
|
181
|
+
raise SyntaxError, self
|
182
|
+
end
|
183
|
+
when :tag_attrs
|
184
|
+
if scanner.scan(Patterns.whitespace)
|
185
|
+
scanner.matched.count("\n").times { tokens << [:SILENT_NEWLINE] }
|
186
|
+
elsif scanner.check(Patterns.close_tag)
|
187
|
+
tokens << [:CLOSE_ATTRS]
|
188
|
+
stack.pop
|
189
|
+
elsif scanner.scan(Patterns.attr_assignment)
|
190
|
+
tokens << [:OPEN_ATTR_VALUE]
|
191
|
+
stack.push(:tag_attr_value)
|
192
|
+
elsif scanner.scan(Patterns.attr)
|
193
|
+
tokens << [:ATTR_NAME, scanner.matched.strip]
|
194
|
+
elsif scanner.scan(Patterns.open_attr_splat)
|
195
|
+
tokens << [:OPEN_ATTR_SPLAT]
|
196
|
+
tokens << [:OPEN_EXPRESSION]
|
197
|
+
stack.push(:tag_attr_splat, :expression)
|
198
|
+
else
|
199
|
+
raise SyntaxError, self
|
200
|
+
end
|
201
|
+
when :tag_attr_value
|
202
|
+
if scanner.scan(Patterns.double_quote)
|
203
|
+
stack.push(:quoted_text)
|
204
|
+
elsif scanner.scan(Patterns.open_expression)
|
205
|
+
open_expression
|
206
|
+
elsif scanner.scan(Patterns.whitespace) || scanner.check(Patterns.close_tag)
|
207
|
+
tokens << [:CLOSE_ATTR_VALUE]
|
208
|
+
scanner.matched.count("\n").times { tokens << [:SILENT_NEWLINE] }
|
209
|
+
stack.pop
|
210
|
+
else
|
211
|
+
raise SyntaxError, self
|
212
|
+
end
|
213
|
+
when :tag_attr_splat
|
214
|
+
# Splat is consumed by :expression. It pops control back to here once
|
215
|
+
# it's done, and we just record the completion and pop back to :tag_attrs
|
216
|
+
tokens << [:CLOSE_ATTR_SPLAT]
|
217
|
+
stack.pop
|
218
|
+
when :quoted_text
|
219
|
+
if scanner.scan(Patterns.double_quoted_text_content)
|
220
|
+
self.curr_quoted_text += scanner.matched
|
221
|
+
if scanner.matched.end_with?('\\') && scanner.peek(1) == "\""
|
222
|
+
self.curr_quoted_text += scanner.getch
|
223
|
+
end
|
224
|
+
elsif scanner.scan(Patterns.double_quote)
|
225
|
+
tokens << [:TEXT, curr_quoted_text]
|
226
|
+
self.curr_quoted_text = ""
|
227
|
+
stack.pop
|
228
|
+
else
|
229
|
+
raise SyntaxError, self
|
230
|
+
end
|
231
|
+
else
|
232
|
+
raise SyntaxError, self
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
tokens
|
237
|
+
end
|
238
|
+
|
239
|
+
def potential_expression_inner_tag
|
240
|
+
if self.curr_expr =~ Patterns.expression_internal_tag_prefixes
|
241
|
+
tokens << [:EXPRESSION_BODY, curr_expr]
|
242
|
+
self.curr_expr = ""
|
243
|
+
open_tag_def
|
244
|
+
else
|
245
|
+
self.curr_expr += scanner.matched
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def open_tag_def
|
250
|
+
tokens << [:OPEN_TAG_DEF]
|
251
|
+
stack.push(:tag, :tag_def)
|
252
|
+
end
|
253
|
+
|
254
|
+
def open_expression
|
255
|
+
tokens << [:OPEN_EXPRESSION]
|
256
|
+
stack.push(:expression)
|
257
|
+
end
|
258
|
+
|
259
|
+
def expression_inner_bracket
|
260
|
+
self.curr_expr += scanner.matched
|
261
|
+
stack.push(:expression_inner_bracket)
|
262
|
+
end
|
263
|
+
|
264
|
+
def expression_inner_double_quote
|
265
|
+
self.curr_expr += scanner.matched
|
266
|
+
stack.push(:expression_inner_double_quote)
|
267
|
+
end
|
268
|
+
|
269
|
+
def expression_inner_single_quote
|
270
|
+
self.curr_expr += scanner.matched
|
271
|
+
stack.push(:expression_inner_single_quote)
|
272
|
+
end
|
273
|
+
|
274
|
+
def expression_quoted_string_content
|
275
|
+
self.curr_expr += scanner.getch
|
276
|
+
stack.pop unless curr_expr.end_with?('\\')
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|