rblade 3.0.1 → 3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c122be45a5a4cf085c9303105e2ba077e98f985ec1d460b661a34c90011f5bc6
4
- data.tar.gz: 2025546b49a6058123cc089ade23c51c8a63be70f311336938d79898ee5c08e7
3
+ metadata.gz: 565a823219a85db94f6fa31e42de52cab5d1de562c89c42243a0105d54eab581
4
+ data.tar.gz: 484ca84492233eabd1c61a534be991cfdb489273751e4e43a1b5ac400965b867
5
5
  SHA512:
6
- metadata.gz: 7254ddefde7ec012469079917d69c6009bc16853ed230270803b751cf60cf724d642da949247e183a0dd8eff33fff540ce66698b52471ae5c0dd145969040aec
7
- data.tar.gz: 7f66601910831102f56aaf3c637be8fe1284f043b80927c0b9d41adba7e35bee224f7fa00b6b5f6615b0883e5330879feb560c2de2ac1126700e2368ae1fe226
6
+ metadata.gz: a3fa94d7c7c3f19f3671dd346bffedbee1fdaaf3b93ce0928cba1b6bb7d24a7c7da83c016863df9aa8c5cbdd0d5519d65efef7aa18a59a77a7861764e8188f03
7
+ data.tar.gz: 29325e82f35eb1e6b949b49684ea5c78cea910ffeeacf18ef012c39ff6cbe2a73e26bd47d1753734cc712421b19a958dab54224c024c25ca580b53b34b6dcb9a
@@ -31,5 +31,9 @@ jobs:
31
31
  with:
32
32
  ruby-version: ${{ matrix.ruby-version }}
33
33
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
34
+
35
+ - name: Run rubocop rules
36
+ run: bundle exec rubocop
37
+
34
38
  - name: Run tests
35
39
  run: bundle exec rake
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 3.1.0 [2025-04-02]
2
+ - Add ability to use slots in dynamic components and the component view helper method
3
+ - Change statement matching to use regular expressions to improve compile performance
4
+
1
5
  ## 3.0.1 [2025-03-18]
2
6
  - Fix dynamic components not working when `RBlade.component_helper_method_name` is set
3
7
 
