herb 0.8.9-arm-linux-gnu → 0.9.0-arm-linux-gnu

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.
Files changed (221) hide show
  1. checksums.yaml +4 -4
  2. data/Makefile +33 -11
  3. data/README.md +64 -34
  4. data/Rakefile +48 -40
  5. data/config.yml +323 -33
  6. data/ext/herb/error_helpers.c +384 -132
  7. data/ext/herb/error_helpers.h +1 -0
  8. data/ext/herb/extconf.rb +67 -28
  9. data/ext/herb/extension.c +317 -51
  10. data/ext/herb/extension.h +1 -0
  11. data/ext/herb/extension_helpers.c +23 -14
  12. data/ext/herb/extension_helpers.h +2 -2
  13. data/ext/herb/nodes.c +537 -270
  14. data/ext/herb/nodes.h +1 -0
  15. data/herb.gemspec +3 -2
  16. data/lib/herb/3.0/herb.so +0 -0
  17. data/lib/herb/3.1/herb.so +0 -0
  18. data/lib/herb/3.2/herb.so +0 -0
  19. data/lib/herb/3.3/herb.so +0 -0
  20. data/lib/herb/3.4/herb.so +0 -0
  21. data/lib/herb/4.0/herb.so +0 -0
  22. data/lib/herb/ast/helpers.rb +3 -3
  23. data/lib/herb/ast/node.rb +15 -2
  24. data/lib/herb/ast/nodes.rb +1132 -157
  25. data/lib/herb/bootstrap.rb +87 -0
  26. data/lib/herb/cli.rb +341 -31
  27. data/lib/herb/configuration.rb +248 -0
  28. data/lib/herb/defaults.yml +32 -0
  29. data/lib/herb/engine/compiler.rb +83 -14
  30. data/lib/herb/engine/debug_visitor.rb +51 -6
  31. data/lib/herb/engine/error_formatter.rb +13 -9
  32. data/lib/herb/engine/parser_error_overlay.rb +10 -6
  33. data/lib/herb/engine/validator.rb +8 -3
  34. data/lib/herb/engine/validators/nesting_validator.rb +2 -2
  35. data/lib/herb/engine.rb +92 -33
  36. data/lib/herb/errors.rb +582 -87
  37. data/lib/herb/lex_result.rb +1 -0
  38. data/lib/herb/location.rb +7 -3
  39. data/lib/herb/parse_result.rb +12 -2
  40. data/lib/herb/parser_options.rb +57 -0
  41. data/lib/herb/position.rb +1 -0
  42. data/lib/herb/prism_inspect.rb +116 -0
  43. data/lib/herb/project.rb +923 -331
  44. data/lib/herb/range.rb +1 -0
  45. data/lib/herb/token.rb +7 -1
  46. data/lib/herb/version.rb +1 -1
  47. data/lib/herb/visitor.rb +37 -2
  48. data/lib/herb/warnings.rb +6 -1
  49. data/lib/herb.rb +35 -3
  50. data/sig/herb/ast/helpers.rbs +2 -2
  51. data/sig/herb/ast/node.rbs +12 -2
  52. data/sig/herb/ast/nodes.rbs +641 -128
  53. data/sig/herb/bootstrap.rbs +31 -0
  54. data/sig/herb/configuration.rbs +89 -0
  55. data/sig/herb/engine/compiler.rbs +9 -1
  56. data/sig/herb/engine/debug_visitor.rbs +8 -0
  57. data/sig/herb/engine/validator.rbs +5 -1
  58. data/sig/herb/engine.rbs +18 -2
  59. data/sig/herb/errors.rbs +268 -63
  60. data/sig/herb/location.rbs +4 -0
  61. data/sig/herb/parse_result.rbs +4 -2
  62. data/sig/herb/parser_options.rbs +42 -0
  63. data/sig/herb/position.rbs +1 -0
  64. data/sig/herb/prism_inspect.rbs +28 -0
  65. data/sig/herb/range.rbs +1 -0
  66. data/sig/herb/token.rbs +6 -0
  67. data/sig/herb/visitor.rbs +25 -4
  68. data/sig/herb/warnings.rbs +6 -1
  69. data/sig/herb.rbs +14 -0
  70. data/sig/herb_c_extension.rbs +5 -2
  71. data/sig/serialized_ast_errors.rbs +57 -6
  72. data/sig/serialized_ast_nodes.rbs +60 -6
  73. data/src/analyze/action_view/attribute_extraction_helpers.c +290 -0
  74. data/src/analyze/action_view/content_tag.c +70 -0
  75. data/src/analyze/action_view/link_to.c +143 -0
  76. data/src/analyze/action_view/registry.c +60 -0
  77. data/src/analyze/action_view/tag.c +64 -0
  78. data/src/analyze/action_view/tag_helper_node_builders.c +305 -0
  79. data/src/analyze/action_view/tag_helpers.c +748 -0
  80. data/src/analyze/action_view/turbo_frame_tag.c +88 -0
  81. data/src/analyze/analyze.c +882 -0
  82. data/src/{analyzed_ruby.c → analyze/analyzed_ruby.c} +13 -11
  83. data/src/analyze/builders.c +343 -0
  84. data/src/analyze/conditional_elements.c +594 -0
  85. data/src/analyze/conditional_open_tags.c +640 -0
  86. data/src/analyze/control_type.c +250 -0
  87. data/src/{analyze_helpers.c → analyze/helpers.c} +79 -31
  88. data/src/analyze/invalid_structures.c +193 -0
  89. data/src/{analyze_missing_end.c → analyze/missing_end.c} +33 -22
  90. data/src/analyze/parse_errors.c +84 -0
  91. data/src/analyze/prism_annotate.c +397 -0
  92. data/src/{analyze_transform.c → analyze/transform.c} +17 -3
  93. data/src/ast_node.c +17 -7
  94. data/src/ast_nodes.c +662 -387
  95. data/src/ast_pretty_print.c +190 -6
  96. data/src/errors.c +1099 -506
  97. data/src/extract.c +148 -49
  98. data/src/herb.c +52 -34
  99. data/src/html_util.c +241 -12
  100. data/src/include/analyze/action_view/attribute_extraction_helpers.h +36 -0
  101. data/src/include/analyze/action_view/tag_helper_handler.h +41 -0
  102. data/src/include/analyze/action_view/tag_helper_node_builders.h +70 -0
  103. data/src/include/analyze/action_view/tag_helpers.h +38 -0
  104. data/src/include/{analyze.h → analyze/analyze.h} +14 -4
  105. data/src/include/{analyzed_ruby.h → analyze/analyzed_ruby.h} +3 -3
  106. data/src/include/analyze/builders.h +27 -0
  107. data/src/include/analyze/conditional_elements.h +9 -0
  108. data/src/include/analyze/conditional_open_tags.h +9 -0
  109. data/src/include/analyze/control_type.h +14 -0
  110. data/src/include/{analyze_helpers.h → analyze/helpers.h} +22 -17
  111. data/src/include/analyze/invalid_structures.h +11 -0
  112. data/src/include/analyze/prism_annotate.h +16 -0
  113. data/src/include/ast_node.h +11 -5
  114. data/src/include/ast_nodes.h +117 -38
  115. data/src/include/ast_pretty_print.h +5 -0
  116. data/src/include/element_source.h +3 -8
  117. data/src/include/errors.h +154 -53
  118. data/src/include/extract.h +21 -5
  119. data/src/include/herb.h +18 -6
  120. data/src/include/herb_prism_node.h +13 -0
  121. data/src/include/html_util.h +7 -2
  122. data/src/include/io.h +3 -1
  123. data/src/include/lex_helpers.h +29 -0
  124. data/src/include/lexer.h +1 -1
  125. data/src/include/lexer_peek_helpers.h +87 -13
  126. data/src/include/lexer_struct.h +2 -0
  127. data/src/include/location.h +2 -1
  128. data/src/include/parser.h +27 -2
  129. data/src/include/parser_helpers.h +19 -3
  130. data/src/include/pretty_print.h +10 -5
  131. data/src/include/prism_context.h +45 -0
  132. data/src/include/prism_helpers.h +10 -7
  133. data/src/include/prism_serialized.h +12 -0
  134. data/src/include/token.h +16 -4
  135. data/src/include/token_struct.h +10 -3
  136. data/src/include/utf8.h +2 -1
  137. data/src/include/util/hb_allocator.h +78 -0
  138. data/src/include/util/hb_arena.h +6 -1
  139. data/src/include/util/hb_arena_debug.h +12 -1
  140. data/src/include/util/hb_array.h +7 -3
  141. data/src/include/util/hb_buffer.h +6 -4
  142. data/src/include/util/hb_foreach.h +79 -0
  143. data/src/include/util/hb_narray.h +8 -4
  144. data/src/include/util/hb_string.h +56 -9
  145. data/src/include/util/string.h +11 -0
  146. data/src/include/util.h +6 -3
  147. data/src/include/version.h +1 -1
  148. data/src/io.c +3 -2
  149. data/src/lexer.c +42 -30
  150. data/src/lexer_peek_helpers.c +12 -74
  151. data/src/location.c +2 -2
  152. data/src/main.c +79 -66
  153. data/src/parser.c +784 -247
  154. data/src/parser_helpers.c +110 -23
  155. data/src/parser_match_tags.c +109 -48
  156. data/src/pretty_print.c +29 -24
  157. data/src/prism_helpers.c +30 -27
  158. data/src/ruby_parser.c +2 -0
  159. data/src/token.c +151 -66
  160. data/src/token_matchers.c +0 -1
  161. data/src/utf8.c +7 -6
  162. data/src/util/hb_allocator.c +341 -0
  163. data/src/util/hb_arena.c +81 -56
  164. data/src/util/hb_arena_debug.c +32 -17
  165. data/src/util/hb_array.c +30 -15
  166. data/src/util/hb_buffer.c +17 -21
  167. data/src/util/hb_narray.c +22 -7
  168. data/src/util/hb_string.c +49 -35
  169. data/src/util.c +21 -11
  170. data/src/visitor.c +47 -0
  171. data/templates/ext/herb/error_helpers.c.erb +24 -11
  172. data/templates/ext/herb/error_helpers.h.erb +1 -0
  173. data/templates/ext/herb/nodes.c.erb +50 -16
  174. data/templates/ext/herb/nodes.h.erb +1 -0
  175. data/templates/java/error_helpers.c.erb +1 -1
  176. data/templates/java/nodes.c.erb +30 -8
  177. data/templates/java/org/herb/ast/Errors.java.erb +24 -1
  178. data/templates/java/org/herb/ast/Nodes.java.erb +80 -21
  179. data/templates/javascript/packages/core/src/errors.ts.erb +16 -3
  180. data/templates/javascript/packages/core/src/node-type-guards.ts.erb +3 -1
  181. data/templates/javascript/packages/core/src/nodes.ts.erb +109 -32
  182. data/templates/javascript/packages/node/extension/error_helpers.cpp.erb +13 -4
  183. data/templates/javascript/packages/node/extension/nodes.cpp.erb +43 -4
  184. data/templates/lib/herb/ast/nodes.rb.erb +88 -31
  185. data/templates/lib/herb/errors.rb.erb +15 -3
  186. data/templates/lib/herb/visitor.rb.erb +2 -2
  187. data/templates/rust/src/ast/nodes.rs.erb +97 -44
  188. data/templates/rust/src/errors.rs.erb +2 -1
  189. data/templates/rust/src/nodes.rs.erb +167 -15
  190. data/templates/rust/src/union_types.rs.erb +60 -0
  191. data/templates/rust/src/visitor.rs.erb +81 -0
  192. data/templates/src/{analyze_missing_end.c.erb → analyze/missing_end.c.erb} +9 -6
  193. data/templates/src/{analyze_transform.c.erb → analyze/transform.c.erb} +2 -2
  194. data/templates/src/ast_nodes.c.erb +34 -26
  195. data/templates/src/ast_pretty_print.c.erb +24 -5
  196. data/templates/src/errors.c.erb +60 -54
  197. data/templates/src/include/ast_nodes.h.erb +6 -2
  198. data/templates/src/include/ast_pretty_print.h.erb +5 -0
  199. data/templates/src/include/errors.h.erb +15 -11
  200. data/templates/src/include/util/hb_foreach.h.erb +20 -0
  201. data/templates/src/parser_match_tags.c.erb +10 -4
  202. data/templates/src/visitor.c.erb +2 -2
  203. data/templates/template.rb +204 -29
  204. data/templates/wasm/error_helpers.cpp.erb +9 -5
  205. data/templates/wasm/nodes.cpp.erb +41 -4
  206. data/vendor/prism/config.yml +4 -4
  207. data/vendor/prism/include/prism/ast.h +4 -4
  208. data/vendor/prism/include/prism/version.h +2 -2
  209. data/vendor/prism/src/prism.c +1 -1
  210. data/vendor/prism/templates/java/org/prism/Loader.java.erb +1 -1
  211. data/vendor/prism/templates/javascript/src/deserialize.js.erb +1 -1
  212. data/vendor/prism/templates/lib/prism/node.rb.erb +23 -15
  213. data/vendor/prism/templates/lib/prism/serialize.rb.erb +1 -1
  214. data/vendor/prism/templates/rbi/prism/node.rbi.erb +3 -0
  215. data/vendor/prism/templates/sig/prism/node.rbs.erb +3 -0
  216. data/vendor/prism/templates/sig/prism.rbs.erb +9 -10
  217. metadata +58 -16
  218. data/src/analyze.c +0 -1594
  219. data/src/element_source.c +0 -12
  220. data/src/include/util/hb_system.h +0 -9
  221. data/src/util/hb_system.c +0 -30
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "pathname"
5
+
6
+ module Herb
7
+ class Configuration
8
+ CONFIG_FILENAMES = [".herb.yml"].freeze
9
+
10
+ PROJECT_INDICATORS = [
11
+ ".git",
12
+ ".herb",
13
+ ".herb.yml",
14
+ "Gemfile",
15
+ "package.json",
16
+ "Rakefile",
17
+ "README.md",
18
+ "*.gemspec",
19
+ "config/application.rb"
20
+ ].freeze
21
+
22
+ DEFAULTS_PATH = File.expand_path("defaults.yml", __dir__ || __FILE__).freeze
23
+ DEFAULTS = YAML.safe_load_file(DEFAULTS_PATH).freeze
24
+
25
+ attr_reader :config, :config_path, :project_root
26
+
27
+ def initialize(project_path = nil)
28
+ @start_path = project_path ? Pathname.new(project_path) : Pathname.pwd
29
+ @config_path, @project_root = find_config_file
30
+ @config = load_config
31
+ end
32
+
33
+ def [](key)
34
+ @config[key.to_s]
35
+ end
36
+
37
+ def dig(*keys)
38
+ @config.dig(*keys.map(&:to_s))
39
+ end
40
+
41
+ def version
42
+ @config["version"]
43
+ end
44
+
45
+ def files
46
+ @config["files"] || {}
47
+ end
48
+
49
+ def file_include_patterns
50
+ files["include"] || DEFAULTS.dig("files", "include") || []
51
+ end
52
+
53
+ def file_exclude_patterns
54
+ files["exclude"] || DEFAULTS.dig("files", "exclude") || []
55
+ end
56
+
57
+ def linter
58
+ @config["linter"] || {}
59
+ end
60
+
61
+ def engine
62
+ @config["engine"] || {}
63
+ end
64
+
65
+ def enabled_validators(overrides = {})
66
+ config = dig("engine", "validators") || {}
67
+
68
+ {
69
+ security: config.fetch("security", true),
70
+ nesting: config.fetch("nesting", true),
71
+ accessibility: config.fetch("accessibility", true),
72
+ }.merge(
73
+ overrides.to_h { |key, value| [key.to_sym, !!value] }
74
+ )
75
+ end
76
+
77
+ def formatter
78
+ @config["formatter"] || {}
79
+ end
80
+
81
+ def include_patterns_for(tool)
82
+ tool_config = send(tool.to_s)
83
+ file_include_patterns + (tool_config["include"] || [])
84
+ end
85
+
86
+ def exclude_patterns_for(tool)
87
+ tool_config = send(tool.to_s)
88
+ file_exclude_patterns + (tool_config["exclude"] || [])
89
+ end
90
+
91
+ def linter_include_patterns
92
+ include_patterns_for(:linter)
93
+ end
94
+
95
+ def linter_exclude_patterns
96
+ exclude_patterns_for(:linter)
97
+ end
98
+
99
+ def formatter_include_patterns
100
+ include_patterns_for(:formatter)
101
+ end
102
+
103
+ def formatter_exclude_patterns
104
+ exclude_patterns_for(:formatter)
105
+ end
106
+
107
+ def enabled_for_path?(path, tool)
108
+ tool_config = send(tool.to_s)
109
+ tool_include = tool_config["include"] || []
110
+ tool_exclude = tool_config["exclude"] || []
111
+
112
+ if tool_include.any? && path_included?(path, tool_include)
113
+ return !path_excluded?(path, tool_exclude)
114
+ end
115
+
116
+ exclude_patterns = exclude_patterns_for(tool)
117
+
118
+ !path_excluded?(path, exclude_patterns)
119
+ end
120
+
121
+ def linter_enabled_for_path?(path)
122
+ enabled_for_path?(path, :linter)
123
+ end
124
+
125
+ def formatter_enabled_for_path?(path)
126
+ enabled_for_path?(path, :formatter)
127
+ end
128
+
129
+ def path_excluded?(path, patterns)
130
+ patterns.any? { |pattern| File.fnmatch?(pattern, path, File::FNM_PATHNAME) }
131
+ end
132
+
133
+ def path_included?(path, patterns)
134
+ patterns.any? { |pattern| File.fnmatch?(pattern, path, File::FNM_PATHNAME) }
135
+ end
136
+
137
+ def find_files(search_path = nil)
138
+ search_path ||= @project_root || @start_path
139
+ expanded_path = File.expand_path(search_path.to_s)
140
+
141
+ all_files = file_include_patterns.flat_map do |pattern|
142
+ Dir[File.join(expanded_path, pattern)]
143
+ end.uniq
144
+
145
+ all_files.reject do |file|
146
+ relative = file.sub("#{expanded_path}/", "")
147
+ path_excluded?(relative, file_exclude_patterns)
148
+ end.sort
149
+ end
150
+
151
+ def find_files_for_tool(tool, search_path = nil)
152
+ search_path ||= @project_root || @start_path
153
+ expanded_path = File.expand_path(search_path.to_s)
154
+
155
+ include_patterns = include_patterns_for(tool)
156
+ exclude_patterns = exclude_patterns_for(tool)
157
+
158
+ all_files = include_patterns.flat_map do |pattern|
159
+ Dir[File.join(expanded_path, pattern)]
160
+ end.uniq
161
+
162
+ all_files.reject do |file|
163
+ relative = file.sub("#{expanded_path}/", "")
164
+ path_excluded?(relative, exclude_patterns)
165
+ end.sort
166
+ end
167
+
168
+ def find_files_for_linter(search_path = nil)
169
+ find_files_for_tool(:linter, search_path)
170
+ end
171
+
172
+ def find_files_for_formatter(search_path = nil)
173
+ find_files_for_tool(:formatter, search_path)
174
+ end
175
+
176
+ class << self
177
+ def load(project_path = nil)
178
+ new(project_path)
179
+ end
180
+
181
+ def default
182
+ @default ||= new
183
+ end
184
+
185
+ def default_file_patterns
186
+ DEFAULTS.dig("files", "include") || []
187
+ end
188
+
189
+ def default_exclude_patterns
190
+ DEFAULTS.dig("files", "exclude") || []
191
+ end
192
+ end
193
+
194
+ private
195
+
196
+ def find_config_file
197
+ search_path = @start_path
198
+ search_path = search_path.parent if search_path.file?
199
+
200
+ while search_path.to_s != "/"
201
+ CONFIG_FILENAMES.each do |filename|
202
+ config_file = search_path / filename
203
+ return [config_file, search_path] if config_file.exist?
204
+ end
205
+
206
+ return [nil, search_path] if project_root?(search_path)
207
+
208
+ search_path = search_path.parent
209
+ end
210
+
211
+ [nil, @start_path]
212
+ end
213
+
214
+ def project_root?(path)
215
+ PROJECT_INDICATORS.any? do |indicator|
216
+ if indicator.include?("*")
217
+ Dir.glob(path / indicator).any?
218
+ else
219
+ (path / indicator).exist?
220
+ end
221
+ end
222
+ end
223
+
224
+ def load_config
225
+ return deep_merge(DEFAULTS, {}) unless @config_path&.exist?
226
+
227
+ begin
228
+ user_config = YAML.safe_load_file(@config_path, permitted_classes: [Symbol]) || {}
229
+ deep_merge(DEFAULTS, user_config)
230
+ rescue Psych::SyntaxError => e
231
+ warn "Warning: Invalid YAML in #{@config_path}: #{e.message}"
232
+ deep_merge(DEFAULTS, {})
233
+ end
234
+ end
235
+
236
+ def deep_merge(base, override, additive_keys: ["include", "exclude"])
237
+ base.merge(override) do |key, old_val, new_val|
238
+ if old_val.is_a?(Hash) && new_val.is_a?(Hash)
239
+ deep_merge(old_val, new_val, additive_keys: additive_keys)
240
+ elsif old_val.is_a?(Array) && new_val.is_a?(Array) && additive_keys.include?(key)
241
+ old_val + new_val
242
+ else
243
+ new_val
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,32 @@
1
+ files:
2
+ include:
3
+ - "**/*.herb"
4
+ - "**/*.html.erb"
5
+ - "**/*.html.herb"
6
+ - "**/*.html"
7
+ - "**/*.html+*.erb"
8
+ - "**/*.rhtml"
9
+ - "**/*.turbo_stream.erb"
10
+
11
+ exclude:
12
+ - "coverage/**/*"
13
+ - "log/**/*"
14
+ - "node_modules/**/*"
15
+ - "storage/**/*"
16
+ - "tmp/**/*"
17
+ - "vendor/**/*"
18
+
19
+ engine:
20
+ validators:
21
+ security: true
22
+ nesting: true
23
+ accessibility: true
24
+
25
+ linter:
26
+ enabled: true
27
+ rules: {}
28
+
29
+ formatter:
30
+ enabled: false
31
+ indentWidth: 2
32
+ maxLineLength: 80
@@ -22,7 +22,7 @@ module Herb
22
22
  def generate_output
