Class: Herb::Engine::DebugVisitor

Inherits:
Visitor
  • Object
show all
Defined in:
lib/herb/engine/debug_visitor.rb,
sig/herb/engine/debug_visitor.rbs

Instance Method Summary collapse

Methods inherited from Visitor

#visit, #visit_all, #visit_cdata_node, #visit_child_nodes, #visit_erb_begin_node, #visit_erb_case_match_node, #visit_erb_case_node, #visit_erb_else_node, #visit_erb_end_node, #visit_erb_ensure_node, #visit_erb_for_node, #visit_erb_if_node, #visit_erb_in_node, #visit_erb_node, #visit_erb_open_tag_node, #visit_erb_render_node, #visit_erb_rescue_node, #visit_erb_strict_locals_node, #visit_erb_unless_node, #visit_erb_until_node, #visit_erb_when_node, #visit_erb_while_node, #visit_html_attribute_name_node, #visit_html_attribute_value_node, #visit_html_close_tag_node, #visit_html_conditional_element_node, #visit_html_conditional_open_tag_node, #visit_html_omitted_close_tag_node, #visit_html_open_tag_node, #visit_html_text_node, #visit_html_virtual_close_tag_node, #visit_literal_node, #visit_node, #visit_ruby_html_attributes_splat_node, #visit_ruby_literal_node, #visit_ruby_parameter_node, #visit_ruby_render_keywords_node, #visit_ruby_render_local_node, #visit_whitespace_node, #visit_xml_declaration_node

Methods included from AST::Helpers

#erb_comment?, #erb_graphql?, #erb_outputs?, #inline_ruby_comment?

Constructor Details

#initialize(file_path: nil, project_path: nil) ⇒ DebugVisitor

Returns a new instance of DebugVisitor.

Parameters:

  • file_path: (Object) (defaults to: nil)
  • project_path: (Object) (defaults to: nil)


7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/herb/engine/debug_visitor.rb', line 7

def initialize(file_path: nil, project_path: nil)
  super()

  @filename = case file_path
              when ::Pathname
                file_path
              when String
                file_path.empty? ? nil : ::Pathname.new(file_path)
              end

  @project_path = case project_path
                  when ::Pathname
                    project_path
                  when String
                    ::Pathname.new(project_path)
                  else
                    ::Pathname.new(Dir.pwd)
                  end

  @relative_file_path = calculate_relative_path
  @top_level_elements = [] #: Array[Herb::AST::HTMLElementNode]
  @element_stack = [] #: Array[String]
  @erb_block_stack = [] #: Array[Herb::AST::ERBBlockNode]
  @debug_attributes_applied = false
  @in_attribute = false
  @in_html_comment = false
  @in_html_doctype = false
  @erb_nodes_to_wrap = [] #: Array[Herb::AST::ERBContentNode]
  @top_level_elements = [] #: Array[Herb::AST::HTMLElementNode]
end

Instance Method Details

#add_debug_attributes_to_element(open_tag_node) ⇒ Object

Parameters:

  • open_tag_node (Object)

Returns:

  • (Object)


174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/herb/engine/debug_visitor.rb', line 174

def add_debug_attributes_to_element(open_tag_node)
  return if @debug_attributes_applied

  view_type = determine_view_type

  debug_attributes = [
    create_debug_attribute("data-herb-debug-outline-type", view_type),
    create_debug_attribute("data-herb-debug-file-name", component_display_name),
    create_debug_attribute("data-herb-debug-file-relative-path", @relative_file_path || "unknown"),
    create_debug_attribute("data-herb-debug-file-full-path", @filename&.to_s || "unknown")
  ]

  if @top_level_elements.length > 1
    debug_attributes << create_debug_attribute("data-herb-debug-attach-to-parent", "true")
  end

  open_tag_node.children.concat(debug_attributes)

  @debug_attributes_applied = true
end

#calculate_relative_pathObject

Returns:

  • (Object)


97
98
99
100
101
102
103
104
105
106
107
# File 'lib/herb/engine/debug_visitor.rb', line 97

