Class: ThemeCheck::HtmlNode

Inherits:
Node
  • Object
show all
Extended by:
Forwardable, RegexHelpers
Includes:
PositionHelper, RegexHelpers
Defined in:
lib/theme_check/html_node.rb

Constant Summary

Constants included from RegexHelpers

RegexHelpers::HTML_LIQUID_PLACEHOLDER, RegexHelpers::LIQUID_TAG, RegexHelpers::LIQUID_TAG_OR_VARIABLE, RegexHelpers::LIQUID_VARIABLE, RegexHelpers::START_OR_END_QUOTE

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from RegexHelpers

matches

Methods included from PositionHelper

#bounded, #from_index_to_row_column, #from_row_column_to_index

Constructor Details

#initialize(value, theme_file, placeholder_values, parseable_source, parent = nil) ⇒ HtmlNode

Returns a new instance of HtmlNode.



72
73
74
75
76
77
78
# File 'lib/theme_check/html_node.rb', line 72

def initialize(value, theme_file, placeholder_values, parseable_source, parent = nil)
  @value = value
  @theme_file = theme_file
  @placeholder_values = placeholder_values
  @parseable_source = parseable_source
  @parent = parent
end

Instance Attribute Details

#parentObject (readonly)

Returns the value of attribute parent.



9
10
11
# File 'lib/theme_check/html_node.rb', line 9

def parent
  @parent
end

#theme_fileObject (readonly)

Returns the value of attribute theme_file.



9
10
11
# File 'lib/theme_check/html_node.rb', line 9

def theme_file
  @theme_file
end

Class Method Details

.parse(liquid_file) ⇒ Object



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/theme_check/html_node.rb', line 14

def parse(liquid_file)
  placeholder_values = []
  parseable_source = +liquid_file.source.clone

  # Replace all non-empty liquid tags with ≬{i}######≬ to prevent the HTML
  # parser from freaking out. We transparently replace those placeholders in
  # HtmlNode.
  #
  # We're using base36 to prevent index bleeding on 36^3 tags.
  # `{{x}}` -> `≬#{i}≬` would properly be transformed for 46656 tags in a single file.
  # Should be enough.
  #
  # The base10 alternative would have overflowed at 1000 (`{{x}}` -> `≬1000≬`) which seemed more likely.
  #
  # Didn't go with base64 because of the `=` character that would have messed with HTML parsing.
  #
  # (Note, we're also maintaining newline characters in there so
  # that line numbers match the source...)
  matches(parseable_source, LIQUID_TAG_OR_VARIABLE).each do |m|
    value = m[0]
    next unless value.size > 4 # skip empty tags/variables {%%} and {{}}
    placeholder_values.push(value)
    key = (placeholder_values.size - 1).to_s(36)

    # Doing shenanigans so that line numbers match... Ugh.
    keyed_placeholder = parseable_source[m.begin(0)...m.end(0)]

    # First and last chars are ≬
    keyed_placeholder[0] = ""
    keyed_placeholder[-1] = ""

    # Non newline characters are #
    keyed_placeholder.gsub!(/[^\n≬]/, '#')

    # First few # are replaced by the base10 ID of the tag
    i = -1
    keyed_placeholder.gsub!('#') do
      i += 1
      if i > key.size - 1
        '#'
      else
        key[i]
      end
    end

    # Replace source by placeholder
    parseable_source[m.begin(0)...m.end(0)] = keyed_placeholder
  end

  new(
    Nokogiri::HTML5.fragment(parseable_source, max_tree_depth: 400, max_attributes: 400),
    liquid_file,
    placeholder_values,
    parseable_source
  )
end

Instance Method Details

#attributesObject



136
137
138
139
140
# File 'lib/theme_check/html_node.rb', line 136

def attributes
  @attributes ||= @value.attributes
    .map { |k, v| [replace_placeholders(k), replace_placeholders(v.value)] }
    .to_h
end

#childrenObject



90
91
92
93
94
# File 'lib/theme_check/html_node.rb', line 90

def children
  @children ||= @value
    .children
    .map { |child| HtmlNode.new(child, theme_file, @placeholder_values, @parseable_source, self) }
end

#contentObject



178
179
180
# File 'lib/theme_check/html_node.rb', line 178

def content
  @content ||= replace_placeholders(@value.content)
end

#element?Boolean

Returns:

  • (Boolean)


132
133
134
# File 'lib/theme_check/html_node.rb', line 132

def element?
  @value.element?
end

#end_columnObject



124
125
126
# File 'lib/theme_check/html_node.rb', line 124

def end_column
  position.end_column
end

#end_indexObject



108
109
110
# File 'lib/theme_check/html_node.rb', line 108

def end_index
  position.end_index
end

#end_rowObject



120
121
122
# File 'lib/theme_check/html_node.rb', line 120

def end_row
  position.end_row
end

#line_numberObject



100
101
102
# File 'lib/theme_check/html_node.rb', line 100

def line_number
  @value.line
end

#literal?Boolean

Returns:

  • (Boolean)


128
129
130
# File 'lib/theme_check/html_node.rb', line 128

def literal?
  @value.name == "text"
end

#markupObject



96
97
98
# File 'lib/theme_check/html_node.rb', line 96

def markup
  @markup ||= replace_placeholders(parseable_markup)
end

#nameObject



182
183
184
185
186
187
188
# File 'lib/theme_check/html_node.rb', line 182

def name
  if @value.name == "#document-fragment"
    "document"
  else
    @value.name
  end
end

#parseable_markupObject



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/theme_check/html_node.rb', line 142

def parseable_markup
  return @parseable_source if @value.name == "#document-fragment"
  return @value.to_str if @value.comment?
  return @value.content if literal?

  start_index = from_row_column_to_index(@parseable_source, line_number - 1, 0)
  @parseable_source
    .match(/<\s*#{name}[^>]*>/im, start_index)[0]
rescue NoMethodError
  # Don't know what's up with the following issue. Don't think
  # null check is correct approach. This should give us more info.
  # https://github.com/Shopify/theme-check/issues/528
  ThemeCheck.bug(<<~MSG)
    Can't find a parseable tag of name #{name} inside the parseable HTML.

    Tag name:
      #{@value.name.inspect}

    File:
      #{@theme_file.relative_path}

    Line number:
      #{line_number}

    Excerpt:
      ```
      #{@theme_file.source.lines[line_number - 1...line_number + 5].join("")}
      ```

    Parseable Excerpt:
      ```
      #{@parseable_source.lines[line_number - 1...line_number + 5].join("")}
      ```
  MSG
end

#start_columnObject



116
117
118
# File 'lib/theme_check/html_node.rb', line 116

def start_column
  position.start_column
end

#start_indexObject



104
105
106
# File 'lib/theme_check/html_node.rb', line 104

def start_index
  position.start_index
end

#start_rowObject



112
113
114
# File 'lib/theme_check/html_node.rb', line 112

def start_row
  position.start_row
end

#valueObject

placeholders for the HtmlNode to make sense.



82
83
84
85
86
87
88
# File 'lib/theme_check/html_node.rb', line 82

def value
  if literal?
    content
  else
    markup
  end
end