23
23
  optimized_tokens = optimize_tokens(@tokens)
24
24
 
25
- optimized_tokens.each do |type, value, context|
25
+ optimized_tokens.each do |type, value, context, escaped|
26
26
  case type
27
27
  when :text
28
28
  @engine.send(:add_text, value)
@@ -48,6 +48,8 @@ module Herb
48
48
  when :expr_block_escaped
49
49
  indicator = @escape ? "=" : "=="
50
50
  @engine.send(:add_expression_block, indicator, value)
51
+ when :expr_block_end
52
+ @engine.send(:add_expression_block_end, value, escaped: escaped)
51
53
  end
52
54
  end
53
55
  end
@@ -71,7 +73,27 @@ module Herb
71
73
  visit_all(node.body)
72
74
  visit(node.close_tag)
73
75
 
74
- pop_context if %w[script style].include?(tag_name)
76
+ pop_context if ["script", "style"].include?(tag_name)
77
+
78
+ @element_stack.pop if tag_name
79
+ end
80
+
81
+ def visit_html_conditional_element_node(node)
82
+ tag_name = node.tag_name&.value&.downcase
83
+
84
+ @element_stack.push(tag_name) if tag_name
85
+
86
+ if tag_name == "script"
87
+ push_context(:script_content)
88
+ elsif tag_name == "style"
89
+ push_context(:style_content)
90
+ end
91
+
92
+ visit(node.open_conditional)
93
+ visit_all(node.body)
94
+ visit(node.close_conditional)
95
+
96
+ pop_context if ["script", "style"].include?(tag_name)
75
97
 
