rblade 3.1.0 → 3.2.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: 565a823219a85db94f6fa31e42de52cab5d1de562c89c42243a0105d54eab581
4
- data.tar.gz: 484ca84492233eabd1c61a534be991cfdb489273751e4e43a1b5ac400965b867
3
+ metadata.gz: b2a97583c74dd9bff9b805c0eeedc59751909eea8bd3130e6947ca5e70355b30
4
+ data.tar.gz: fceb38fd16cbb4e569e8cdbf0082710862388d98e428bf99a81f6bc8e4f15e0a
5
5
  SHA512:
6
- metadata.gz: a3fa94d7c7c3f19f3671dd346bffedbee1fdaaf3b93ce0928cba1b6bb7d24a7c7da83c016863df9aa8c5cbdd0d5519d65efef7aa18a59a77a7861764e8188f03
7
- data.tar.gz: 29325e82f35eb1e6b949b49684ea5c78cea910ffeeacf18ef012c39ff6cbe2a73e26bd47d1753734cc712421b19a958dab54224c024c25ca580b53b34b6dcb9a
6
+ metadata.gz: 1d8d277be52f54f2a524eacb9f7f5cf8095032f2d076bb893640ed7e86be6a49dcde5eb50e0309f592c2d69451b171f7d265eb198fd324eadcc41daf1df74b70
7
+ data.tar.gz: 5ac646bdcbc33e0183c981ac651cf8493beaf2494b1c07fb3aa13c933eb50115d8673bb6ff37ee2f17f3f8e367e6020ca3433c2d656b5d87d0be0bb9eeaf6b3f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## 3.2.0 [UNRELEASED]
2
+ - Add translate_location method to improve error messages in RBlade templates
3
+
4
+ ## 3.1.1 [2025-05-11]
5
+ - Fix issue with component attributes mutating unfrozen strings (#19)
6
+
1
7
  ## 3.1.0 [2025-04-02]
2
8
  - Add ability to use slots in dynamic components and the component view helper method
3
9
  - Change statement matching to use regular expressions to improve compile performance
@@ -10,5 +10,22 @@ module RBlade
10
10
  token.value.gsub!(/<%#(?:[^%]++|%)*?%>/, "")
11
11
  end
12
12
  end
13
+
14
+ def self.comment_offsets(source)
15
+ current_match_id = nil
16
+ offsets = []
17
+
18
+ source.split(/(\{\{--(?:[^-]++|-)*?--}}|<%#(?:[^%]++|%)*?%>)/) do |before_match|
19
+ next if current_match_id == $~.object_id || $&.nil?
20
+ current_match_id = $~.object_id
21
+
22
+ offsets << {
23
+ source_position: (offsets.last&.[](:source_position) || 0) + before_match.length,
24
+ offset: $&.length,
25
+ }
26
+ end
27
+
28
+ offsets
29
+ end
13
30
  end
14
31
  end
@@ -107,15 +107,13 @@ module RBlade
107
107
  end
108
108
 
109
109
  def process_string_attribute(string)
110
- result = string.split(/((?<!@)\{\{(?:[^}]++|\})*?\}\})/).map do |substring|
110
+ string.split(/((?<!@)\{\{(?:[^}]++|\})*?\}\})/).map do |substring|
111
111
  if substring.start_with?("{{") && substring.end_with?("}}")
112
112
  "(#{substring[2..-3]}).to_s"
113
113
  elsif !substring.empty?
114
114
  "'#{RBlade.escape_quotes(substring.gsub(/@\{\{/, "{{"))}'"
115
115
  end
116
- end.compact.join("<<")
117
-
118
- result.empty? ? "+''" : result.prepend("+")
116
+ end.compact.unshift("+''").join("<<")
119
117
  end
120
118
  end
121
119
  end
@@ -6,56 +6,79 @@ module RBlade
6
6
  tokens.map! do |token|
7
7
  next(token) if token.type != :unprocessed
8
8
 
9
- segments = token.value.split(/
10
- (@?\{!!)(\s*+(?:[^!}%@]++|[!}%@])+?\s*+)(!!})
9
+ current_match_id = nil
10
+ segments = []
11
+ token.value.split(/
12
+ (?<escape_unsafe_rblade>@?)\{!!(?<unsafe_rblade>(?:[^!}]++|[!}])+?)!!}
11
13
  |
