herb 0.8.10-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 (209) hide show
  1. checksums.yaml +4 -4
  2. data/Makefile +11 -3
  3. data/README.md +64 -34
  4. data/Rakefile +48 -40
  5. data/config.yml +317 -34
  6. data/ext/herb/error_helpers.c +367 -140
  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 +78 -11
  30. data/lib/herb/engine/debug_visitor.rb +13 -3
  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 +82 -35
  36. data/lib/herb/errors.rb +563 -88
  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 +2 -0
  57. data/sig/herb/engine/validator.rbs +5 -1
  58. data/sig/herb/engine.rbs +17 -3
  59. data/sig/herb/errors.rbs +258 -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 +54 -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} +48 -23
  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 +1076 -520
  97. data/src/extract.c +145 -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} +4 -2
  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 +148 -55
  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.h +6 -3
  146. data/src/include/version.h +1 -1
  147. data/src/io.c +3 -2
  148. data/src/lexer.c +42 -30
  149. data/src/lexer_peek_helpers.c +12 -74
  150. data/src/location.c +2 -2
  151. data/src/main.c +53 -28
  152. data/src/parser.c +783 -247
  153. data/src/parser_helpers.c +110 -23
  154. data/src/parser_match_tags.c +109 -48
  155. data/src/pretty_print.c +29 -24
  156. data/src/prism_helpers.c +30 -27
  157. data/src/ruby_parser.c +2 -0
  158. data/src/token.c +151 -66
  159. data/src/token_matchers.c +0 -1
  160. data/src/utf8.c +7 -6
  161. data/src/util/hb_allocator.c +341 -0
  162. data/src/util/hb_arena.c +81 -56
  163. data/src/util/hb_arena_debug.c +32 -17
  164. data/src/util/hb_array.c +30 -15
  165. data/src/util/hb_buffer.c +17 -21
  166. data/src/util/hb_narray.c +22 -7
  167. data/src/util/hb_string.c +49 -35
  168. data/src/util.c +21 -11
  169. data/src/visitor.c +47 -0
  170. data/templates/ext/herb/error_helpers.c.erb +24 -11
  171. data/templates/ext/herb/error_helpers.h.erb +1 -0
  172. data/templates/ext/herb/nodes.c.erb +50 -16
  173. data/templates/ext/herb/nodes.h.erb +1 -0
  174. data/templates/java/error_helpers.c.erb +1 -1
  175. data/templates/java/nodes.c.erb +30 -8
  176. data/templates/java/org/herb/ast/Errors.java.erb +24 -1
  177. data/templates/java/org/herb/ast/Nodes.java.erb +80 -21
  178. data/templates/javascript/packages/core/src/errors.ts.erb +16 -3
  179. data/templates/javascript/packages/core/src/node-type-guards.ts.erb +3 -1
  180. data/templates/javascript/packages/core/src/nodes.ts.erb +109 -32
  181. data/templates/javascript/packages/node/extension/error_helpers.cpp.erb +13 -4
  182. data/templates/javascript/packages/node/extension/nodes.cpp.erb +43 -4
  183. data/templates/lib/herb/ast/nodes.rb.erb +88 -31
  184. data/templates/lib/herb/errors.rb.erb +15 -3
  185. data/templates/lib/herb/visitor.rb.erb +2 -2
  186. data/templates/rust/src/ast/nodes.rs.erb +97 -44
  187. data/templates/rust/src/errors.rs.erb +2 -1
  188. data/templates/rust/src/nodes.rs.erb +167 -15
  189. data/templates/rust/src/union_types.rs.erb +60 -0
  190. data/templates/rust/src/visitor.rs.erb +81 -0
  191. data/templates/src/{analyze_missing_end.c.erb → analyze/missing_end.c.erb} +9 -6
  192. data/templates/src/{analyze_transform.c.erb → analyze/transform.c.erb} +2 -2
  193. data/templates/src/ast_nodes.c.erb +34 -26
  194. data/templates/src/ast_pretty_print.c.erb +24 -5
  195. data/templates/src/errors.c.erb +60 -54
  196. data/templates/src/include/ast_nodes.h.erb +6 -2
  197. data/templates/src/include/ast_pretty_print.h.erb +5 -0
  198. data/templates/src/include/errors.h.erb +15 -11
  199. data/templates/src/include/util/hb_foreach.h.erb +20 -0
  200. data/templates/src/parser_match_tags.c.erb +10 -4
  201. data/templates/src/visitor.c.erb +2 -2
  202. data/templates/template.rb +204 -29
  203. data/templates/wasm/error_helpers.cpp.erb +9 -5
  204. data/templates/wasm/nodes.cpp.erb +41 -4
  205. metadata +57 -16
  206. data/src/analyze.c +0 -1608
  207. data/src/element_source.c +0 -12
  208. data/src/include/util/hb_system.h +0 -9
  209. 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|
