rblade 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.
@@ -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