12
- (@?\{\{)(\s*+(?:[^!}%@]++|[!}%@])+?\s*+)(}})
14
+ (?<escape_safe_rblade>@?) \{\{ (?<safe_rblade>(?:[^!}]++|[!}])+?) }}
13
15
  |
14
- (\s?(?<!\w)@?@ruby)(\s++(?:[^!}%@]++|[!}%@])+?\s*+)((?<!\w)@end_?ruby(?!\w)\s?)
16
+ \s?(?<![\w@])@ruby\s++(?<ruby>(?:[^@]++|[@])+?)(?<!\w)@end_?ruby(?!\w)\s?
15
17
  |
16
- (<%%?=?=?)(\s*+(?:[^!}%@]++|[!}%@])+?\s*+)(%%?>)
17
- /xi)
18
+ (?<escaped_erb_start><%%)
19
+ |
20
+ (?<escaped_erb_end>%%>)
21
+ |
22
+ (?<erb_tag><%(?!%)=?=?)\s*+(?<erb_tag_content>(?:[^%]++|[%])+?)(?<!%)%?>
23
+ /xi) do |before_match|
24
+ next if current_match_id == $~.object_id
25
+ current_match_id = $~.object_id
18
26
 
19
- i = 0
20
- while i < segments.count
21
- case segments[i]
22
- when "{{", "<%="
23
- segments[i] = create_token(segments[i + 1].strip, true)
24
- when "<%", /\A\s?@ruby\z/i
25
- segments[i + 1].strip!
26
- segments[i + 1] << ";" unless segments[i + 1].end_with?(";")
27
- segments[i] = Token.new(type: :ruby, value: segments[i + 1])
28
- when "{!!", "<%=="
29
- segments[i] = create_token(segments[i + 1].strip, false)
30
- when "@{!!", "@{{", /\A\s?@@ruby\z/i
31
- segments[i].sub!("@", "")
32
- segments[i] = Token.new(type: :raw_text, value: "#{segments[i]}#{segments[i + 1]}#{segments[i + 2]}")
33
- when "<%%", "<%%=", "<%%=="
34
- segments[i] = Token.new(type: :raw_text, value: "<#{segments[i].delete_prefix!("<%")}#{segments[i + 1]}%>")
35
- when "", nil
36
- segments.delete_at i
37
- next
38
- else
39
- segments[i] = Token.new(type: :unprocessed, value: segments[i])
40
- i += 1
41
- next
27
+ unless before_match == ""
28
+ RBlade::Utility.append_unprocessed_string_segment!(token, segments, before_match)
42
29
  end
30
+ next if $~.nil?
31
+
32
+ if $~[:unsafe_rblade].present? || $~[:erb_tag] == "<%=="
33
+ if $~[:escape_unsafe_rblade] == "@"
34
+ RBlade::Utility.append_unprocessed_string_segment!(token, segments, $&.delete_prefix("@"))
35
+ else
36
+ start_offset = segments.last&.end_offset || token.start_offset
37
+ segments << create_token(
38
+ ($~[:unsafe_rblade] || $~[:erb_tag_content]).strip,
39
+ false,
40
+ start_offset,
41
+ start_offset + $&.length,
42
+ )
43
+ end
44
+ elsif $~[:safe_rblade].present? || $~[:erb_tag] == "<%="
45
+ if $~[:escape_safe_rblade] == "@"
46
+ RBlade::Utility.append_unprocessed_string_segment!(token, segments, $&.delete_prefix("@"))
47
+ else
48
+ start_offset = segments.last&.end_offset || token.start_offset
49
+ segments << create_token(
50
+ ($~[:safe_rblade] || $~[:erb_tag_content]).strip,
51
+ true,
52
+ start_offset,
53
+ start_offset + $&.length,
54
+ )
55
+ end
56
+ elsif $~[:ruby].present? || $~[:erb_tag] == "<%"
57
+ value = ($~[:ruby] || $~[:erb_tag_content]).strip
58
+ value << ";" unless value.end_with?(";")
59
+ start_offset = segments.last&.end_offset || token.start_offset
43
60
 