data/README.md CHANGED
@@ -62,6 +62,7 @@ For a quick overview of RBlade's capabilities, refer to the [reference file](REF
62
62
  + [Method Field](#method-field)
63
63
  * [Stacks](#stacks)
64
64
  * [Integrating RBlade With Other Templates](#rblade-integration)
65
+ * [Limitations](#limitations)
65
66
 
66
67
  <a name="displaying-data"></a>
67
68
  ## Displaying Data
@@ -984,6 +985,18 @@ You might want to use RBlade components within other templates, e.g. if you are
984
985
  <% end %>
985
986
  ```
986
987
 
988
+ The component method also passes a proc that can be used to specify slots:
989
+
990
+ ```erb
991
+ <%= component "card" do |slot| %>
992
+ <%# The "class" attribute and the contents of the block will be passed in as the "heading" attribute to the card component %>
993
+ <% slot :heading, class: "font-bold" do %>
994
+ Heading
995
+ <% end %>
996
+ Content
997
+ <% end %>
998
+ ```
999
+
987
1000
  If preferred, the `component` method can be renamed using the `RBlade.component_helper_method_name` option:
988
1001
 
989
1002
  ```ruby
@@ -1017,3 +1030,42 @@ Once enabled, RBlade components can be used as layouts for ERB templates, or ren
1017
1030
 
1018
1031
  > [!NOTE]
1019
1032
  > Using the `component` helper instead of RBlade's component syntax does not take advantage of RBlade's component caching
1033
+
1034
+
1035
+ <a name="limitations"></a>
1036
+ ## Limitations
1037
+
1038
+ ### Regular expressions in RBlade directives
1039
+
1040
+ Regular expression literals may cause improper bracket matching in RBlade directive. To work around this, use the `%r` percent literal syntax.
1041
+
1042
+ ```rblade
1043
+ {{-- Parentheses in regular expressions may cause incorrect matching --}}
1044
+ @ruby(/\)/)
1045
+
1046
+ {{-- A workaround is to use the alternative %r syntax --}}
1047
+ @ruby(%r/\)/)
1048
+ ```
1049
+
1050
+ ### `<<` in RBlade directives
1051
+
1052
+ The append operator is assumed to be a HEREDOC if followed immediately by a word character (a-z, A-Z, _), `-` or `~`.
1053
+
1054
+ ```rblade
1055
+ {{-- If using `<<` to append, add a space after the operator --}}
1056
+ @ruby(string << "extra")
1057
+ ```
1058
+
1059
+ ### End brackets in print statements
1060
+
1061
+ Print statements cannot contain their end bracket in strings or other literals:
1062
+
1063
+ ```rblade
1064
+ # These examples will cause a syntax error
1065
+ {{ '}}' }}
1066
+ <%= 'foo%>' %>
1067
+
1068
+ # A workaround is to use the alternative syntax
1069
+ <%= 'foo}}' %>
1070
+ {{ 'foo%>' }}
1071
+ ```
data/Rakefile CHANGED
@@ -3,4 +3,4 @@ require "standard/rake"
3
3
 
4
4
  Minitest::TestTask.create
5
5
 
6
- task default: ["standard", "test"]
6
+ task default: ["test"]
data/do CHANGED
@@ -43,36 +43,36 @@ fi
43
43
 
44
44
  if [ "$1" == "run" ] || [ "$1" == "r" ]
45
45
  then
46
- echo Running: ${DOCKER_COMPOSE_COMMAND} run blade ${@:2}
47
- ${DOCKER_COMPOSE_COMMAND} run blade "${@:2}"
46
+ echo Running: ${DOCKER_COMPOSE_COMMAND} run --remove-orphans --rm blade ${@:2}
47
+ ${DOCKER_COMPOSE_COMMAND} run --remove-orphans --rm blade "${@:2}"
48
48
  exit 0
49
49
  fi
50
50
 
51
51
  if [ "$1" == "rake" ] || [ "$1" == "rk" ]
52
52
  then
53
- echo Running: ${DOCKER_COMPOSE_COMMAND} run blade rake ${@:2}
54
- ${DOCKER_COMPOSE_COMMAND} run blade rake "${@:2}"
53
+ echo Running: ${DOCKER_COMPOSE_COMMAND} run --remove-orphans --rm blade rake ${@:2}
54
+ ${DOCKER_COMPOSE_COMMAND} run --remove-orphans --rm blade rake "${@:2}"
55
55
  exit 0
56
56
  fi
57
57
 
58
58
  if [ "$1" == "cs" ] || [ "$1" == "c" ]
59
59
  then
60
- echo Running: ${DOCKER_COMPOSE_COMMAND} run blade rubocop "${@:2}"
61
- ${DOCKER_COMPOSE_COMMAND} run blade rubocop "${@:2}"
60
+ echo Running: ${DOCKER_COMPOSE_COMMAND} run --remove-orphans --rm blade rubocop "${@:2}"
61
+ ${DOCKER_COMPOSE_COMMAND} run --remove-orphans --rm blade rubocop "${@:2}"
62
62
  exit 0
63
63
  fi
64
64
 
65
65
  if [ "$1" == "cs:fix" ]
66
66
  then
67
- echo Running: ${DOCKER_COMPOSE_COMMAND} run blade rubocop --autocorrect "${@:2}"
68
- ${DOCKER_COMPOSE_COMMAND} run blade rubocop --autocorrect "${@:2}"
67
+ echo Running: ${DOCKER_COMPOSE_COMMAND} run --remove-orphans --rm blade rubocop --autocorrect "${@:2}"
68
+ ${DOCKER_COMPOSE_COMMAND} run --remove-orphans --rm blade rubocop --autocorrect "${@:2}"
69
69
  exit 0
70
70
  fi
71
71
 
72
72
  if [ "$1" == "test" ] || [ "$1" == "t" ]
73
73
  then
74
- echo Running: ${DOCKER_COMPOSE_COMMAND} run blade rake test "${@:2}"
75
- ${DOCKER_COMPOSE_COMMAND} run blade rake test "${@:2}"
74
+ echo Running: ${DOCKER_COMPOSE_COMMAND} run --remove-orphans --rm blade rake test "${@:2}"
75
+ ${DOCKER_COMPOSE_COMMAND} run --remove-orphans --rm blade rake test "${@:2}"
76
76
  exit 0
77
77
  fi
78
78
 
@@ -7,7 +7,7 @@ module RBlade
7
7
  next if token.type != :unprocessed
8
8
 
9
9
  token.value.gsub!(/\{\{--(?:[^-]++|-)*?--}}/, "")
10
- token.value.gsub!(/<%#(?:[^%]++|-)*?%>/, "")
10
+ token.value.gsub!(/<%#(?:[^%]++|%)*?%>/, "")
11
11
  end
12
12
  end
13
13
  end
@@ -45,7 +45,7 @@ module RBlade
45
45
  attributes = compile_attributes token.value[:attributes]
46
46
 
47
47
  if component[:name].start_with? "slot::"
48
- "_slot.call(:'#{RBlade.escape_quotes(component[:name].delete_prefix("slot::"))}', {#{attributes.join(",")}}) do;"
48
+ "_slot.call(:'#{RBlade.escape_quotes(component[:name].delete_prefix("slot::"))}', **{#{attributes.join(",")}}) do;"
49
49
  else
50
50
  "#{@component_store.component(component[:name])}(RBlade::AttributesManager.new({#{attributes.join(",")}})) do |_slot|;"
51
51
  end
@@ -67,7 +67,7 @@ module RBlade
67
67
 
68
68
  attributes = compile_attributes token.value[:attributes]
69
69
 
70
- "@output_buffer.raw_buffer<<#{RBlade.component_helper_method_name}(#{component_value}, '#{RBlade.escape_quotes(@component_store.current_view_name)}', #{attributes.join ","}) do;"
70
+ "@output_buffer.raw_buffer<<#{RBlade.component_helper_method_name}(#{component_value}, '#{RBlade.escape_quotes(@component_store.current_view_name)}', #{attributes.join ","}) do |_slot|;"
71
71
  end
72
72
 
73
73
  def compile_token_end(token)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rblade/helpers/tokenizer"
4
-
5
3
  module RBlade
6
4
  class CompilesStatements
7
5
  class CompilesComponentHelpers
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rblade/helpers/tokenizer"
3
+ require "rblade/helpers/regular_expressions"
4
4
  require "ripper"
5
5
 
6
6
  module RBlade
@@ -9,161 +9,171 @@ module RBlade
9
9
  tokens.map! do |token|
10
10
  next(token) if token.type != :unprocessed
11
11
 
12
- segments = token.value.split(/
13
- (\s)?(?<!\w)
12
+ current_match_id = nil
13
+ segments = []
14
+ token.value.split(/
15
+ \s?(?<!\w|@)
14
16
  (?:
15
17
  (?:
16
- (@@)
17
- (\w++[!\?]?)
18
+ (?<escaped_at>@@)
19
+ (?=\w++[!\?]?(?!\w))
18
20
  )
19
21
  |
20
22
  (?:
21
- (@)
22
- (\w++[!\?]?)
23
- (?:([ \t]*+)
24
- (\([^)]*+\))
23
+ @
24
+ (?# Statement name )
25
+ (?<statement_name>\w++[!\?]?)
26
+ (?# Optional parameters )
27
+ (?:
28
+ [ \t]*+
29
+ (?# Matched parentheses )
30
+ (?<statement_arguments>
31
+ \(
32
+ (?:
33
+ [^()#{RegularExpressions::RUBY_STRING_CHARACTERS}]++
34
+ |
35
+ #{RegularExpressions::RUBY_STRING}
36
+ |
37
+ \g<statement_arguments>
38
+ )*+
39
+ \)
40
+ )
25
41
  )?
26
42
  )
27
43
  )
28
- (\s)?
29
- /x)
30
-
31
- parse_segments! segments
32
- end.flatten!
33
- end
34
-
35
- private
36
-
37
- def parse_segments!(segments)
38
- i = 0
39
- while i < segments.count
40
- segment = segments[i]
41
-
42
- # The @ symbol is used to escape blade directives so we return it unprocessed
43
- if segment == "@@"
44
- segments[i] = Token.new(type: :unprocessed, value: segment[1..] + segments[i + 1])
45
- segments.delete_at i + 1
46
-
47
- i += 1
48
- elsif segment == "@"
49
- statement_handle = segments[i + 1].downcase.tr "_", ""
50
- if CompilesStatements.has_handler(statement_handle)
51
- tokenize_statement! statement_handle, segments, i
52
- handle_special_cases! segments, i
53
-
54
- segments.delete_at(i + 1) if segments[i + 1]&.match?(/\A\s\z/)
55
- if segments[i - 1].is_a?(Token) && segments[i - 1].type == :unprocessed && segments[i - 1].value.match?(/\A\s\z/)
56
- segments.delete_at i - 1
57
- i -= 1
44
+ \s?
45
+ /xo) do |before_match|
46
+ next if current_match_id == $~.object_id
47
+ current_match_id = $~.object_id
48
+
49
+ # Add the current string to the segment list
50
+ unless before_match == ""
51
+ # Skip output between case and when statements
52
+ unless segments.last&.type == :statement && segments.last&.value&.[](:name) == "case"
53
+ if segments.last && segments.last.type == :unprocessed
54
+ segments.last.value << before_match
55
+ else
56
+ segments << Token.new(type: :unprocessed, value: before_match)
57
+ end
58
58
  end
59
- else
60
- # For unhandled statements, restore the original string
61
- segments[i] = Token.new(type: :unprocessed, value: segments[i] + segments[i + 1])
62
- segments.delete_at i + 1
63
-
64
- if segments.count > i + 2 && segments[i + 1].match?(/\A[ \t]*+\z/) && segments[i + 2][0] == "("
65
- segments[i].value += segments[i + 1] + segments[i + 2]
66
- segments.delete_at i + 1
67
- segments.delete_at i + 1
68
- elsif segments.count > i + 1 && segments[i + 1][0] == "("
69
- segments[i].value += segments[i + 1]
70
- segments.delete_at i + 1
59
+ end
60
+ next if $~.nil?
61
+
62
+ # Skip escaped statements
63
+ if $~&.[](:escaped_at) == "@@"
64
+ segment = $&
65
+ # Remove the first or second @, depending on whether there is whitespace
66
+ segment.slice!(1).inspect
67
+ if segments.last && segments.last.type == :unprocessed
68
+ segments.last.value << segment
69
+ else
70
+ segments << Token.new(type: :unprocessed, value: segment)
71
71
  end
72
+
73
+ next
72
74
  end
73
75
 
74
- i += 1
75
- elsif !segments[i].nil? && segments[i] != ""
76
- segments[i] = Token.new(type: :unprocessed, value: segments[i])
76
+ statement_handle = $~[:statement_name].downcase.tr("_", "")
77
+ unless CompilesStatements.has_handler(statement_handle)
78
+ if segments.last && segments.last.type == :unprocessed
79
+ segments.last.value << $&
80
+ else
81
+ segments << Token.new(type: :unprocessed, value: $&)
82
+ end
77
83
 
78
- i += 1
79
- else
80
- segments.delete_at i
81
- end
82
- end
84
+ next
85
+ end
83
86
 
84
- segments
85
- end
87
+ statement_data = {name: statement_handle}
86
88
 
87
- def tokenize_statement!(handle, segments, i)
88
- segments.delete_at i + 1
89
- statement_data = {name: handle}
89
+ unless $~[:statement_arguments].blank?
90
+ arguments = tokenize_arguments! statement_handle, $~[:statement_arguments]
90
91
 
91
- # Remove optional whitespace
92
- if segments.count > i + 2 && segments[i + 1].match?(/\A[ \t]*+\z/) && segments[i + 2][0] == "("
93
- segments.delete_at i + 1
94
- end
92
+ unless arguments.nil?
93
+ statement_data[:arguments] = arguments
94
+ end
95
95
 
96
- if segments.count > i + 1 && segments[i + 1][0] == "("
97
- arguments = tokenize_arguments! handle, segments, i + 1
96
+ end
98
97
 
99
- unless arguments.nil?
100
- statement_data[:arguments] = arguments
98
+ segments << Token.new(type: :statement, value: statement_data)
101
99
  end
102
- end
103
100
 
104
- segments[i] = Token.new(type: :statement, value: statement_data)
105
- end
106
-
107
- def handle_special_cases!(segments, i)
108
- if segments[i][:value][:name] == "case"
109
- # Remove any whitespace before a when statement
110
- until segments[i + 1].nil? || segments[i + 1] == "@"
111
- segments.delete_at i + 1
112
- end
113
- end
101
+ segments
102
+ end.flatten!
114
103
  end
115
104
 
116
- def tokenize_arguments!(statement_handle, segments, segment_index)
117
- success = expand_segment_to_end_parenthesis! segments, segment_index
118
-
119
- # If no matching parentheses were found, so we combine the argument string with the next segment
120
- unless success
121
- unless segments[segment_index + 1].nil?
122
- segments[segment_index] <<= segments[segment_index + 1]
123
- segments.delete_at segment_index + 1
124
- end
125
-
126
- return nil
127
- end
105
+ private
128
106
 
129
- # Remove the parentheses from the argument string
130
- argument_string = segments[segment_index][1..-2]
107
+ def tokenize_arguments!(statement_handle, argument_string)
108
+ argument_string.delete_prefix! "("
109
+ argument_string.delete_suffix! ")"
131
110
 
132
- # Special case for the props statement: remove the wrapping braces if they exist
133
111
  if statement_handle == "props"
134
- if argument_string.start_with?("{") && argument_string.end_with?("}")
135
- argument_string = argument_string[1..-2]
136
- end
112
+ # Special case for the props statement: remove wrapping braces if they exist
113
+ argument_string.delete_prefix! "{"
114
+ argument_string.delete_suffix! "}"
137
115
  end
138
116
 
139
- arguments = Tokenizer.extract_comma_separated_values argument_string
140
- segments.delete_at segment_index
141
-
142
- arguments
143
- end
144
-
145
- def expand_segment_to_end_parenthesis!(segments, segment_index)
146
- parentheses_difference = 0
147
-
148
- loop do
149
- tokens = Ripper.lex(segments[segment_index]).map { |token| token[1] }
150
- parentheses_difference = tokens.count(:on_lparen) - tokens.count(:on_rparen)
151
-
152
- break if parentheses_difference.zero? || segments[segment_index + 1].nil?
153
-
154
- index = segments[segment_index + 1].each_char.find_index { |c| c == ")" && (parentheses_difference -= 1).zero? }
117
+ argument_string.strip!
118
+ return nil if argument_string == ""
155
119
 
156
- if index.nil?
157
- segments[segment_index] << segments[segment_index + 1]
158
- segments.delete_at segment_index + 1
159
- else
160
- segments[segment_index] << segments[segment_index + 1].slice!(0..index)
161
- end
162
-
163
- break if segments[segment_index + 1].nil?
120
+ current_match_id = nil
121
+ arguments = []
122
+ argument_string.split(/
123
+ \G
124
+ (?<argument>
125
+ (?:
126
+ [^,\(\{\[#{RegularExpressions::RUBY_STRING_CHARACTERS}]++
127
+ |
128
+ #{RegularExpressions::RUBY_STRING}
129
+ |
130
+ (?<parentheses>
131
+ \(
132
+ (?:
133
+ [^\(\)#{RegularExpressions::RUBY_STRING_CHARACTERS}]++
134
+ |
135
+ \g<string>
136
+ |
137
+ \g<parentheses>
138
+ )*+
139
+ \)
140
+ )
141
+ |
142
+ (?<brackets>
143
+ \[
144
+ (?:
145
+ [^\[\]#{RegularExpressions::RUBY_STRING_CHARACTERS}]++
146
+ |
147
+ \g<string>
148
+ |
149
+ \g<brackets>
150
+ )*+
151
+ \]
152
+ )
153
+ |
154
+ (?<braces>
155
+ \{
156
+ (?:
157
+ [^\{\}#{RegularExpressions::RUBY_STRING_CHARACTERS}]++
158
+ |
159
+ \g<string>
160
+ |
161
+ \g<braces>
162
+ )*+
163
+ \}
164
+ )
165
+ )*+
166
+ )
167
+ ,
168
+ /xmo, -1) do |x|
169
+ next if current_match_id == $~.object_id
170
+ current_match_id = $~.object_id
171
+
172
+ argument = ($~&.[](:argument) || x).strip
173
+ arguments << argument unless argument == ""
164
174
  end
165
175
 
166
- parentheses_difference.zero?
176
+ arguments
167
177
  end
168
178
  end
169
179
  end
@@ -86,7 +86,7 @@ module RBlade
86
86
 
87
87
  compiled_component = RBlade::Compiler.compile_string(template, self)
88
88
 
89
- @component_definitions << "def self._rblade_component_#{escaped_name}(attributes,&);slot=if block_given?;RBlade::SlotManager.new(@output_buffer.capture(->(name, slot_attributes, &slot_block)do;attributes[name]=RBlade::SlotManager.new(@output_buffer.capture(&slot_block), slot_attributes);end,&));end;_stacks=[];@output_buffer.raw_buffer<<@output_buffer.capture do;#{compiled_component}@output_buffer.raw_buffer.prepend(@_rblade_stack_manager.get(_stacks));end;end;"
89
+ @component_definitions << "def self._rblade_component_#{escaped_name}(attributes,&);slot=if block_given?;RBlade::SlotManager.new(@output_buffer.capture(->(name, **slot_attributes, &slot_block)do;attributes[name]=RBlade::SlotManager.new(@output_buffer.capture(&slot_block), slot_attributes);end,&));end;slot.nil?;_stacks=[];@output_buffer.raw_buffer<<@output_buffer.capture do;#{compiled_component}@output_buffer.raw_buffer.prepend(@_rblade_stack_manager.get(_stacks));end;end;"
90
90
 
91
91
  @component_method_names[name] = "_rblade_component_#{escaped_name}"
92
92
  end
@@ -0,0 +1,135 @@
1
+ module RBlade::RegularExpressions
2
+ RUBY_STRING_CHARACTERS = "\"'%?<"
3
+ RUBY_STRING = /
4
+ (?<string>
5
+ (?# Interpolated strings )
6
+ "
7
+ (?:
8
+ [^#"\\]++
9
+ |
10
+ \#(?<curly>\{
11
+ (?:
12
+ [^"'{}?%]++
13
+ |
14
+ \g<string>
15
+ |
16
+ \g<curly>
17
+ )*+
18
+ \})
19
+ |
20
+ \\.
21
+ |
22
+ (?!\#\{)\#[@$]?
23
+ )*+
24
+ "
25
+ |
26
+ (?# Non interpolated strings )
27
+ '
28
+ (?:
29
+ [^'\\]++
30
+ |
31
+ \\.
32
+ )*+
33
+ '
34
+ |
35
+ (?# Non interpolated percent expressions )
36
+ %[qwis]
37
+ (?:
38
+ (?<ni_parentheses> \( (?: [^()\\]++ | \\. | \g<ni_parentheses> )*+ \) )
39
+ |
40
+ (?<ni_brackets> \[ (?: [^\[\]\\]++ | \\. | \g<ni_brackets> )*+ \] )
41
+ |
42
+ (?<ni_crocs> < (?: [^<>\\]++ | \\. | \g<ni_crocs> )*+ > )
43
+ |
44
+ (?<ni_braces> \{ (?: [^{}\\]++ | \\. | \g<ni_braces> )*+ \} )
45
+ |
46
+ (?<percent_delimiter>[\x00-\x7F&&[^a-zA-Z0-9(\[{<]])
47
+ (?:
48
+ [a-zA-Z0-9(\[{<[^\x00-\x7F]]++
49
+ |
50
+ \\.
51
+ |
52
+ (?!\k<percent_delimiter>)[^\\]
53
+ )*?
54
+ \k<percent_delimiter>
55
+ )
56
+ |
57
+ (?# Interpolated percent expressions )
58
+ %[QWIrx]?
59
+ (?:
60
+ (?<i_parentheses> \( (?: [^()\\#]++ | \#\g<curly> | \\. | \g<i_parentheses> )*+ \) )
61
+ |
62
+ (?<i_brackets> \[ (?: [^\[\]\\#]++ | \#\g<curly> | \\. | \g<i_brackets> )*+ \] )
63
+ |
64
+ (?<i_crocs> < (?: [^<>\\#]++ | \#\g<curly> | \\. | \g<i_crocs> )*+ > )
65
+ |
66
+ (?<i_braces> \{ (?: [^{}\\#]++ | \#\g<curly> | \\. | \g<i_braces> )*+ \} )
67
+ |
68
+ \g<percent_delimiter>
69
+ (?:
70
+ [a-zA-Z0-9(\[{<[^\x00-\x7F]]++
71
+ |
72
+ \#\g<curly>
73
+ |
74
+ \\.
75
+ |
76
+ (?!\k<percent_delimiter>)[^\\#]
77
+ |
78
+ (?!\#\{)\#[@$]?+
79
+ )*?
80
+ (?!\#\{)\k<percent_delimiter>
81
+ )
82
+ |
83
+ (?# Interpolated HEREDOC)
84
+ <<(?:(?<heredoc_delimiter>[a-zA-Z_]\w*+)|"\g<heredoc_delimiter>")[^\n]*+\n
85
+ (?:
86
+ (?!\k<heredoc_delimiter>$)[^\#\n]++
87
+ |
88
+ \#\g<curly>
89
+ |
90
+ (?!\#\{)\#[^\#\n]++
91
+ |
92
+ \n
93
+ )*?
94
+ \n\k<heredoc_delimiter>$
95
+ |
96
+ (?# Interpolated HEREDOC with leading spaces)
97
+ <<[-~](?:\g<heredoc_delimiter>|"\g<heredoc_delimiter>")[^\n]*+\n
98
+ (?:
99
+ (?!\k<heredoc_delimiter>$)[^\#\n]++
100
+ |
101
+ \#\g<curly>
102
+ |
103
+ (?!\#\{)\#[^\#\n]++
104
+ |
105
+ \n
106
+ )*?
107
+ \n\s*+\k<heredoc_delimiter>$
108
+ |
109
+ (?# Non-interpolated HEREDOC)
110
+ <<'\g<heredoc_delimiter>'[^\n]*+\n
111
+ (?:
112
+ (?!\k<heredoc_delimiter>$)[^\n]*+\n
113
+ )*+
114
+ \k<heredoc_delimiter>$
115
+ |
116
+ (?# Non-interpolated HEREDOC with leading spaces)
117
+ <<[-~]'\g<heredoc_delimiter>'[^\n]*+\n
118
+ (?:
119
+ (?!\s*\k<heredoc_delimiter>$)[^\n]*+\n
120
+ )*+
121
+ \s*\k<heredoc_delimiter>$
122
+ |
123
+ (?<!\w)\?.
124
+ |
125
+ (?# Consume characters that aren't string literals)
126
+ (?<=\w)\?
127
+ |
128
+ (?# A percentage sign that's not a percent literal)
129
+ %(?![qwisQWIrx]?+\g<percent_delimiter>)
130
+ )
131
+ |
132
+ (?# Consume angled brackets that aren't HEREDOCs)
133
+ <(?!<[~-]?['"a-zA-Z_])
134
+ /mx
135
+ end
@@ -23,7 +23,7 @@ module RBlade
23
23
  end
24
24
 
25
25
  def setup_component_view_helper(mod)
26
- mod.send(:define_method, RBlade.component_helper_method_name) do |component_name, current_view = nil, **attributes, &block|
26
+ mod.send(:define_method, RBlade.component_helper_method_name) do |component_name, current_view = nil, slot: "", **attributes, &block|
27
27
  # If this is a relative path, prepend with the previous component name's base
28
28
  if !current_view.nil? && component_name.start_with?(".")
29
29
  component_name = current_view.sub(/[^\.]++\z/, "") + component_name.delete_prefix(".")
@@ -37,12 +37,22 @@ module RBlade
37
37
  end
38
38
  path.sub!(/(?:\.[^.]++)?\.rblade\z/, "")
39
39
 
40
- locals = {
41
- slot: block.nil? ? attributes.delete(:slot) || -"" : capture(&block),
42
- attributes: RBlade::AttributesManager.new(attributes),
43
- }
40
+ attributes = RBlade::AttributesManager.new(attributes)
44
41
 
45
- render template: path, locals:
42
+ unless block.nil?
43
+ value = nil
44
+ slot = @output_buffer.capture do
45
+ value = block.call(->(name, **slot_attributes, &slot_block) do
46
+ attributes[name] = RBlade::SlotManager.new(@output_buffer.capture(&slot_block), slot_attributes)
47
+ end)
48
+ end
49
+
50
+ if slot.blank?
51
+ slot = value || ""
52
+ end
53
+ end
54
+
55
+ render template: path, locals: {slot: RBlade::SlotManager.new(slot), attributes:}
46
56
  end
47
57
  end
48
58
  end
data/rblade.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "rblade"
3
- s.version = "3.0.1"
3
+ s.version = "3.1.0"
4
4
  s.summary = "A component-first templating engine for Rails"
5
5
  s.description = "RBlade is a simple, yet powerful templating engine for Ruby on Rails, inspired by Laravel Blade."
6
6
  s.authors = ["Simon J"]
@@ -14,7 +14,7 @@ Gem::Specification.new do |s|
14
14
 
15
15
  s.add_development_dependency "minitest", "~> 5.0"
16
16
  s.add_development_dependency "minitest-reporters", "~> 1.1"
17
- s.add_development_dependency "standard", ">= 1.3"
17
+ s.add_development_dependency "standard", ">= 1.35.1"
18
18
  s.add_development_dependency "rubocop", ">= 1.73"
19
19
  s.add_development_dependency "rails", ">= 7.0"
20
20
  s.add_development_dependency "benchmark-ips"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rblade
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.1
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon J
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-03-18 00:00:00.000000000 Z
11
+ date: 2025-04-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '1.3'
47
+ version: 1.35.1
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '1.3'
54
+ version: 1.35.1
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rubocop
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -154,10 +154,10 @@ files:
154
154
  - lib/rblade/component_store.rb
155
155
  - lib/rblade/helpers/attributes_manager.rb
156
156
  - lib/rblade/helpers/class_manager.rb
157
+ - lib/rblade/helpers/regular_expressions.rb
157
158
  - lib/rblade/helpers/slot_manager.rb
158
159
  - lib/rblade/helpers/stack_manager.rb
159
160
  - lib/rblade/helpers/style_manager.rb
160
- - lib/rblade/helpers/tokenizer.rb
161
161
  - lib/rblade/rails_template.rb
162
162
  - lib/rblade/railtie.rb
163
163
  - rblade.gemspec
@@ -1,65 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RBlade
4
- class Tokenizer
5
- def self.extract_comma_separated_values(segment)
6
- unless segment.match?(/,\s*+\z/)
7
- # Add a comma to the end to delimit the end of the last argument
8
- segment += ","
9
- end
10
- segment_lines = segment.lines
11
-
12
- tokens = Ripper.lex segment
13
- arguments = []
14
-
15
- current_line = 1
16
- current_index = 0
17
- bracket_count = {
18
- "[]": 0,
19
- "{}": 0,
20
- "()": 0,
21
- }
22
- tokens.each do |token|
23
- case token[1]
24
- when :on_lbracket
25
- bracket_count[:[]] += 1
26
- when :on_rbracket
27
- bracket_count[:[]] -= 1
28
- when :on_lbrace
29
- bracket_count[:"{}"] += 1
30
- when :on_rbrace
31
- bracket_count[:"{}"] -= 1
32
- when :on_lparen
33
- bracket_count[:"()"] += 1
34
- when :on_rparen
35
- bracket_count[:"()"] -= 1
36
- when :on_comma
37
- if bracket_count[:[]] != 0 || bracket_count[:"{}"] != 0 || bracket_count[:"()"] != 0
38
- next
39
- end
40
-
41
- argument = +""
42
-
43
- # Concatenate all lines up to this token's line, including the tail end of the current line
44
- if token[0][0] != current_line
45
- (current_line...token[0][0]).each do |i|
46
- argument << (segment_lines[i - 1].slice(current_index..-1) || "")
47
- current_index = 0
48
- end
49
- current_line = token[0][0]
50
- end
51
- argument << segment_lines[current_line - 1].slice(current_index...token[0][1])
52
- argument.strip!
53
-
54
- arguments.push argument
55
-
56
- current_index = token[0][1] + 1
57
- end
58
- end
59
-
60
- return nil if arguments.count == 1 && arguments[0] == ""
61
-
62
- arguments
63
- end
64
- end
65
- end