def calculate_relative_path
  return "unknown" unless @filename

  if @filename.absolute?
    @filename.relative_path_from(@project_path).to_s
  else
    @filename.to_s
  end
rescue ArgumentError
  @filename.to_s
end

#classify(name) ⇒ Object

Parameters:

  • name (Object)

Returns:

  • (Object)


322
323
324
325
326
327
328
# File 'lib/herb/engine/debug_visitor.rb', line 322

def classify(name)
  if name.respond_to?(:camelize)
    name.camelize
  else
    name.split(/[_-]/).map(&:capitalize).join
  end
end

#complex_rails_helper?(code) ⇒ Object

TODO: Rewrite using Prism Nodes once available

Parameters:

  • code (Object)

Returns:

  • (Object)


354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/herb/engine/debug_visitor.rb', line 354

def complex_rails_helper?(code)
  cleaned_code = code.strip.gsub(/\s+/, " ")

  return true if cleaned_code.match?(/\bturbo_frame_tag\s*[(\s]/)

  return true if cleaned_code.match?(/\blink_to\s.*\s+do\s*$/) ||
                 cleaned_code.match?(/\blink_to\s.*\{\s*$/) ||
                 cleaned_code.match?(/\blink_to\s.*\s+do\s*\|/) ||
                 cleaned_code.match?(/\blink_to\s.*\{\s*\|/)

  return true if cleaned_code.match?(/\brender[\s(]/)

  return true if cleaned_code.match?(/\bform_with\s.*\s+do\s*[|$]/) ||
                 cleaned_code.match?(/\bform_with\s.*\{\s*[|$]/)

  return true if cleaned_code.match?(/\bcontent_for\s.*\s+do\s*$/) ||
                 cleaned_code.match?(/\bcontent_for\s.*\{\s*$/)

  return true if cleaned_code.match?(/\bcontent_tag\s.*\s+do\s*$/) ||
                 cleaned_code.match?(/\bcontent_tag\s.*\{\s*$/)

  return true if cleaned_code.match?(/\bcontent_tag\(.*\s+do\s*$/) ||
                 cleaned_code.match?(/\bcontent_tag\(.*\{\s*$/)

  return true if cleaned_code.match?(/\btag\.\w+\s.*do\s*$/) ||
                 cleaned_code.match?(/\btag\.\w+\s.*\{\s*$/)

  false
end

#component?Boolean

Returns:

  • (Boolean)


288
289
290
291
292
# File 'lib/herb/engine/debug_visitor.rb', line 288

def component?
  return false unless @filename

  @filename.to_s.match?(%r{(^|/)app/components/})
end

#component_display_nameObject

Returns:

  • (Object)


301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
# File 'lib/herb/engine/debug_visitor.rb', line 301

def component_display_name
  return @filename&.basename&.to_s || "unknown" unless @filename

  basename = @filename.basename.to_s
  path = @filename.to_s

  if sidecar_component? && (match = path.match(%r{/components/(.+)/component\.[^/]+\z}))
    return match[1].split("/").map { |s| classify(s) }.join("::")
  end

  if component?
    path_without_ext = path.sub(/\.(?:html\.erb|html\.herb|erb|herb)\z/, "")

    if (match = path_without_ext.match(%r{/components/(.+)\z}))
      return match[1].split("/").map { |s| classify(s) }.join("::")
    end
  end

  basename
end

#create_debug_attribute(name, value) ⇒ Object

Parameters:

  • name (Object)
  • value (Object)

Returns:

  • (Object)


195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/herb/engine/debug_visitor.rb', line 195

def create_debug_attribute(name, value)
  name_literal = Herb::AST::LiteralNode.new("LiteralNode", dummy_location, [], name.dup)
  name_node = Herb::AST::HTMLAttributeNameNode.new("HTMLAttributeNameNode", dummy_location, [], [name_literal])

  value_literal = Herb::AST::LiteralNode.new("LiteralNode", dummy_location, [], value.dup)
  value_node = Herb::AST::HTMLAttributeValueNode.new("HTMLAttributeValueNode", dummy_location, [], create_token(:quote, '"'),
                                                     [value_literal], create_token(:quote, '"'), true)

  equals_token = create_token(:equals, "=")

  Herb::AST::HTMLAttributeNode.new("HTMLAttributeNode", dummy_location, [], name_node, equals_token, value_node)
end

#create_debug_span_for_erb(erb_node) ⇒ Object

Parameters:

  • erb_node (Object)

Returns:

  • (Object)


212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/herb/engine/debug_visitor.rb', line 212

def create_debug_span_for_erb(erb_node)
  opening = erb_node.tag_opening.value
  code = erb_node.content.value.strip
  erb_code = "#{opening} #{code} %>"

  return erb_node if complex_rails_helper?(code)

  line = erb_node.location&.start&.line
  column = erb_node.location&.start&.column

  escaped_erb = erb_code.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;").gsub("'",
                                                                                                         "&#39;")

  outline_type = if @top_level_elements.empty?
                   "erb-output #{determine_view_type}"
                 else
                   "erb-output"
                 end

  debug_attributes = [
    create_debug_attribute("data-herb-debug-outline-type", outline_type),
    create_debug_attribute("data-herb-debug-erb", escaped_erb),
    create_debug_attribute("data-herb-debug-file-name", component_display_name),
    create_debug_attribute("data-herb-debug-file-relative-path", @relative_file_path || "unknown"),
    create_debug_attribute("data-herb-debug-file-full-path", @filename&.to_s || "unknown"),
    create_debug_attribute("data-herb-debug-inserted", "true")
  ]

  debug_attributes << create_debug_attribute("data-herb-debug-line", line.to_s) if line
  debug_attributes << create_debug_attribute("data-herb-debug-column", (column + 1).to_s) if column
  debug_attributes << create_debug_attribute("style", "display: contents;")

  tag_name_token = create_token(:tag_name, "span")

  open_tag = Herb::AST::HTMLOpenTagNode.new(
    "HTMLOpenTagNode",
    dummy_location,
    [],
    create_token(:tag_opening, "<"),
    tag_name_token,
    create_token(:tag_closing, ">"),
    debug_attributes,
    false
  )

  close_tag = Herb::AST::HTMLCloseTagNode.new(
    "HTMLCloseTagNode",
    dummy_location,
    [],
    create_token(:tag_opening, "</"),
    create_token(:tag_name, "span"),
    [],
    create_token(:tag_closing, ">")
  )

  Herb::AST::HTMLElementNode.new("HTMLElementNode", dummy_location, [], open_tag, tag_name_token, [erb_node], close_tag,
                                 false, "Debug")
end

#create_token(type, value) ⇒ Object

Parameters:

  • type (Object)
  • value (Object)

Returns:

  • (Object)


208
209
210
# File 'lib/herb/engine/debug_visitor.rb', line 208

def create_token(type, value)
  Herb::Token.new(value.dup, dummy_range, dummy_location, type.to_s)
end

#determine_view_typeObject

Returns:

  • (Object)


271
272
273
274
275
276
277
278
279
# File 'lib/herb/engine/debug_visitor.rb', line 271

def determine_view_type
  if component?
    "component"
  elsif partial?
    "partial"
  else
    "view"
  end
end

#dummy_locationHerb::Location

Creates a dummy location for AST nodes that don't need real location info : () -> Herb::Location

Returns:



115
116
117
# File 'lib/herb/engine/debug_visitor.rb', line 115

def dummy_location
  @dummy_location ||= Herb::Location.from(0, 0, 0, 0)
end

#dummy_rangeHerb::Range

Creates a dummy range for tokens that don't need real range info : () -> Herb::Range

Returns:



121
122
123
# File 'lib/herb/engine/debug_visitor.rb', line 121

def dummy_range
  @dummy_range ||= Herb::Range.from(0, 0)
end

#erb_output?(opening) ⇒ Boolean

Parameters:

  • opening (Object)

Returns:

  • (Boolean)


349
350
351
# File 'lib/herb/engine/debug_visitor.rb', line 349

def erb_output?(opening)
  opening.include?("=") && !opening.include?("#")
end

#find_parent_element_for_open_tag(open_tag_node) ⇒ Object

Parameters:

  • open_tag_node (Object)

Returns:

  • (Object)


170
171
172
# File 'lib/herb/engine/debug_visitor.rb', line 170

def find_parent_element_for_open_tag(open_tag_node)
  @top_level_elements.find { |element| element.open_tag == open_tag_node }
end

#find_top_level_elements(document_node) ⇒ Object

Parameters:

  • document_node (Object)

Returns:

  • (Object)


153
154
155
156
157
# File 'lib/herb/engine/debug_visitor.rb', line 153

def find_top_level_elements(document_node)
  document_node.children.each do |child|
    @top_level_elements << child if child.is_a?(Herb::AST::HTMLElementNode)
  end
end

#in_excluded_context?Boolean

Returns:

  • (Boolean)


338
339
340
341
342
343
344
345
346
347
# File 'lib/herb/engine/debug_visitor.rb', line 338

def in_excluded_context?
  excluded_tags = ["script", "style", "head", "textarea", "pre", "svg", "math"]
  return true if excluded_tags.any? { |tag| @element_stack.include?(tag) }

  if @erb_block_stack.any? { |node| javascript_tag?(node.content.value.strip) || include_debug_disable_comment?(node.content.value.strip) }
    return true
  end

  false
end

#in_head_context?Boolean

Returns:

  • (Boolean)


330
331
332
# File 'lib/herb/engine/debug_visitor.rb', line 330

def in_head_context?
  @element_stack.include?("head")
end

#in_script_or_style_context?Boolean

Returns:

  • (Boolean)


334
335
336
# File 'lib/herb/engine/debug_visitor.rb', line 334

def in_script_or_style_context?
  ["script", "style"].include?(@element_stack.last)
end

#include_debug_disable_comment?(code) ⇒ Boolean

Parameters:

  • code (Object)

Returns:

  • (Boolean)


396
397
398
399
400
401
402
# File 'lib/herb/engine/debug_visitor.rb', line 396

def include_debug_disable_comment?(code)
  cleaned_code = code.strip.gsub(/\s+/, " ")

  return true if cleaned_code.match?(/#\s*herb:debug\sdisable\s*$/)

  false
end

#javascript_tag?(code) ⇒ Object

TODO: Rewrite using Prism Nodes once available

Parameters:

  • code (Object)

Returns:

  • (Object)


385
386
387
388
389
390
391
392
393
394
# File 'lib/herb/engine/debug_visitor.rb', line 385

def javascript_tag?(code)
  cleaned_code = code.strip.gsub(/\s+/, " ")

  return true if cleaned_code.match?(/\bjavascript_tag\s.*do\s*$/) ||
                 cleaned_code.match?(/\bjavascript_tag\s.*\{\s*$/) ||
                 cleaned_code.match?(/\bjavascript_tag\(.*do\s*$/) ||
                 cleaned_code.match?(/\bjavascript_tag\(.*\{\s*$/)

  false
end

#partial?Boolean

Returns:

  • (Boolean)


281
282
283
284
285
286
# File 'lib/herb/engine/debug_visitor.rb', line 281

def partial?
  return false unless @filename

  basename = @filename.basename.to_s
  basename.start_with?("_")
end

#replace_erb_nodes_recursive(node) ⇒ Object

Parameters:

  • node (Object)

Returns:

  • (Object)


125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/herb/engine/debug_visitor.rb', line 125

def replace_erb_nodes_recursive(node)
  array_properties = [:children, :body, :statements]

  array_properties.each do |prop|
    next unless node.respond_to?(prop) && node.send(prop).is_a?(Array)

    array = node.send(prop)

    array.each_with_index do |child, index|
      if @erb_nodes_to_wrap.include?(child)
        debug_span = create_debug_span_for_erb(child)
        array[index] = debug_span
      else
        replace_erb_nodes_recursive(child)
      end
    end
  end

  node_properties = [:subsequent, :else_clause, :end_node, :rescue_clause, :ensure_clause]

  node_properties.each do |prop|
    if node.respond_to?(prop) && node.send(prop)
      child_node = node.send(prop)
      replace_erb_nodes_recursive(child_node)
    end
  end
end

#should_add_debug_attributes_to_element?(open_tag_node) ⇒ Boolean

Parameters:

  • open_tag_node (Object)

Returns:

  • (Boolean)


159
160
161
162
163
164
165
166
167
168
# File 'lib/herb/engine/debug_visitor.rb', line 159

def should_add_debug_attributes_to_element?(open_tag_node)
  return false if @debug_attributes_applied

  parent_element = find_parent_element_for_open_tag(open_tag_node)
  return false unless parent_element

  return @top_level_elements.first == parent_element if @top_level_elements.length >= 1

  false
end

#sidecar_component?Boolean

Returns:

  • (Boolean)


294
295
296
297
298
299
# File 'lib/herb/engine/debug_visitor.rb', line 294

def sidecar_component?
  return false unless component?
  return false unless @filename

  @filename.basename.to_s.match?(/\Acomponent\.(html\.erb|html\.herb|erb|herb)\z/)
end

#visit_document_node(node) ⇒ Object

Parameters:

  • node (Object)

Returns:

  • (Object)


38
39
40
41
42
43
44
# File 'lib/herb/engine/debug_visitor.rb', line 38

def visit_document_node(node)
  find_top_level_elements(node)

  super

  wrap_all_erb_nodes(node)
end

#visit_erb_block_node(node) ⇒ Object

Parameters:

  • node (Object)

Returns:

  • (Object)


89
90
91
92
93
# File 'lib/herb/engine/debug_visitor.rb', line 89

def visit_erb_block_node(node)
  @erb_block_stack.push(node)
  super
  @erb_block_stack.pop
end

#visit_erb_content_node(node) ⇒ Object

Parameters:

  • node (Object)

Returns:

  • (Object)


75
76
77
78
79
80
81
82
83
# File 'lib/herb/engine/debug_visitor.rb', line 75

def visit_erb_content_node(node)
  if !@in_attribute && !@in_html_comment && !@in_html_doctype && !in_excluded_context? && erb_output?(node.tag_opening.value)
    code = node.content.value.strip

    @erb_nodes_to_wrap << node unless complex_rails_helper?(code)
  end

  super
end

#visit_erb_yield_node(_node) ⇒ Object

Parameters:

  • _node (Object)

Returns:

  • (Object)


85
86
87
# File 'lib/herb/engine/debug_visitor.rb', line 85

def visit_erb_yield_node(_node)
  nil
end

#visit_html_attribute_node(node) ⇒ Object

Parameters:

  • node (Object)

Returns:

  • (Object)


57
58
59
60
61
# File 'lib/herb/engine/debug_visitor.rb', line 57

def visit_html_attribute_node(node)
  @in_attribute = true
  super
  @in_attribute = false
end

#visit_html_comment_node(node) ⇒ Object

Parameters:

  • node (Object)

Returns:

  • (Object)


63
64
65
66
67
# File 'lib/herb/engine/debug_visitor.rb', line 63

def visit_html_comment_node(node)
  @in_html_comment = true
  super
  @in_html_comment = false
end

#visit_html_doctype_node(node) ⇒ Object

Parameters:

  • node (Object)

Returns:

  • (Object)


69
70
71
72
73
# File 'lib/herb/engine/debug_visitor.rb', line 69

def visit_html_doctype_node(node)
  @in_html_doctype = true
  super
  @in_html_doctype = false
end

#visit_html_element_node(node) ⇒ Object

Parameters:

  • node (Object)

Returns:

  • (Object)


46
47
48
49
50
51
52
53
54
55
# File 'lib/herb/engine/debug_visitor.rb', line 46

def visit_html_element_node(node)
  tag_name = node.tag_name&.value&.downcase
  @element_stack.push(tag_name) if tag_name

  add_debug_attributes_to_element(node.open_tag) if should_add_debug_attributes_to_element?(node.open_tag)

  super

  @element_stack.pop if tag_name
end

#wrap_all_erb_nodes(node) ⇒ Object

Parameters:

  • node (Object)

Returns:

  • (Object)


109
110
111
# File 'lib/herb/engine/debug_visitor.rb', line 109

def wrap_all_erb_nodes(node)
  replace_erb_nodes_recursive(node)
end