44
- segments.delete_at i + 1
45
- segments.delete_at i + 1
46
- i += 1
61
+ segments << Token.new(
62
+ type: :ruby,
63
+ value: value,
64
+ start_offset: start_offset,
65
+ end_offset: start_offset + $&.length,
66
+ )
67
+ elsif $~[:escaped_erb_start].present?
68
+ RBlade::Utility.append_unprocessed_string_segment!(token, segments, +"<%")
69
+ elsif $~[:escaped_erb_end].present?
70
+ RBlade::Utility.append_unprocessed_string_segment!(token, segments, +"%>")
71
+ end
47
72
  end
48
73
 
49
74
  segments
50
75
  end.flatten!
51
76
  end
52
77
 
53
- private
54
-
55
- def create_token(expression, escape_html)
78
+ private def create_token(expression, escape_html, start_offset, end_offset)
56
79
  # Don't try to print ends
57
80
  if expression.match?(/\A(?:}|end(?![[:alnum:]_]|[^\0-\177]))/i)
58
- return Token.new(:ruby, "#{expression};")
81
+ return Token.new(:ruby, "#{expression};", start_offset, end_offset)
59
82
  end
60
83
 
61
84
  segment_value = if escape_html
@@ -75,7 +98,7 @@ module RBlade
75
98
  "@output_buffer.raw_buffer<<(#{expression}).to_s;"
76
99
  end
77
100
 
78
- Token.new(:print, segment_value)
101
+ Token.new(:print, segment_value, start_offset, end_offset)
79
102
  end
80
103
  end
81
104
  end
@@ -6,26 +6,38 @@ module RBlade
6
6
  tokens.map! do |token|
7
7
  next(token) if token.type != :unprocessed
8
8
 
9
- segments = token.value.split(/\s?(?<!\w)(@verbatim)(?!\w)\s?((?:[^@\s]++|[@\s])+?)\s?(?<!\w)@end_?verbatim(?!\w)\s?/i)
9
+ current_match_id = nil
10
+ segments = []
11
+ token.value.split(/\s?(?<!\w)@verbatim(?!\w)\s?(?<contents>(?:[^@\s]++|[@\s])+?)\s?(?<!\w)@end_?verbatim(?!\w)\s?/i) do |before_match|
12
+ next if current_match_id == $~.object_id
13
+ current_match_id = $~.object_id
10
14
 
11
- i = 0
12
- while i < segments.count
13
- if segments[i] == "@verbatim"
14
- segments.delete_at i
15
- segments[i] = Token.new(type: :raw_text, value: segments[i])
16
-
17
- i += 1
18
- elsif !segments[i].nil? && segments[i] != ""
19
- segments[i] = Token.new(type: :unprocessed, value: segments[i])
15
+ # Add the current string to the segment list
16
+ unless before_match == ""
17
+ RBlade::Utility.append_unprocessed_string_segment!(token, segments, before_match)
18
+ end
19
+ next if $~.nil?
20
20
 
21
- i += 1
22
- else
23
- segments.delete_at i
21
+ if $~[:contents].present?
22
+ start_offset = segments.last&.end_offset || token.start_offset
23
+ segments << Token.new(
24
+ type: :raw_text,
25
+ value: $~[:contents],
26
+ start_offset: start_offset,
27
+ end_offset: start_offset + $&.length,
28
+ )
24
29
  end