76
98
  @element_stack.pop if tag_name
77
99
  end
@@ -125,6 +147,10 @@ module Herb
125
147
  add_text(node.tag_closing&.value)
126
148
  end
127
149
 
150
+ def visit_html_omitted_close_tag_node(node)
151
+ # no-op
152
+ end
153
+
128
154
  def visit_html_text_node(node)
129
155
  add_text(node.content)
130
156
  end
@@ -168,7 +194,9 @@ module Herb
168
194
  end
169
195
 
170
196
  def visit_erb_control_node(node, &_block)
171
- apply_trim(node, node.content.value.strip)
197
+ if node.content
198
+ apply_trim(node, node.content.value.strip)
199
+ end
172
200
 
173
201
  yield if block_given?
174
202
  end
@@ -257,7 +285,7 @@ module Herb
257
285
  end
258
286
 
259
287
  visit_all(node.body)
260
- visit(node.end_node)
288
+ visit_erb_block_end_node(node.end_node, escaped: should_escape)
261
289
  else
262
290
  visit_erb_control_node(node) do
263
291
  visit_all(node.body)
@@ -266,6 +294,24 @@ module Herb
266
294
  end
267
295
  end
268
296
 
297
+ def visit_erb_block_end_node(node, escaped: false)
298
+ has_left_trim = node.tag_opening.value.start_with?("<%-")
299
+
300
+ remove_trailing_whitespace_from_last_token! if has_left_trim
301
+
302
+ code = node.content.value.strip
303
+
304
+ if at_line_start?
305
+ lspace = extract_and_remove_lspace!
306
+ rspace = " \n"
307
+
308
+ @tokens << [:expr_block_end, "#{lspace}#{code}#{rspace}", current_context, escaped]
309
+ @trim_next_whitespace = true
310
+ else
311
+ @tokens << [:expr_block_end, code, current_context, escaped]
312
+ end
313
+ end
314
+
269
315
  def visit_erb_control_with_parts(node, *parts)
