lib/theme_check/liquid_node.rb in theme-check-1.8.0 vs lib/theme_check/liquid_node.rb in theme-check-1.9.0
- old
+ new
@@ -43,15 +43,51 @@
# The original source code of the node. Doesn't contain wrapping braces.
def markup
if tag?
tag_markup
+ elsif literal?
+ value.to_s
elsif @value.instance_variable_defined?(:@markup)
@value.instance_variable_get(:@markup)
end
end
+ # The original source code of the node. Does contain wrapping braces.
+ def outer_markup
+ if literal?
+ markup
+ elsif variable_lookup?
+ ''
+ elsif variable?
+ start_token + markup + end_token
+ elsif tag? && block?
+ start_index = block_start_start_index
+ end_index = block_start_end_index
+ end_index += inner_markup.size
+ end_index = find_block_delimiter(end_index)&.end(0)
+ source[start_index...end_index]
+ elsif tag?
+ source[block_start_start_index...block_start_end_index]
+ else
+ inner_markup
+ end
+ end
+
+ def inner_markup
+ return '' unless block?
+ @inner_markup ||= source[block_start_end_index...block_end_start_index]
+ end
+
+ def inner_json
+ return nil unless schema?
+ @inner_json ||= JSON.parse(inner_markup)
+ rescue JSON::ParserError
+ # Handled by ValidSchema
+ @inner_json = nil
+ end
+
def markup=(markup)
if @value.instance_variable_defined?(:@markup)
@value.instance_variable_set(:@markup, markup)
end
end
@@ -68,42 +104,30 @@
def start_index
position.start_index
end
- def end_index
- position.end_index
+ def start_row
+ position.start_row
end
- def start_token_index
- return position.start_index if inside_liquid_tag?
- position.start_index - (start_token.length + 1)
+ def start_column
+ position.start_column
end
- def end_token_index
- return position.end_index if inside_liquid_tag?
- position.end_index + end_token.length
+ def end_index
+ position.end_index
end
- def render_start_tag
- "#{start_token} #{@value.raw}#{end_token}"
+ def end_row
+ position.end_row
end
- def render_end_tag
- "#{start_token} #{@value.block_delimiter} #{end_token}"
+ def end_column
+ position.end_column
end
- def block_body_start_index
- return unless block_tag?
- block_regex.begin(:body)
- end
-
- def block_body_end_index
- return unless block_tag?
- block_regex.end(:body)
- end
-
# Literals are hard-coded values in the liquid file.
def literal?
@value.is_a?(String) || @value.is_a?(Integer)
end
@@ -114,10 +138,18 @@
def variable?
@value.is_a?(Liquid::Variable)
end
+ def assigned_or_echoed_variable?
+ variable? && start_token == ""
+ end
+
+ def variable_lookup?
+ @value.is_a?(Liquid::VariableLookup)
+ end
+
# A {% comment %} block node?
def comment?
@value.is_a?(Liquid::Comment)
end
@@ -140,20 +172,104 @@
# A block of type of node?
def block?
block_tag? || block_body? || document?
end
+ def schema?
+ @value.is_a?(ThemeCheck::Tags::Schema)
+ end
+
# The `:under_score_name` of this type of node. Used to dispatch to the `on_<type_name>`
# and `after_<type_name>` check methods.
def type_name
@type_name ||= StringHelpers.underscore(StringHelpers.demodulize(@value.class.name)).to_sym
end
def source
theme_file&.source
end
+ def block_start_markup
+ source[block_start_start_index...block_start_end_index]
+ end
+
+ def block_start_start_index
+ @block_start_start_index ||= if inside_liquid_tag?
+ backtrack_on_whitespace(source, start_index, /[ \t]/)
+ elsif tag?
+ backtrack_on_whitespace(source, start_index) - start_token.length
+ else
+ position.start_index - start_token.length
+ end
+ end
+
+ def block_start_end_index
+ @block_start_end_index ||= position.end_index + end_token.size
+ end
+
+ def block_end_markup
+ source[block_end_start_index...block_end_end_index]
+ end
+
+ def block_end_start_index
+ return block_start_end_index unless tag? && block?
+ @block_end_start_index ||= block_end_match&.begin(0) || block_start_end_index
+ end
+
+ def block_end_end_index
+ return block_end_start_index unless tag? && block?
+ @block_end_end_index ||= block_end_match&.end(0) || block_start_end_index
+ end
+
+ def outer_markup_start_index
+ outer_markup_position.start_index
+ end
+
+ def outer_markup_end_index
+ outer_markup_position.end_index
+ end
+
+ def outer_markup_start_row
+ outer_markup_position.start_row
+ end
+
+ def outer_markup_start_column
+ outer_markup_position.start_column
+ end
+
+ def outer_markup_end_row
+ outer_markup_position.end_row
+ end
+
+ def outer_markup_end_column
+ outer_markup_position.end_column
+ end
+
+ def inner_markup_start_index
+ inner_markup_position.start_index
+ end
+
+ def inner_markup_end_index
+ inner_markup_position.end_index
+ end
+
+ def inner_markup_start_row
+ inner_markup_position.start_row
+ end
+
+ def inner_markup_start_column
+ inner_markup_position.start_column
+ end
+
+ def inner_markup_end_row
+ inner_markup_position.end_row
+ end
+
+ def inner_markup_end_column
+ inner_markup_position.end_column
+ end
+
WHITESPACE = /\s/
# Is this node inside a `{% liquid ... %}` block?
def inside_liquid_tag?
# What we're doing here is starting at the start of the tag and
@@ -191,42 +307,69 @@
false
end
end
def start_token
- return "" if inside_liquid_tag?
- output = ""
- output += "{{" if variable?
- output += "{%" if tag?
- output += "-" if whitespace_trimmed_start?
- output
+ if inside_liquid_tag?
+ ""
+ elsif variable? && source[start_index - 3..start_index - 1] == "{{-"
+ "{{-"
+ elsif variable? && source[start_index - 2..start_index - 1] == "{{"
+ "{{"
+ elsif tag? && whitespace_trimmed_start?
+ "{%-"
+ elsif tag?
+ "{%"
+ else
+ ""
+ end
end
def end_token
- return "" if inside_liquid_tag?
- output = ""
- output += "-" if whitespace_trimmed_end?
- output += "}}" if variable?
- output += "%}" if tag?
- output
+ if inside_liquid_tag? && source[end_index] == "\n"
+ "\n"
+ elsif inside_liquid_tag?
+ ""
+ elsif variable? && source[end_index...end_index + 3] == "-}}"
+ "-}}"
+ elsif variable? && source[end_index...end_index + 2] == "}}"
+ "}}"
+ elsif tag? && whitespace_trimmed_end?
+ "-%}"
+ elsif tag?
+ "%}"
+ else # this could happen because we're in an assign statement (variable)
+ ""
+ end
end
private
- def block_regex
- return unless block_tag?
- /(?<start_token>#{render_start_tag})(?<body>.*)(?<end_token>#{render_end_tag})/m.match(source)
- end
-
def position
@position ||= Position.new(
markup,
theme_file&.source,
line_number_1_indexed: line_number
)
end
+ def outer_markup_position
+ @outer_markup_position ||= StrictPosition.new(
+ outer_markup,
+ source,
+ block_start_start_index,
+ )
+ end
+
+ def inner_markup_position
+ @inner_markup_position ||= StrictPosition.new(
+ inner_markup,
+ source,
+ block_start_end_index,
+ )
+ end
+
# Here we're hacking around a glorious bug in Liquid that makes it so the
# line_number and markup of a tag is wrong if there's whitespace
# between the tag_name and the markup of the tag.
#
# {%
@@ -317,8 +460,75 @@
# keep track of the error in line_number
@line_number_offset = source[tag_start...markup_start].count("\n")
# return the real raw content
@tag_markup = source[tag_start...markup_end]
+ end
+
+ # Returns the index of the leftmost consecutive whitespace
+ # starting from start going backwards.
+ #
+ # e.g. backtrack_on_whitespace("01 45", 4) would return 2.
+ # e.g. backtrack_on_whitespace("{% render %}", 5) would return 2.
+ def backtrack_on_whitespace(string, start, whitespace = WHITESPACE)
+ i = start
+ i -= 1 while string[i - 1] =~ whitespace && i > 0
+ i
+ end
+
+ def find_block_delimiter(start_index)
+ return nil unless tag? && block?
+
+ tag_start, tag_end = if inside_liquid_tag?
+ [
+ /^\s*#{@value.tag_name}\s*/,
+ /^\s*end#{@value.tag_name}\s*/,
+ ]
+ else
+ [
+ /#{Liquid::TagStart}-?\s*#{@value.tag_name}/mi,
+ /#{Liquid::TagStart}-?\s*end#{@value.tag_name}\s*-?#{Liquid::TagEnd}/mi,
+ ]
+ end
+
+ # This little algorithm below find the _correct_ block delimiter
+ # (endif, endcase, endcomment) for the current tag. What do I
+ # mean by correct? It means the one you'd expect. Making sure
+ # that we don't do the naive regex find. Since you can have
+ # nested ifs, fors, etc.
+ #
+ # It works by having a stack, pushing onto the stack when we
+ # open a tag of our type_name. And popping when we find a
+ # closing tag of our type_name.
+ #
+ # When the stack is empty, we return the end tag match.
+ index = start_index
+ stack = []
+ stack.push("open")
+ loop do
+ tag_start_match = tag_start.match(source, index)
+ tag_end_match = tag_end.match(source, index)
+
+ return nil unless tag_end_match
+
+ # We have found a tag_start and it appeared _before_ the
+ # tag_end that we found, thus we push it onto the stack.
+ if tag_start_match && tag_start_match.end(0) < tag_end_match.end(0)
+ stack.push("open")
+ end
+
+ # We have found a tag_end, therefore we pop
+ stack.pop
+
+ # Nothing left on the stack, we're done.
+ break tag_end_match if stack.empty?
+
+ # We keep looking from the end of the end tag we just found.
+ index = tag_end_match.end(0)
+ end
+ end
+
+ def block_end_match
+ @block_end_match ||= find_block_delimiter(block_start_end_index)
end
end
end