25
30
  end
26
31
 
27
32
  segments
28
33
  end.flatten!
29
34
  end
35
+
36
+ # Replaces verbatim statements with spaces of the same length. Used in comment offset calculation for source maps.
37
+ def self.nullify_verbatim(source)
38
+ source.gsub(/\s?(?<!\w)@verbatim(?!\w)\s?(?<contents>(?:[^@\s]++|[@\s])+?)\s?(?<!\w)@end_?verbatim(?!\w)\s?/i) do |match|
39
+ " " * match.length
40
+ end
41
+ end
30
42
  end
31
43
  end
@@ -8,53 +8,106 @@ module RBlade
8
8
  tokens.map! do |token|
9
9
  next(token) if token.type != :unprocessed
10
10
 
11
- segments = tokenize_component_tags token.value
12
-
13
- i = 0
14
- while i < segments.count
15
- if segments[i] == "</" && segments[i + 1]&.match?(/x[-:]/)
16
- segments[i] = Token.new(type: :component_end, value: {name: segments[i + 1][2..]})
17
-
18
- segments.delete_at i + 1
19
- i += 1
20
- elsif segments[i] == "<//>"
21
- segments[i] = Token.new(type: :component_unsafe_end)
22
- i += 1
23
- elsif segments[i] == "<" && segments[i + 1]&.match?(/x[-:]/)
24
- name = segments[i + 1][2..]
25
- raw_attributes = (segments[i + 2] != ">") ? tokenize_attributes(segments[i + 2]) : nil
26
-
27
- attributes = process_attributes raw_attributes
28
-
29
- if raw_attributes.nil?
30
- segments.delete_at i + 1
31
- else
32
- while segments[i + 1] != ">" && segments[i + 1] != "/>"
33
- segments.delete_at i + 1
34
- end
35
- end
36
-
37
- token_type = (segments[i + 1] == "/>") ? :component : :component_start
38
- segments[i] = Token.new(type: token_type, value: {name:, attributes:})
39
- segments.delete_at i + 1
11
+ process_component_tags token
12
+ end.flatten!
13
+ end
40
14
 
41
- i += 1
42
- elsif !segments[i].nil? && segments[i] != ""
43
- segments[i] = Token.new(type: :unprocessed, value: segments[i])
15
+ private def process_component_tags(token)
16
+ current_match_id = nil
17
+ segments = []
44
18
 
45
- i += 1
46
- else
47
- segments.delete_at i
48
- end
19
+ token.value.split(%r/
20
+ # Opening and self-closing tags
21
+ (?<opening_tag>
22
+ <
23
+ \s*+
24
+ x[-\:](?<opening_tag_name>[\w\-\:\.]++)
25
+ (?<tag_attributes>
26
+ (?:
27
+ \s++
28
+ (?:
29
+ (?:
30
+ @class(?<nested_parentheses>\( (?: [^()]++ | \g<nested_parentheses> )*+ \))
31
+ )
32
+ |
33
+ (?:
34
+ @style\g<nested_parentheses>
35
+ )
36
+ |
37
+ \{\{ \s*+ attributes (?:[^}]++|\})*? \}\}
38
+ |
39
+ (?:
40
+ [\w\-:.@%]++
41
+ (?:
42
+ =
43
+ (?:
44
+ "(?> [^"{]++ | (?<!@)\{\{ (?:[^}]++|\})*? \}\} | \{ )*+"
45
+ |
46
+ '(?> [^'{]++ | (?<!@)\{\{ (?:[^}]++|\})*? \}\} | \{ )*+'
47
+ |
48
+ (?> [^'"=<>\s\/{]++ | (?<!@)\{\{ (?:[^}]++|\})*? \}\} | \{ )++
49
+ )
50
+ )?
51
+ )
52
+ )
53
+ )*+
54
+ )
55
+ \s*+
56
+ (?<opening_tag_end>\/?>)
57
+ )
58
+ |
59
+ # Closing tags
60
+ (?<closing_tag>
61
+ <\/
62
+ \s*+
63
+ x[-\:](?<closing_tag_name>[\w\-\:\.]++)
64
+ \s*+
65
+ >
66
+ )
67
+ |
68
+ (?<unsafe_closing_tag><\/\/>)
69
+ /x) do |before_match|
70
+ next if current_match_id == $~.object_id
71
+ current_match_id = $~.object_id
72
+
73
+ # Add the current string to the segment list
74
+ unless before_match == ""
75
+ RBlade::Utility.append_unprocessed_string_segment!(token, segments, before_match)
49
76
  end