270
316
  visit_erb_control_node(node) do
271
317
  parts.each do |part|
@@ -297,18 +343,20 @@ module Herb
297
343
  end
298
344
 
299
345
  def add_context_aware_expression(code, context)
346
+ closing = code.include?("#") ? "\n))" : "))"
347
+
300
348
  case context
301
349
  when :attribute_value
302
350
  @engine.send(:with_buffer) {
303
- @engine.src << " << #{@attrfunc}((" << code << "))"
351
+ @engine.src << " << #{@attrfunc}((" << code << closing
304
352
  }
305
353
  when :script_content
306
354
  @engine.send(:with_buffer) {
307
- @engine.src << " << #{@jsfunc}((" << code << "))"
355
+ @engine.src << " << #{@jsfunc}((" << code << closing
308
356
  }
309
357
  when :style_content
310
358
  @engine.send(:with_buffer) {
311
- @engine.src << " << #{@cssfunc}((" << code << "))"
359
+ @engine.src << " << #{@cssfunc}((" << code << closing
312
360
  }
313
361
  else
314
362
  @engine.send(:add_expression_result_escaped, code)
@@ -318,13 +366,22 @@ module Herb
318
366
  def process_erb_tag(node, skip_comment_check: false)
319
367
  opening = node.tag_opening.value