@@ -320,13 +366,22 @@ module Herb
320
366
  def process_erb_tag(node, skip_comment_check: false)
321
367
  opening = node.tag_opening.value
322
368
 
323
- 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
324
379
  return if erb_graphql?(opening)
325
380
 
326
381
  code = node.content.value.strip
327
382
 
328
383
  if erb_output?(opening)
329
- process_erb_output(opening, code)
384
+ process_erb_output(node, opening, code)
330
385
  else
331
386
  apply_trim(node, code)
332
387
  end
@@ -370,7 +425,7 @@ module Herb
370
425
  current_text = ""
371
426
  current_context = nil
372
427
 
373
- compacted.each do |type, value, context|
428
+ compacted.each do |type, value, context, escaped|
374
429
  if type == :text
375
430
  current_text += value
376
431
  current_context ||= context
@@ -382,7 +437,7 @@ module Herb
382
437
  current_context = nil
383
438
  end
384
439
 
385
- optimized << [type, value, context]
440
+ optimized << [type, value, context, escaped]
386
441
  end
387
442
  end
388
443
 
@@ -441,9 +496,11 @@ module Herb
441
496
  search_index >= 0 ? tokens[search_index] : nil
442
497
  end
443
498
 
444
- def process_erb_output(opening, code)
499
+ def process_erb_output(node, opening, code)
500
+ has_right_trim = node.tag_closing&.value == "-%>"
445
501
  should_escape = should_escape_output?(opening)
446
502
  add_expression_with_escaping(code, should_escape)
503
+ @trim_next_whitespace = true if has_right_trim
447
504
  end
448
505
 
449
506
  def should_escape_output?(opening)
@@ -464,10 +521,20 @@ module Herb
464
521
  @tokens.last[0] != :text ||
465
522
  @tokens.last[1].empty? ||
466
523
  @tokens.last[1].end_with?("\n") ||
467
- @tokens.last[1] =~ /\A[ \t]+\z/ ||
524
+ (@tokens.last[1] =~ /\A[ \t]+\z/ && preceding_token_ends_with_newline?) ||
468
525
  @tokens.last[1] =~ /\n[ \t]+\z/
469
526
  end
470
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
+
471
538
  def extract_lspace
472
539
  return "" unless @tokens.last && @tokens.last[0] == :text
473
540
 
@@ -501,7 +568,7 @@ module Herb
501
568
 
502
569
  if at_line_start?
503
570
  lspace = extract_and_remove_lspace!
504
- rspace = " \n"
571
+ rspace = Herb::Engine.heredoc?(code) ? "\n" : " \n"
505
572
 
506
573
  @tokens << [:code, "#{lspace}#{code}#{rspace}", current_context]
507
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
@@ -339,7 +339,9 @@ module Herb
339
339
  excluded_tags = ["script", "style", "head", "textarea", "pre", "svg", "math"]
340
340
  return true if excluded_tags.any? { |tag| @element_stack.include?(tag) }
341
341
 
342
- 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
343
345
 
344
346
  false
345
347
  end
@@ -390,6 +392,14 @@ module Herb
390
392
 
391
393
  false
392
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
393
403
  end
394
404
  end
395
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
 
@@ -3,14 +3,19 @@
3
3
  module Herb
4
4
  class Engine
5
5
  class Validator < Herb::Visitor
6
- attr_reader :diagnostics
6
+ attr_reader :diagnostics, :enabled
7
7
 
8
- def initialize
9
- super
8
+ def initialize(enabled: true)
9
+ super()
10
10
 
11
+ @enabled = enabled
11
12
  @diagnostics = []
12
13
  end
13
14
 
15
+ def enabled?
16
+ @enabled
17
+ end
18
+
14
19
  def validate(node)
15
20
  visit(node)
16
21
  end
@@ -31,7 +31,7 @@ module Herb
31
31
  end
32
32
 
33
33
  def validate_no_block_elements_in_paragraph(node)
34
- block_elements = %w[div section article header footer nav aside p h1 h2 h3 h4 h5 h6 ul ol dl table form]
34
+ block_elements = ["div", "section", "article", "header", "footer", "nav", "aside", "p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "dl", "table", "form"]
35
35
 
36
36
  node.body.each do |child|
37
37
  next unless child.is_a?(Herb::AST::HTMLElementNode)
@@ -58,7 +58,7 @@ module Herb
58
58
  end
59
59
 
60
60
  def validate_no_interactive_in_button(node)
61
- interactive_elements = %w[a button input select textarea]
61
+ interactive_elements = ["a", "button", "input", "select", "textarea"]
62
62
 
63
63
  node.body.each do |child|
64
64
  next unless child.is_a?(Herb::AST::HTMLElementNode)