77
+ next if $~.nil?
78
+
79
+ start_offset = segments.last&.end_offset || token.start_offset
80
+ if $~[:unsafe_closing_tag].present?
81
+ segments << Token.new(
82
+ type: :component_unsafe_end,
83
+ start_offset: start_offset,
84
+ end_offset: start_offset + $&.length,
85
+ )
86
+ elsif $~[:closing_tag].present?
87
+ segments << Token.new(
88
+ type: :component_end,
89
+ value: {name: $~[:closing_tag_name]},
90
+ start_offset: start_offset,
91
+ end_offset: start_offset + $&.length,
92
+ )
93
+ elsif $~[:opening_tag].present?
94
+ raw_attributes = tokenize_attributes($~[:tag_attributes])
95
+ attributes = process_attributes raw_attributes
96
+
97
+ token_type = ($~[:opening_tag_end] == "/>") ? :component : :component_start
98
+ segments << Token.new(
99
+ type: token_type,
100
+ value: {name: $~[:opening_tag_name], attributes:},
101
+ start_offset: start_offset,
102
+ end_offset: start_offset + $&.length,
103
+ )
104
+ end
105
+ end
50
106
 
51
- segments
52
- end.flatten!
107
+ segments
53
108
  end
54
109
 
55
- private
56
-
57
- def process_attributes(raw_attributes)
110
+ private def process_attributes(raw_attributes)
58
111
  attributes = []
59
112
  i = 0
60
113
  while i < raw_attributes.count
@@ -112,61 +165,7 @@ module RBlade
112
165
  attributes
113
166
  end
114
167
 