320
368
 
321
- return if !skip_comment_check && erb_comment?(opening)
369
+ if !skip_comment_check && erb_comment?(opening)
370
+ has_left_trim = opening.start_with?("<%-")
371
+ remove_trailing_whitespace_from_last_token! if has_left_trim
372
+
373
+ if at_line_start?
374
+ extract_and_remove_lspace!
375
+ @trim_next_whitespace = true
376
+ end
377
+ return
378
+ end
322
379
  return if erb_graphql?(opening)
323
380
 
324
381
  code = node.content.value.strip
325
382
 
326
383
  if erb_output?(opening)
327
- process_erb_output(opening, code)
384
+ process_erb_output(node, opening, code)
328
385
  else
329
386
  apply_trim(node, code)
330
387
  end
@@ -368,7 +425,7 @@ module Herb
368
425
  current_text = ""
369
426
  current_context = nil
370
427
 
371
- compacted.each do |type, value, context|
428
+ compacted.each do |type, value, context, escaped|
372
429
  if type == :text
373
430
  current_text += value
374
431
  current_context ||= context
@@ -380,7 +437,7 @@ module Herb
380
437
  current_context = nil
381
438
  end
382
439
 
383
- optimized << [type, value, context]
440
+ optimized << [type, value, context, escaped]
384
441
  end
