rblade 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,65 @@
1
+ module RBlade
2
+ class CompilesStatements
3
+ class CompilesStacks
4
+ def initialize
5
+ @push_counter = 0
6
+ end
7
+
8
+ def compileStack args
9
+ if args&.count != 1
10
+ raise StandardError.new "Stack statement: wrong number of arguments (given #{args&.count}, expecting 1)"
11
+ end
12
+
13
+ "RBlade::StackManager.initialize(#{args[0]}, _out);_stacks.push(#{args[0]});_out = '';"
14
+ end
15
+
16
+ def compilePrepend args
17
+ if args.nil? || args.count > 2
18
+ raise StandardError.new "Prepend statement: wrong number of arguments (given #{args&.count}, expecting 1 or 2)"
19
+ end
20
+
21
+ if args.count == 2
22
+ "RBlade::StackManager.prepend(#{args[0]}, #{args[1]});"
23
+ else
24
+ @push_counter += 1
25
+
26
+ "_push_#{@push_counter}_name=#{args[0]};_push_#{@push_counter}_buffer=_out;_out='';"
27
+ end
28
+ end
29
+
30
+ def compileEndPrepend args
31
+ if !args.nil?
32
+ raise StandardError.new "End prepend statement: wrong number of arguments (given #{args&.count}, expecting 0)"
33
+ end
34
+
35
+ @push_counter -= 1
36
+
37
+ "RBlade::StackManager.prepend(_push_#{@push_counter + 1}_name, _out);_out=_push_#{@push_counter + 1}_buffer;"
38
+ end
39
+
40
+ def compilePush args
41
+ if args.nil? || args.count > 2
42
+ raise StandardError.new "Push statement: wrong number of arguments (given #{args&.count}, expecting 1 or 2)"
43
+ end
44
+
45
+ if args.count == 2
46
+ "RBlade::StackManager.push(#{args[0]}, #{args[1]});"
47
+ else
48
+ @push_counter += 1
49
+
50
+ "_push_#{@push_counter}_name=#{args[0]};_push_#{@push_counter}_buffer=_out;_out='';"
51
+ end
52
+ end
53
+
54
+ def compileEndPush args
55
+ if !args.nil?
56
+ raise StandardError.new "End push statement: wrong number of arguments (given #{args&.count}, expecting 0)"
57
+ end
58
+
59
+ @push_counter -= 1
60
+
61
+ "RBlade::StackManager.push(_push_#{@push_counter + 1}_name, _out);_out=_push_#{@push_counter + 1}_buffer;"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,187 @@
1
+ require "ripper"
2
+
3
+ module RBlade
4
+ class TokenizesComponents
5
+ def tokenize!(tokens)
6
+ tokens.map! do |token|
7
+ next(token) if token.type != :unprocessed
8
+
9
+ segments = tokenizeComponentOpeningTags token.value
10
+
11
+ i = 0
12
+ while i < segments.count
13
+ if segments[i] == "</" && segments[i + 1]&.match(/x[-:]/)
14
+ segments[i] = Token.new(type: :component_end, value: {name: segments[i + 1][2..]})
15
+
16
+ segments.delete_at i + 1
17
+ i += 1
18
+ elsif segments[i] == "<" && segments[i + 1]&.match(/x[-:]/)
19
+ name = segments[i + 1][2..]
20
+ raw_attributes = (segments[i + 2] != ">") ? tokenizeAttributes(segments[i + 2]) : nil
21
+
22
+ attributes = processAttributes raw_attributes
23
+
24
+ if raw_attributes.nil?
25
+ segments.delete_at i + 1
26
+ else
27
+ while segments[i + 1] != ">" && segments[i + 1] != "/>"
28
+ segments.delete_at i + 1
29
+ end
30
+ end
31
+
32
+ token_type = (segments[i + 1] == "/>") ? :component : :component_start
33
+ segments[i] = Token.new(type: token_type, value: {name:, attributes:})
34
+ segments.delete_at i + 1
35
+
36
+ i += 1
37
+ elsif !segments[i].nil? && segments[i] != ""
38
+ segments[i] = Token.new(type: :unprocessed, value: segments[i])
39
+
40
+ i += 1
41
+ else
42
+ segments.delete_at i
43
+ end
44
+ end
45
+
46
+ segments
47
+ end.flatten!
48
+ end
49
+
50
+ private
51
+
52
+ def processAttributes raw_attributes
53
+ attributes = []
54
+ i = 0
55
+ while i < raw_attributes.count
56
+ name = raw_attributes[i]
57
+ if name == "@class" || name == "@style"
58
+ attributes.push({type: name[1..], value: raw_attributes[i + 1][1..-2]})
59
+ i += 2
60
+ elsif name[0..1] == "{{"
61
+ attributes.push({type: "attributes", value: raw_attributes[i + 1][2..-2]})
62
+ i += 1
63
+ else
64
+ attribute = {name:}
65
+
66
+ if raw_attributes[i + 1] == "="
67
+ attribute[:value] = raw_attributes[i + 2]
68
+ i += 3
69
+ else
70
+ i += 1
71
+ end
72
+
73
+ # The "::" at the start of attributes is used to escape attribute names beginning with ":"
74
+ if name[0..1] == "::"
75
+ attribute[:type] = "string"
76
+ attribute[:name].delete_prefix! ":"
77
+ attributes.push(attribute)
78
+ next
79
+ end
80
+
81
+ if name[0] == ":"
82
+ attribute[:type] = attribute[:value].nil? ? "pass_through" : "ruby"
83
+ attribute[:name].delete_prefix! ":"
84
+ attributes.push(attribute)
85
+ next
86
+ end
87
+
88
+ attribute[:type] = if attribute[:value].nil?
89
+ "empty"
90
+ else
91
+ "string"
92
+ end
93
+ attributes.push(attribute)
94
+ end
95
+ end
96
+
97
+ attributes
98
+ end
99
+
100
+ def tokenizeComponentOpeningTags value
101
+ value.split(%r/
102
+ # Opening and self-closing tags
103
+ (?:
104
+ (<)
105
+ \s*
106
+ (x[-\:][\w\-\:\.]*)
107
+ ((?:
108
+ \s+
109
+ (?:
110
+ (?:
111
+ @class(\( (?: (?>[^()]+) | \g<-1> )* \))
112
+ )
113
+ |
114
+ (?:
115
+ @style(\( (?: (?>[^()]+)| \g<-1> )* \))
116
+ )
117
+ |
118
+ (
119
+ \{\{\s*attributes(?:[^}]+?)?\s*\}\}
120
+ )
121
+ |
122
+ (?:
123
+ [\w\-:.@%]+
124
+ (?:
125
+ =
126
+ (?:
127
+ "[^"]*"
128
+ |
129
+ '[^\']*'
130
+ |
131
+ [^'"=<>\s]+
132
+ )
133
+ )?
134
+ )
135
+ )
136
+ )*)
137
+ \s*
138
+ (?<![=\-])
139
+ (\/?>)
140
+ )
141
+ |
142
+ # Closing tags
143
+ (?:
144
+ (<\/)
145
+ \s*
146
+ (x[-\:][\w\-\:\.]*)
147
+ \s*
148
+ >
149
+ )
150
+ /x)
151
+ end
152
+
153
+ def tokenizeAttributes segment
154
+ segment.scan(%r/
155
+ (?<=\s|^)
156
+ (?:
157
+ (?:
158
+ (@class)(\( (?: (?>[^()]+) | \g<-1> )* \))
159
+ )
160
+ |
161
+ (?:
162
+ (@style)(\( (?: (?>[^()]+)| \g<-1> )* \))
163
+ )
164
+ |
165
+ (?:
166
+ (\{\{)\s*attributes([^}]+?)?\s*(\}\})
167
+ )
168
+ |
169
+ (?:
170
+ ([\w\-:.@%]+)
171
+ (?:
172
+ (=)
173
+ (?:
174
+ "([^"]*)"
175
+ |
176
+ '([^\']*)'
177
+ |
178
+ ([^'"=<>\s]+)
179
+ )
180
+ )?
181
+ )
182
+ )
183
+ (?=\s|$)
184
+ /x).flatten.compact
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,171 @@
1
+ require "ripper"
2
+
3
+ module RBlade
4
+ class TokenizesStatements
5
+ def tokenize!(tokens)
6
+ tokens.map! do |token|
7
+ next(token) if token.type != :unprocessed
8
+
9
+ segments = token.value.split(/
10
+ (?:^|[\b\s])
11
+ (@@?)
12
+ (\w+(?:::\w+)?)
13
+ (?:[ \t]*
14
+ (\(.*?\))
15
+ )?/mx)
16
+
17
+ parseSegments! segments
18
+ end.flatten!
19
+ end
20
+
21
+ private
22
+
23
+ def parseSegments! segments
24
+ i = 0
25
+ while i < segments.count
26
+ segment = segments[i]
27
+
28
+ # The @ symbol is used to escape blade directives so we return it unprocessed
29
+ if segment == "@@"
30
+ segments[i] = Token.new(type: :unprocessed, value: segment[1..] + segments[i + 1])
31
+ segments.delete_at i + 1
32
+
33
+ i += 1
34
+ elsif segment == "@"
35
+ tokenizeStatement! segments, i
36
+
37
+ # Remove trailing whitespace if it exists, but don't double dip when another statement follows
38
+ if !segments[i + 1].nil? && segments[i + 1].match(/^\s/) && (segments[i + 1].length > 1 || segments[i + 2].nil?)
39
+ segments[i + 1].slice! 0, 1
40
+ end
41
+
42
+ i += 1
43
+ elsif !segments[i].nil? && segments[i] != ""
44
+ segments[i] = Token.new(type: :unprocessed, value: segments[i])
45
+
46
+ i += 1
47
+ else
48
+ segments.delete_at i
49
+ end
50
+ end
51
+
52
+ segments
53
+ end
54
+
55
+ def tokenizeStatement!(segments, i)
56
+ statement_data = {name: segments[i + 1]}
57
+ segments.delete_at i + 1
58
+
59
+ if segments.count > i + 1 && segments[i + 1][0] == "("
60
+ arguments = tokenizeArguments! segments, i + 1
61
+
62
+ if !arguments.nil?
63
+ statement_data[:arguments] = arguments
64
+ end
65
+ end
66
+
67
+ segments[i] = Token.new(type: :statement, value: statement_data)
68
+ end
69
+
70
+ def tokenizeArguments!(segments, segment_index)
71
+ success = expandSegmentToEndParenthesis! segments, segment_index
72
+
73
+ # If no matching parentheses were found, so we combine the argument string with the next segment
74
+ if !success
75
+ if !segments[segment_index + 1].nil?
76
+ segments[segment_index] <<= segments[segment_index + 1]
77
+ segments.delete_at segment_index + 1
78
+ end
79
+
80
+ return nil
81
+ end
82
+
83
+ arguments = extractArguments segments[segment_index]
84
+ segments.delete_at segment_index
85
+
86
+ arguments
87
+ end
88
+
89
+ def expandSegmentToEndParenthesis! segments, segment_index
90
+ parentheses_difference = 0
91
+ tokens = nil
92
+
93
+ loop do
94
+ tokens = Ripper.lex(segments[segment_index]).map { |token| token[1] }
95
+ parentheses_difference = tokens.count(:on_lparen) - tokens.count(:on_rparen)
96
+
97
+ break if parentheses_difference.zero? || segments[segment_index + 1].nil?
98
+
99
+ index = segments[segment_index + 1].each_char.find_index { |c| c == ")" && (parentheses_difference -= 1).zero? }
100
+
101
+ if index.nil?
102
+ segments[segment_index] << segments[segment_index + 1]
103
+ segments.delete_at segment_index + 1
104
+ else
105
+ segments[segment_index] << segments[segment_index + 1].slice!(0..index)
106
+ end
107
+
108
+ break if segments[segment_index + 1].nil?
109
+ end
110
+
111
+ parentheses_difference.zero?
112
+ end
113
+
114
+ def extractArguments(segment)
115
+ # Add a comma to the end to delimit the end of the last argument
116
+ segment = segment[1..-2] + ","
117
+ segment_lines = segment.lines
118
+
119
+ tokens = Ripper.lex segment
120
+ arguments = []
121
+
122
+ current_line = 1
123
+ current_index = 0
124
+ bracket_count = {
125
+ "[]": 0,
126
+ "{}": 0,
127
+ "()": 0
128
+ }
129
+ tokens.each do |token|
130
+ case token[1]
131
+ when :on_lbracket
132
+ bracket_count[:[]] += 1
133
+ when :on_rbracket
134
+ bracket_count[:[]] -= 1
135
+ when :on_lbrace
136
+ bracket_count[:"{}"] += 1
137
+ when :on_rbrace
138
+ bracket_count[:"{}"] -= 1
139
+ when :on_lparen
140
+ bracket_count[:"()"] += 1
141
+ when :on_rparen
142
+ bracket_count[:"()"] -= 1
143
+ when :on_comma
144
+ if bracket_count[:[]] != 0 || bracket_count[:"{}"] != 0 || bracket_count[:"()"] != 0
145
+ next
146
+ end
147
+
148
+ argument = ""
149
+
150
+ # Concatenate all lines up to this token's line, including the tail end of the current line
151
+ if token[0][0] != current_line
152
+ (current_line...token[0][0]).each do |i|
153
+ argument << (segment_lines[i - 1].slice(current_index..-1) || "")
154
+ current_index = 0
155
+ end
156
+ current_line = token[0][0]
157
+ end
158
+ argument <<= segment_lines[current_line - 1].slice(current_index...token[0][1])
159
+
160
+ arguments.push argument.strip
161
+
162
+ current_index = token[0][1] + 1
163
+ end
164
+ end
165
+
166
+ return nil if arguments.count == 1 && arguments.first == ""
167
+
168
+ arguments
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,60 @@
1
+ require "rblade/compiler/compiles_comments"
2
+ require "rblade/compiler/compiles_components"
3
+ require "rblade/compiler/compiles_echos"
4
+ require "rblade/compiler/compiles_ruby"
5
+ require "rblade/compiler/compiles_statements"
6
+ require "rblade/compiler/tokenizes_components"
7
+ require "rblade/compiler/tokenizes_statements"
8
+
9
+ Token = Struct.new(:type, :value)
10
+
11
+ if !defined?(h)
12
+ require "erb/escape"
13
+ define_method(:h, ERB::Escape.instance_method(:html_escape))
14
+ end
15
+
16
+ module RBlade
17
+ def self.escape_quotes string
18
+ string.gsub(/['\\\x0]/, '\\\\\0')
19
+ end
20
+
21
+ class Compiler
22
+ def self.compileString(string_template)
23
+ tokens = [Token.new(:unprocessed, string_template)]
24
+
25
+ CompilesComments.new.compile! tokens
26
+ CompilesEchos.new.compile! tokens
27
+ CompilesRuby.new.compile! tokens
28
+ TokenizesComponents.new.tokenize! tokens
29
+ TokenizesStatements.new.tokenize! tokens
30
+ CompilesStatements.new.compile! tokens
31
+ CompilesComponents.new.compile! tokens
32
+
33
+ compileTokens tokens
34
+ end
35
+
36
+ def self.compileAttributeString(string_template)
37
+ tokens = [Token.new(:unprocessed, string_template)]
38
+
39
+ CompilesRuby.compile! tokens
40
+ CompilesComments.compile!(tokens)
41
+ CompilesEchos.compile!(tokens)
42
+
43
+ compileTokens tokens
44
+ end
45
+
46
+ def self.compileTokens tokens
47
+ output = ""
48
+
49
+ tokens.each do |token, cake|
50
+ if token.type == :unprocessed || token.type == :raw_text
51
+ output << "_out<<'" << RBlade.escape_quotes(token.value) << "';"
52
+ else
53
+ output << token.value
54
+ end
55
+ end
56
+
57
+ output
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,42 @@
1
+ require "rblade/compiler"
2
+
3
+ module RBlade
4
+ FILE_EXTENSIONS = [".blade", ".html.blade"]
5
+
6
+ class ComponentStore
7
+ def self.fetchComponent name
8
+ namespace = nil
9
+ path = name
10
+
11
+ if name.match '::'
12
+ namespace, path = name.split('::')
13
+ end
14
+
15
+ path.gsub! '.', '/'
16
+
17
+ @@templatePaths[namespace]&.each do |base_path|
18
+ FILE_EXTENSIONS.each do |extension|
19
+ if File.exist? base_path + path + extension
20
+ return RBlade::Compiler.compileString File.read(base_path + path + extension)
21
+ end
22
+ end
23
+ end
24
+
25
+ raise StandardError.new "Unknown component #{name}"
26
+ end
27
+
28
+ def self.add_path path, namespace = nil
29
+ path = path.to_s
30
+ if !path.end_with? "/"
31
+ path = path + "/"
32
+ end
33
+
34
+ @@templatePaths[namespace] ||= []
35
+ @@templatePaths[namespace] << path
36
+ end
37
+
38
+ private
39
+
40
+ @@templatePaths = {}
41
+ end
42
+ end
@@ -0,0 +1,58 @@
1
+ module RBlade
2
+ class StackManager
3
+ def self.initialize stack_name, before_stack
4
+ @@stacks[stack_name] ||= Stack.new
5
+ @@stacks[stack_name].set_before_stack before_stack
6
+ end
7
+
8
+ def self.clear
9
+ @@stacks = {}
10
+ end
11
+
12
+ def self.push stack_name, code
13
+ @@stacks[stack_name] ||= Stack.new
14
+ @@stacks[stack_name].push code.to_s
15
+ end
16
+
17
+ def self.prepend stack_name, code
18
+ @@stacks[stack_name] ||= Stack.new
19
+ @@stacks[stack_name].prepend code.to_s
20
+ end
21
+
22
+ def self.get(stacks)
23
+ stacks.map do |name|
24
+ out = @@stacks[name].to_s
25
+ @@stacks.delete name
26
+
27
+ out
28
+ end.join
29
+ end
30
+
31
+ private
32
+
33
+ @@stacks = {}
34
+
35
+ class Stack
36
+ def initialize
37
+ @prepends = ""
38
+ @stack = ""
39
+ end
40
+
41
+ def set_before_stack before_stack
42
+ @before_stack = before_stack
43
+ end
44
+
45
+ def to_s
46
+ "#{@before_stack}#{@prepends}#{@stack}"
47
+ end
48
+
49
+ def push code
50
+ @stack << code
51
+ end
52
+
53
+ def prepend code
54
+ @prepends << code
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,14 @@
1
+ require "rails"
2
+ require "rblade/compiler"
3
+ require "rblade/helpers/stack_manager"
4
+
5
+ module RBlade
6
+ class RailsTemplate
7
+ def call(template, source = nil)
8
+ RBlade::StackManager.clear
9
+ setup = "foo = 'FOO';bar = 'BAR';_out='';_stacks=[];"
10
+ setdown = "RBlade::StackManager.get(_stacks) + _out"
11
+ setup + RBlade::Compiler.compileString(source || template.source) + setdown
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ require "rails"
2
+ require "rblade/rails_template"
3
+ require "rblade/component_store"
4
+
5
+ module RBlade
6
+ class Railtie < ::Rails::Railtie
7
+ initializer :rblade, before: :load_config_initializers do |app|
8
+ ActionView::Template.register_template_handler(:blade, RBlade::RailsTemplate.new)
9
+
10
+ RBlade::ComponentStore.add_path(Rails.root.join("app", "views", "components"))
11
+ RBlade::ComponentStore.add_path(Rails.root.join("app", "views", "layouts"), "layout")
12
+ end
13
+ end
14
+ end
data/lib/rblade.rb ADDED
@@ -0,0 +1,2 @@
1
+ require "rails"
2
+ require "rblade/railtie"
data/rblade.gemspec ADDED
@@ -0,0 +1,18 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "rblade"
3
+ s.version = "0.1.0"
4
+ s.summary = "Blade templates for ruby"
5
+ s.description = "A port of the Laravel blade templating engine to ruby"
6
+ s.authors = ["Simon J"]
7
+ s.email = "2857218+mwnciau@users.noreply.github.com"
8
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|storage)/}) }
9
+ s.require_paths = ['lib']
10
+ s.homepage = "https://rubygems.org/gems/rblade"
11
+ s.license = "MIT"
12
+ s.required_ruby_version = ">= 3.0.0"
13
+
14
+ s.add_development_dependency "minitest", "~> 5.0"
15
+ s.add_development_dependency "minitest-reporters", "~> 1.1"
16
+ s.add_development_dependency "standard", "~> 1.3"
17
+ s.add_development_dependency "rails", "~> 7.0"
18
+ end