115
- def tokenize_component_tags(value)
116
- value.split(%r/
117
- # Opening and self-closing tags
118
- (?:
119
- (<)
120
- \s*+
121
- (x[-\:][\w\-\:\.]++)
122
- ((?:
123
- \s++
124
- (?:
125
- (?:
126
- @class(\( (?: [^()]++ | \g<-1> )*+ \))
127
- )
128
- |
129
- (?:
130
- @style(\( (?: [^()]++ | \g<-1> )*+ \))
131
- )
132
- |
133
- (
134
- \{\{ \s*+ attributes (?:[^}]++|\})*? \}\}
135
- )
136
- |
137
- (?:
138
- [\w\-:.@%]++
139
- (?:
140
- =
141
- (?:
142
- "(?> [^"{]++ | (?<!@)\{\{ (?:[^}]++|\})*? \}\} | \{ )*+"
143
- |
144
- '(?> [^'{]++ | (?<!@)\{\{ (?:[^}]++|\})*? \}\} | \{ )*+'
145
- |
146
- (?> [^'"=<>\s\/{]++ | (?<!@)\{\{ (?:[^}]++|\})*? \}\} | \{ )++
147
- )
148
- )?
149
- )
150
- )
151
- )*+)
152
- \s*+
153
- (\/?>)
154
- )
155
- |
156
- # Closing tags
157
- (?:
158
- (<\/)
159
- \s*+
160
- (x[-\:][\w\-\:\.]++)
161
- \s*+
162
- >
163
- )
164
- |
165
- (<\/\/>)
166
- /x)
167
- end
168
-
169
- def tokenize_attributes(segment)
168
+ private def tokenize_attributes(segment)
170
169
  segment.scan(%r/
171
170
  (?<=\s|^)
172
171
  (?:
@@ -16,7 +16,7 @@ module RBlade
16
16
  (?:
17
17
  (?:
18
18
  (?<escaped_at>@@)
19
- (?=\w++[!\?]?(?!\w))
19
+ (?<escaped_statement_name>\w++[!\?]?(?!\w))
20
20
  )
21
21
  |
22
22
  (?:
@@ -50,36 +50,29 @@ module RBlade
50
50
  unless before_match == ""
51
51
  # Skip output between case and when statements
52
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
53
+ RBlade::Utility.append_unprocessed_string_segment!(token, segments, before_match)
58
54
  end
59
55
  end
60
56
  next if $~.nil?
61
57
 
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
- end
58
+ statement_handle = ($~[:statement_name] || $~[:escaped_statement_name])
59
+ &.downcase
60
+ &.tr("_", "")
61
+ next if statement_handle.nil?
62
+
63
+ unless CompilesStatements.has_handler(statement_handle)
64
+ RBlade::Utility.append_unprocessed_string_segment!(token, segments, $&)
72
65
 
73
66
  next
74
67
  end
75
68
 
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
69
+ # Skip escaped statements
70
+ if $~&.[](:escaped_at) == "@@"
71
+ segment = $&.dup
72
+ # Remove the first or second @, depending on whether there is whitespace
73
+ segment.slice!(1)
74
+
75
+ RBlade::Utility.append_unprocessed_string_segment!(token, segments, segment, 1)
83
76
 
84
77
  next
85
78
  end
@@ -95,7 +88,13 @@ module RBlade
95
88
 
96
89
  end
97
90
 
98
- segments << Token.new(type: :statement, value: statement_data)
91
+ start_offset = segments.last&.end_offset || token.start_offset
92
+ segments << Token.new(
93
+ type: :statement,
94
+ value: statement_data,
95
+ start_offset: start_offset,
96
+ end_offset: start_offset + $&.length,
97
+ )
99
98
  end
100
99
 
101
100
  segments
@@ -7,9 +7,10 @@ require "rblade/compiler/compiles_verbatim"
7
7
  require "rblade/compiler/compiles_statements"
8
8
  require "rblade/compiler/tokenizes_components"
9
9
  require "rblade/compiler/tokenizes_statements"
10
+ require "rblade/helpers/utility"
10
11
  require "active_support/core_ext/string/output_safety"
11
12
 
12
- Token = Struct.new(:type, :value)
13
+ Token = Struct.new(:type, :value, :start_offset, :end_offset)
13
14
 
14
15
  module RBlade
15
16
  def self.escape_quotes(string)
@@ -38,7 +39,46 @@ module RBlade
38
39
 
39
40
  class Compiler
40
41
  def self.compile_string(string_template, component_store)
41
- tokens = [Token.new(:unprocessed, string_template)]
42
+ tokens = tokenize_string string_template, component_store
43
+
44
+ compile_tokens tokens
45
+ end
46
+
47
+ def self.generate_source_map(string_template, component_store)
48
+ tokens = tokenize_string string_template, component_store
49
+ source_map = SourceMap.new(string_template)
50
+
51
+ i = 0
52
+ while i < tokens.count
53
+ token = tokens[i]
54
+
55
+ if token.type == :unprocessed || token.type == :raw_text
56
+ start_offset = token.start_offset
57
+ compiled_code = +"@output_buffer.raw_buffer<<-'"
58
+
59
+ # Merge together consecutive prints
60
+ while tokens[i + 1]&.type == :unprocessed || tokens[i + 1]&.type == :raw_text
61
+ compiled_code << RBlade.escape_quotes(token.value)
62
+ i += 1
63
+ token = tokens[i]
64
+ end
65
+
66
+ compiled_code << RBlade.escape_quotes(token.value)
67
+ compiled_code << "';"
68
+
69
+ source_map.add(start_offset, token.end_offset, compiled_code)
70
+ else
71
+ source_map.add(token.start_offset, token.end_offset, token.value)
72
+ end
73
+
74
+ i += 1
75
+ end
76
+
77
+ source_map
78
+ end
79
+
80
+ def self.tokenize_string(string_template, component_store)
81
+ tokens = [Token.new(:unprocessed, string_template, 0, string_template.length)]
42
82
 
43
83
  CompilesVerbatim.new.compile! tokens
44
84
  CompilesComments.new.compile! tokens
@@ -50,7 +90,8 @@ module RBlade
50
90
  component_compiler = CompilesComponents.new(component_store)
51
91
  component_compiler.compile! tokens
52
92
  component_compiler.ensure_all_tags_closed
53
- compile_tokens tokens
93
+
94
+ tokens
54
95
  end
55
96
 
56
97
  def self.compile_attribute_string(string_template)
@@ -0,0 +1,58 @@
1
+ module RBlade
2
+ class SourceMap
3
+ def initialize(template)
4
+ @source_tokens = []
5
+ @compiled_column = 0
6
+ @compiled_offset = 0
7
+
8
+ calculate_comment_offsets(template)
9
+ end
10
+
11
+ private def calculate_comment_offsets(template)
12
+ template_without_verbatim = CompilesVerbatim.nullify_verbatim(template)
13
+
14
+ @comment_offsets = CompilesComments.comment_offsets(template_without_verbatim)
15
+ @current_comment_offset = 0
16
+ end
17
+
18
+ def add(start_offset, end_offset, compiled_code)
19
+ increment_comment_offset_to start_offset
20
+ start_offset += @current_comment_offset
21
+
22
+ # Prevent extending the end offset into a comment
23
+ increment_comment_offset_to end_offset - 1
24
+ end_offset += @current_comment_offset
25
+
26
+ lines = StringUtility.lines(compiled_code)
27
+
28
+ @source_tokens << {
29
+ start_offset: start_offset,
30
+ end_offset: end_offset,
31
+ compiled_start_line: @compiled_column,
32
+ compiled_start_offset: @compiled_offset,
33
+ }
34
+
35
+ @compiled_column += lines.length - 1
36
+ @compiled_offset = (lines.length == 1) ? @compiled_offset + compiled_code.length : lines.last.length
37
+ end
38
+
39
+ private def increment_comment_offset_to(source_position)
40
+ while @comment_offsets.any? && @comment_offsets.first[:source_position] <= source_position
41
+ @current_comment_offset += @comment_offsets.shift[:offset]
42
+ end
43
+ end
44
+
45
+ def source_location(first_lineno, first_column)
46
+ previous_token = nil
47
+
48
+ @source_tokens.each do |token|
49
+ break previous_token if token[:compiled_start_line] > first_lineno ||
50
+ (token[:compiled_start_line] == first_lineno && token[:compiled_start_offset] > first_column)
51
+
52
+ previous_token = token
53
+ end
54
+
55
+ previous_token
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,10 @@
1
+ class StringUtility
2
+ class << self
3
+ def lines(string)
4
+ lines = string.split(/(?<=\n)/, -1)
5
+ lines = [""] if lines.length == 0
6
+
7
+ lines
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ module RBlade
2
+ class Utility
3
+ def self.append_unprocessed_string_segment!(token, segments, string, offset = 0)
4
+ if segments.last&.type == :unprocessed
5
+ segments.last.value << string
6
+ segments.last.end_offset += string.length
7
+ else
8
+ start_offset = segments.last&.end_offset || token.start_offset
9
+ segments << Token.new(
10
+ type: :unprocessed,
11
+ value: string,
12
+ start_offset: start_offset,
13
+ end_offset: start_offset + string.length + offset,
14
+ )
15
+ end
16
+ end
17
+ end
18
+ end
@@ -5,7 +5,9 @@ require "rblade/component_store"
5
5
  require "rblade/helpers/attributes_manager"
6
6
  require "rblade/helpers/class_manager"
7
7
  require "rblade/helpers/slot_manager"
8
+ require "rblade/helpers/source_map"
8
9
  require "rblade/helpers/stack_manager"
10
+ require "rblade/helpers/string_utility"
9
11
  require "rblade/helpers/style_manager"
10
12
 
11
13
  module RBlade
@@ -32,7 +34,47 @@ module RBlade
32
34
  end
33
35
  end
34
36
 
35
- -"#{preamble}#{component_store.get}#{RBlade::Compiler.compile_string(source, component_store)}@output_buffer.raw_buffer.prepend(@_rblade_stack_manager.get(_stacks));@output_buffer;"
37
+ -"#{preamble}\n#{component_store.get}\n#{RBlade::Compiler.compile_string(source, component_store)}@output_buffer.raw_buffer.prepend(@_rblade_stack_manager.get(_stacks));@output_buffer;"
38
+ end
39
+
40
+ def translate_location(spot, backtrace_location, source)
41
+ view_name = backtrace_location.path
42
+ .sub(/^.*app\/views\/(.+?)(?:\.\w++)?\.rblade$/, "\\1")
43
+ .tr("/", ".")
44
+
45
+ # Let the component store know about the current view for relative components
46
+ component_store = RBlade::ComponentStore.new
47
+ component_store.view_name("view::#{view_name}")
48
+
49
+ source_map = RBlade::Compiler.generate_source_map(source, component_store)
50
+
51
+ # Account for the preamble and component store
52
+ offset = 2 + StringUtility.lines(component_store.get).length
53
+ offset += 1 if spot[:script_lines]&.first == "# frozen_string_literal: true\n"
54
+
55
+ location = source_map.source_location(spot[:first_lineno] - offset - 1, spot[:first_column])
56
+
57
+ before_lines = StringUtility.lines(source[0...(location[:start_offset])])
58
+ excerpt = source[(location[:start_offset])...(location[:end_offset])]
59
+ excerpt_lines = StringUtility.lines(excerpt)
60
+
61
+ first_lineno = before_lines.length
62
+ first_column = before_lines.last.length
63
+
64
+ last_lineno = first_lineno + excerpt_lines.length - 1
65
+ last_column = (excerpt_lines.length > 1) ? excerpt_lines.last.length : first_column + excerpt_lines.first.length
66
+
67
+ {
68
+ first_lineno: first_lineno,
69
+ first_column: first_column,
70
+ last_lineno: last_lineno,
71
+ last_column: last_column,
72
+ snippet: excerpt,
73
+ script_lines: source.lines,
74
+ }
75
+ rescue => e
76
+ Rails.logger&.debug "Unable to locate error position in template: #{e.message}"
77
+ spot
36
78
  end
37
79
  end
38
80
  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.1.0"
3
+ s.version = "3.2.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"]
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.1.0
4
+ version: 3.2.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-04-02 00:00:00.000000000 Z
11
+ date: 2025-06-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -156,8 +156,11 @@ files:
156
156
  - lib/rblade/helpers/class_manager.rb
157
157
  - lib/rblade/helpers/regular_expressions.rb
158
158
  - lib/rblade/helpers/slot_manager.rb
159
+ - lib/rblade/helpers/source_map.rb
159
160
  - lib/rblade/helpers/stack_manager.rb
161
+ - lib/rblade/helpers/string_utility.rb
160
162
  - lib/rblade/helpers/style_manager.rb
163
+ - lib/rblade/helpers/utility.rb
161
164
  - lib/rblade/rails_template.rb
162
165
  - lib/rblade/railtie.rb
163
166
  - rblade.gemspec