385
442
  end
386
443
 
@@ -439,9 +496,11 @@ module Herb
439
496
  search_index >= 0 ? tokens[search_index] : nil
440
497
  end
441
498
 
442
- def process_erb_output(opening, code)
499
+ def process_erb_output(node, opening, code)
500
+ has_right_trim = node.tag_closing&.value == "-%>"
443
501
  should_escape = should_escape_output?(opening)
444
502
  add_expression_with_escaping(code, should_escape)
503
+ @trim_next_whitespace = true if has_right_trim
445
504
  end
446
505
 
447
506
  def should_escape_output?(opening)
@@ -462,10 +521,20 @@ module Herb
462
521
  @tokens.last[0] != :text ||
463
522
  @tokens.last[1].empty? ||
464
523
  @tokens.last[1].end_with?("\n") ||
465
- @tokens.last[1] =~ /\A[ \t]+\z/ ||
524
+ (@tokens.last[1] =~ /\A[ \t]+\z/ && preceding_token_ends_with_newline?) ||
466
525
  @tokens.last[1] =~ /\n[ \t]+\z/
467
526
  end
468
527
 
528
+ def preceding_token_ends_with_newline?
529
+ return true unless @tokens.length >= 2
530
+
531
+ preceding = @tokens[-2]
532
+ return false if [:expr, :expr_escaped, :expr_block, :expr_block_escaped, :expr_block_end].include?(preceding[0])
533
+ return true unless preceding[0] == :text
534
+
535
+ preceding[1].end_with?("\n")
536
+ end
537
+
469
538
  def extract_lspace
470
539
  return "" unless @tokens.last && @tokens.last[0] == :text
471
540
 
@@ -499,7 +568,7 @@ module Herb
499
568
 
500
569
  if at_line_start?
501
570
  lspace = extract_and_remove_lspace!
502
- rspace = " \n"
571
+ rspace = Herb::Engine.heredoc?(code) ? "\n" : " \n"
503
572
 
504
573
  @tokens << [:code, "#{lspace}#{code}#{rspace}", current_context]
505
574
  @trim_next_whitespace = true
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: false
2
3
 
3
4
  module Herb
4
5
  class Engine
@@ -31,6 +32,7 @@ module Herb
31
32
  @in_html_comment = false
32
33
  @in_html_doctype = false
33
34
  @erb_nodes_to_wrap = [] #: Array[Herb::AST::ERBContentNode]
35
+ @top_level_elements = [] #: Array[Herb::AST::HTMLElementNode]
34
36
  end
35
37
 
36
38
  def visit_document_node(node)
@@ -149,8 +151,6 @@ module Herb
149
151
  end
150
152
 
151
153
  def find_top_level_elements(document_node)
152
- @top_level_elements = [] #: Array[Herb::AST::HTMLElementNode]
153
-
154
154
  document_node.children.each do |child|
155
155
  @top_level_elements << child if child.is_a?(Herb::AST::HTMLElementNode)
156
156
  end
@@ -178,7 +178,7 @@ module Herb
178
178
 
179
179
  debug_attributes = [
180
180
  create_debug_attribute("data-herb-debug-outline-type", view_type),
181
- create_debug_attribute("data-herb-debug-file-name", @filename&.basename&.to_s || "unknown"),
181
+ create_debug_attribute("data-herb-debug-file-name", component_display_name),
182
182
  create_debug_attribute("data-herb-debug-file-relative-path", @relative_file_path || "unknown"),
183
183
  create_debug_attribute("data-herb-debug-file-full-path", @filename&.to_s || "unknown")
184
184
  ]
@@ -231,7 +231,7 @@ module Herb
231
231
  debug_attributes = [
232
232
  create_debug_attribute("data-herb-debug-outline-type", outline_type),
233
233
  create_debug_attribute("data-herb-debug-erb", escaped_erb),
234
- create_debug_attribute("data-herb-debug-file-name", @filename&.basename&.to_s || "unknown"),
234
+ create_debug_attribute("data-herb-debug-file-name", component_display_name),
235
235
  create_debug_attribute("data-herb-debug-file-relative-path", @relative_file_path || "unknown"),
236
236
  create_debug_attribute("data-herb-debug-file-full-path", @filename&.to_s || "unknown"),
237
237
  create_debug_attribute("data-herb-debug-inserted", "true")
@@ -288,8 +288,43 @@ module Herb
288
288
  def component?
289
289
  return false unless @filename
290
290
 
291
+ @filename.to_s.match?(%r{(^|/)app/components/})
292
+ end
293
+
294
+ def sidecar_component?
295
+ return false unless component?
296
+ return false unless @filename
297
+
298
+ @filename.basename.to_s.match?(/\Acomponent\.(html\.erb|html\.herb|erb|herb)\z/)
299
+ end
300
+
301
+ def component_display_name
302
+ return @filename&.basename&.to_s || "unknown" unless @filename
303
+
304
+ basename = @filename.basename.to_s
291
305
  path = @filename.to_s
292
- path.include?("/components/")
306
+
307
+ if sidecar_component? && (match = path.match(%r{/components/(.+)/component\.[^/]+\z}))
308
+ return match[1].split("/").map { |s| classify(s) }.join("::")
309
+ end
310
+
311
+ if component?
312
+ path_without_ext = path.sub(/\.(?:html\.erb|html\.herb|erb|herb)\z/, "")
313
+
314
+ if (match = path_without_ext.match(%r{/components/(.+)\z}))
315
+ return match[1].split("/").map { |s| classify(s) }.join("::")
316
+ end
317
+ end
318
+
319
+ basename
320
+ end
321
+
322
+ def classify(name)
323
+ if name.respond_to?(:camelize)
324
+ name.camelize
325
+ else
326
+ name.split(/[_-]/).map(&:capitalize).join
327
+ end
293
328
  end
294
329
 
295
330
  def in_head_context?
@@ -304,7 +339,9 @@ module Herb
304
339
  excluded_tags = ["script", "style", "head", "textarea", "pre", "svg", "math"]
305
340
  return true if excluded_tags.any? { |tag| @element_stack.include?(tag) }
306
341
 
307
- return true if @erb_block_stack.any? { |node| javascript_tag?(node.content.value.strip) }
342
+ if @erb_block_stack.any? { |node| javascript_tag?(node.content.value.strip) || include_debug_disable_comment?(node.content.value.strip) }
343
+ return true
344
+ end
308
345
 
309
346
  false
310
347
  end
@@ -355,6 +392,14 @@ module Herb
355
392
 
356
393
  false
357
394
  end
395
+
396
+ def include_debug_disable_comment?(code)
397
+ cleaned_code = code.strip.gsub(/\s+/, " ")
398
+
399
+ return true if cleaned_code.match?(/#\s*herb:debug\sdisable\s*$/)
400
+
401
+ false
402
+ end
358
403
  end
359
404
  end
360
405
  end
@@ -214,12 +214,9 @@ module Herb
214
214
  output << " Details: #{error.error_message}\n"
215
215
  output << " Suggestion: Check your Ruby syntax inside the ERB tag\n"
216
216
 
217
- when Herb::Errors::QuotesMismatchError
218
- if error.opening_quote && error.closing_quote
219
- output << " Opening quote: #{error.opening_quote.value}\n"
220
- output << " Closing quote: #{error.closing_quote.value}\n"
221
- output << " Suggestion: Use matching quotes for attribute values\n"
222
- end
217
+ when Herb::Errors::MissingAttributeValueError
218
+ output << " Attribute: #{error.attribute_name}\n"
219
+ output << " Suggestion: Add a value after the equals sign or remove the equals sign\n"
223
220
  end
224
221
 
225
222
  output
@@ -229,7 +226,8 @@ module Herb
229
226
  case error
230
227
  when Herb::Errors::MissingClosingTagError,
231
228
  Herb::Errors::TagNamesMismatchError,
232
- Herb::Errors::UnclosedElementError
229
+ Herb::Errors::UnclosedElementError,
230
+ Herb::Errors::MissingAttributeValueError
233
231
  true
234
232
  else
235
233
  false
@@ -244,6 +242,8 @@ module Herb
244
242
  "← Tag mismatch"
245
243
  when Herb::Errors::UnclosedElementError
246
244
  "← Unclosed element"
245
+ when Herb::Errors::MissingAttributeValueError
246
+ "← Missing attribute value"
247
247
  else
248
248
  ""
249
249
  end
@@ -419,8 +419,12 @@ module Herb
419
419
  end
420
420
  when Herb::Errors::RubyParseError
421
421
  "Check your Ruby syntax inside the ERB tag"
422
- when Herb::Errors::QuotesMismatchError
423
- "Use matching quotes for attribute values"
422
+ when Herb::Errors::MissingAttributeValueError
423
+ if error.attribute_name
424
+ "Add a value after the equals sign for '#{error.attribute_name}' or remove the equals sign"
425
+ else
426
+ "Add a value after the equals sign or remove the equals sign"
427
+ end
424
428
  end
425
429
  end
426
430
  end
@@ -9,12 +9,12 @@ module Herb
9
9
  Herb::Errors::UnexpectedTokenError,
10
10
  Herb::Errors::UnexpectedError,
11
11
  Herb::Errors::RubyParseError,
12
- Herb::Errors::QuotesMismatchError,
13
12
  Herb::Errors::TagNamesMismatchError,
14
13
  Herb::Errors::VoidElementClosingTagError,
15
14
  Herb::Errors::UnclosedElementError,
16
15
  Herb::Errors::MissingClosingTagError,
17
- Herb::Errors::MissingOpeningTagError
16
+ Herb::Errors::MissingOpeningTagError,
17
+ Herb::Errors::MissingAttributeValueError
18
18
  ].freeze
19
19
 
20
20
  def initialize(source, errors, filename: nil)
@@ -729,8 +729,12 @@ module Herb
729
729
  end
730
730
  when Herb::Errors::RubyParseError
731
731
  "Fix Ruby syntax: Check your Ruby syntax inside the ERB tag"
732
- when Herb::Errors::QuotesMismatchError
733
- "Fix quote mismatch: Use matching quotes for attribute values"
732
+ when Herb::Errors::MissingAttributeValueError
733
+ if error.respond_to?(:attribute_name) && error.attribute_name
734
+ "Add attribute value: Add a value after the equals sign for '#{error.attribute_name}' or remove the equals sign"
735
+ else
736
+ "Add attribute value: Add a value after the equals sign or remove the equals sign"
737
+ end
734
738
  else
735
739
  message = error.respond_to?(:message) ? error.message : error.to_s
736
740
  "Fix error: #{message}"
@@ -747,10 +751,10 @@ module Herb
747
751
  "← Unclosed element"
748
752
  when Herb::Errors::VoidElementClosingTagError
749
753
  "← Void element cannot be closed"
750
- when Herb::Errors::QuotesMismatchError
751
- "← Quote mismatch"
752
754
  when Herb::Errors::RubyParseError
753
755
  "← Ruby syntax error"
756
+ when Herb::Errors::MissingAttributeValueError
757
+ "← Missing attribute value"
754
758
  end
755
759
  end
756
760