better_html 0.0.12 → 1.0.0
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.
- checksums.yaml +4 -4
- data/lib/better_html.rb +0 -2
- data/lib/better_html/ast/iterator.rb +32 -0
- data/lib/better_html/ast/node.rb +14 -0
- data/lib/better_html/better_erb/runtime_checks.rb +3 -3
- data/lib/better_html/config.rb +12 -0
- data/lib/better_html/parser.rb +286 -0
- data/lib/better_html/test_helper/ruby_expr.rb +8 -5
- data/lib/better_html/test_helper/safe_erb_tester.rb +121 -108
- data/lib/better_html/test_helper/safe_lodash_tester.rb +44 -42
- data/lib/better_html/tokenizer/base_erb.rb +79 -0
- data/lib/better_html/tokenizer/html_erb.rb +31 -0
- data/lib/better_html/{node_iterator → tokenizer}/html_lodash.rb +30 -34
- data/lib/better_html/tokenizer/javascript_erb.rb +15 -0
- data/lib/better_html/{node_iterator → tokenizer}/location.rb +9 -3
- data/lib/better_html/tokenizer/token.rb +16 -0
- data/lib/better_html/tokenizer/token_array.rb +54 -0
- data/lib/better_html/tree/attribute.rb +31 -0
- data/lib/better_html/tree/attributes_list.rb +25 -0
- data/lib/better_html/tree/tag.rb +39 -0
- data/lib/better_html/version.rb +1 -1
- data/test/better_html/parser_test.rb +279 -0
- data/test/better_html/test_helper/safe_erb_tester_test.rb +11 -0
- data/test/better_html/test_helper/safe_lodash_tester_test.rb +11 -1
- data/test/better_html/tokenizer/html_erb_test.rb +158 -0
- data/test/better_html/tokenizer/html_lodash_test.rb +98 -0
- data/test/better_html/tokenizer/location_test.rb +57 -0
- data/test/better_html/tokenizer/token_array_test.rb +144 -0
- data/test/better_html/tokenizer/token_test.rb +15 -0
- metadata +45 -30
- data/lib/better_html/node_iterator.rb +0 -144
- data/lib/better_html/node_iterator/attribute.rb +0 -34
- data/lib/better_html/node_iterator/base.rb +0 -27
- data/lib/better_html/node_iterator/cdata.rb +0 -8
- data/lib/better_html/node_iterator/comment.rb +0 -8
- data/lib/better_html/node_iterator/content_node.rb +0 -13
- data/lib/better_html/node_iterator/element.rb +0 -26
- data/lib/better_html/node_iterator/html_erb.rb +0 -70
- data/lib/better_html/node_iterator/javascript_erb.rb +0 -55
- data/lib/better_html/node_iterator/text.rb +0 -8
- data/lib/better_html/node_iterator/token.rb +0 -8
- data/lib/better_html/tree.rb +0 -113
- data/test/better_html/node_iterator/html_erb_test.rb +0 -116
- data/test/better_html/node_iterator/html_lodash_test.rb +0 -132
- data/test/better_html/node_iterator/location_test.rb +0 -36
- data/test/better_html/node_iterator_test.rb +0 -221
- data/test/better_html/tree_test.rb +0 -110
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 16bc95c4f55d720a177e9849dea00e7d83c66455
         | 
| 4 | 
            +
              data.tar.gz: 376b80e51f2379ce6c5640e834de1c62aa990ae5
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: bf07d75143cfde43414bb3e8a5c2520b527f047d0c796c7ff97e60aa2d23d0bc03ea1f06c72eab5db5f4c10cdb01eccf0c6abe585b0ae394dc662731599e296b
         | 
| 7 | 
            +
              data.tar.gz: b7dd6bfc56b0a36d773dabbe13a7ad922bc9c5cd322f4a240ba8d9064c8753ec4efca3fb5b1a7c63d36291b60dd48ec142521b3af98811f3a017d5d62f90ab97
         | 
    
        data/lib/better_html.rb
    CHANGED
    
    
| @@ -0,0 +1,32 @@ | |
| 1 | 
            +
            require 'ast'
         | 
| 2 | 
            +
            require 'active_support/core_ext/array/wrap'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module BetterHtml
         | 
| 5 | 
            +
              module AST
         | 
| 6 | 
            +
                class Iterator
         | 
| 7 | 
            +
                  def initialize(types, &block)
         | 
| 8 | 
            +
                    @types = Array.wrap(types)
         | 
| 9 | 
            +
                    @block = block
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def traverse(node)
         | 
| 13 | 
            +
                    return unless node.is_a?(::AST::Node)
         | 
| 14 | 
            +
                    @block.call(node) if @types.include?(node.type)
         | 
| 15 | 
            +
                    traverse_all(node)
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def traverse_all(nodes)
         | 
| 19 | 
            +
                    nodes.to_a.each do |node|
         | 
| 20 | 
            +
                      traverse(node) if node.is_a?(::AST::Node)
         | 
| 21 | 
            +
                    end
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def self.descendants(root_node, type, &block)
         | 
| 25 | 
            +
                    Enumerator.new do |yielder|
         | 
| 26 | 
            +
                      t = new(type) { |node| yielder << node }
         | 
| 27 | 
            +
                      t.traverse(root_node)
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
              end
         | 
| 32 | 
            +
            end
         | 
| @@ -111,7 +111,7 @@ class BetterHtml::BetterErb | |
| 111 111 | 
             
                end
         | 
| 112 112 |  | 
| 113 113 | 
             
                def check_tag_name(type, start, stop, line, column)
         | 
| 114 | 
            -
                  text = @parser. | 
| 114 | 
            +
                  text = @parser.document[start...stop]
         | 
| 115 115 | 
             
                  return if text.upcase == "!DOCTYPE"
         | 
| 116 116 | 
             
                  return if @config.partial_tag_name_pattern === text
         | 
| 117 117 |  | 
| @@ -122,7 +122,7 @@ class BetterHtml::BetterErb | |
| 122 122 | 
             
                end
         | 
| 123 123 |  | 
| 124 124 | 
             
                def check_attribute_name(type, start, stop, line, column)
         | 
| 125 | 
            -
                  text = @parser. | 
| 125 | 
            +
                  text = @parser.document[start...stop]
         | 
| 126 126 | 
             
                  return if @config.partial_attribute_name_pattern === text
         | 
| 127 127 |  | 
| 128 128 | 
             
                  s = "Invalid attribute name #{text.inspect} does not match "\
         | 
| @@ -133,7 +133,7 @@ class BetterHtml::BetterErb | |
| 133 133 |  | 
| 134 134 | 
             
                def check_quoted_value(type, start, stop, line, column)
         | 
| 135 135 | 
             
                  return if @config.allow_single_quoted_attributes
         | 
| 136 | 
            -
                  text = @parser. | 
| 136 | 
            +
                  text = @parser.document[start...stop]
         | 
| 137 137 | 
             
                  return if text == '"'
         | 
| 138 138 |  | 
| 139 139 | 
             
                  s = "Single-quoted attributes are not allowed\n"
         | 
    
        data/lib/better_html/config.rb
    CHANGED
    
    | @@ -12,5 +12,17 @@ module BetterHtml | |
| 12 12 | 
             
                property :javascript_attribute_names, default: [/\Aon/i]
         | 
| 13 13 | 
             
                property :template_exclusion_filter
         | 
| 14 14 | 
             
                property :lodash_safe_javascript_expression, default: [/\AJSON\.stringify\(/]
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def javascript_attribute_name?(name)
         | 
| 17 | 
            +
                  javascript_attribute_names.any?{ |other| other === name.to_s }
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def lodash_safe_javascript_expression?(code)
         | 
| 21 | 
            +
                  lodash_safe_javascript_expression.any?{ |other| other === code }
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def javascript_safe_method?(name)
         | 
| 25 | 
            +
                  javascript_safe_methods.include?(name.to_s)
         | 
| 26 | 
            +
                end
         | 
| 15 27 | 
             
              end
         | 
| 16 28 | 
             
            end
         | 
| @@ -0,0 +1,286 @@ | |
| 1 | 
            +
            require_relative 'tokenizer/javascript_erb'
         | 
| 2 | 
            +
            require_relative 'tokenizer/html_erb'
         | 
| 3 | 
            +
            require_relative 'tokenizer/html_lodash'
         | 
| 4 | 
            +
            require_relative 'tokenizer/location'
         | 
| 5 | 
            +
            require_relative 'tokenizer/token_array'
         | 
| 6 | 
            +
            require_relative 'ast/node'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            module BetterHtml
         | 
| 9 | 
            +
              class Parser
         | 
| 10 | 
            +
                attr_reader :template_language
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def initialize(document, template_language: :html)
         | 
| 13 | 
            +
                  @document = document
         | 
| 14 | 
            +
                  @template_language = template_language
         | 
| 15 | 
            +
                  @erb = case template_language
         | 
| 16 | 
            +
                  when :html
         | 
| 17 | 
            +
                    Tokenizer::HtmlErb.new(@document)
         | 
| 18 | 
            +
                  when :lodash
         | 
| 19 | 
            +
                    Tokenizer::HtmlLodash.new(@document)
         | 
| 20 | 
            +
                  when :javascript
         | 
| 21 | 
            +
                    Tokenizer::JavascriptErb.new(@document)
         | 
| 22 | 
            +
                  else
         | 
| 23 | 
            +
                    raise ArgumentError, "template_language can be :html or :javascript"
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def nodes_with_type(*type)
         | 
| 28 | 
            +
                  types = Array.wrap(type)
         | 
| 29 | 
            +
                  ast.children.select{ |node| node.is_a?(::AST::Node) && types.include?(node.type) }
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def ast
         | 
| 33 | 
            +
                  @ast ||= build_document_node
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def inspect
         | 
| 37 | 
            +
                  "#<#{self.class.name} ast=#{ast.inspect}>"
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                private
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                INTERPOLATION_TYPES = [:erb_begin, :lodash_begin]
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                def build_document_node
         | 
| 45 | 
            +
                  children = []
         | 
| 46 | 
            +
                  tokens = Tokenizer::TokenArray.new(@erb.tokens)
         | 
| 47 | 
            +
                  while tokens.any?
         | 
| 48 | 
            +
                    case tokens.current.type
         | 
| 49 | 
            +
                    when :cdata_start
         | 
| 50 | 
            +
                      children << build_cdata_node(tokens)
         | 
| 51 | 
            +
                    when :comment_start
         | 
| 52 | 
            +
                      children << build_comment_node(tokens)
         | 
| 53 | 
            +
                    when :tag_start
         | 
| 54 | 
            +
                      children << build_tag_node(tokens)
         | 
| 55 | 
            +
                    when :text, *INTERPOLATION_TYPES
         | 
| 56 | 
            +
                      children << build_text_node(tokens)
         | 
| 57 | 
            +
                    else
         | 
| 58 | 
            +
                      raise RuntimeError, "Unhandled token #{tokens.current.type} line #{tokens.current.loc.line} column #{tokens.current.loc.column}, #{children.inspect}"
         | 
| 59 | 
            +
                    end
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                  build_node(:document, children.empty? ? nil : children)
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                def build_erb_node(tokens)
         | 
| 66 | 
            +
                  erb_begin = shift_single(tokens, :erb_begin)
         | 
| 67 | 
            +
                  children = [
         | 
| 68 | 
            +
                    shift_single(tokens, :indicator),
         | 
| 69 | 
            +
                    shift_single(tokens, :trim),
         | 
| 70 | 
            +
                    shift_single(tokens, :code),
         | 
| 71 | 
            +
                    shift_single(tokens, :trim),
         | 
| 72 | 
            +
                  ]
         | 
| 73 | 
            +
                  erb_end = shift_single(tokens, :erb_end)
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  build_node(:erb, children, pre: erb_begin, post: erb_end)
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                def build_lodash_node(tokens)
         | 
| 79 | 
            +
                  lodash_begin = shift_single(tokens, :lodash_begin)
         | 
| 80 | 
            +
                  children = [
         | 
| 81 | 
            +
                    shift_single(tokens, :indicator),
         | 
| 82 | 
            +
                    shift_single(tokens, :code),
         | 
| 83 | 
            +
                  ]
         | 
| 84 | 
            +
                  lodash_end = shift_single(tokens, :lodash_end)
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                  build_node(:lodash, children, pre: lodash_begin, post: lodash_end)
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                def build_cdata_node(tokens)
         | 
| 90 | 
            +
                  cdata_start, children, cdata_end = shift_between_with_interpolation(tokens, :cdata_start, :cdata_end)
         | 
| 91 | 
            +
                  build_node(:cdata, children, pre: cdata_start, post: cdata_end)
         | 
| 92 | 
            +
                end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                def build_comment_node(tokens)
         | 
| 95 | 
            +
                  comment_start, children, comment_end = shift_between_with_interpolation(tokens, :comment_start, :comment_end)
         | 
| 96 | 
            +
                  build_node(:comment, children, pre: comment_start, post: comment_end)
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                def build_tag_node(tokens)
         | 
| 100 | 
            +
                  tag_start, tag_content, tag_end = shift_between(tokens, :tag_start, :tag_end)
         | 
| 101 | 
            +
                  tag_tokens = Tokenizer::TokenArray.new(tag_content)
         | 
| 102 | 
            +
                  tag_tokens.trim(:whitespace)
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  children = [
         | 
| 105 | 
            +
                    shift_single(tag_tokens, :solidus),
         | 
| 106 | 
            +
                    build_tag_name_node(tag_tokens),
         | 
| 107 | 
            +
                    build_tag_attributes_node(tag_tokens),
         | 
| 108 | 
            +
                    shift_single(tag_tokens, :solidus),
         | 
| 109 | 
            +
                  ]
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                  build_node(:tag, children, pre: tag_start, post: tag_end)
         | 
| 112 | 
            +
                end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                def build_tag_name_node(tokens)
         | 
| 115 | 
            +
                  children = shift_all_with_interpolation(tokens, :tag_name)
         | 
| 116 | 
            +
                  build_node(:tag_name, children) if children.any?
         | 
| 117 | 
            +
                end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                def build_tag_attributes_node(tokens)
         | 
| 120 | 
            +
                  attributes_tokens = []
         | 
| 121 | 
            +
                  while tokens.any?
         | 
| 122 | 
            +
                    break if tokens.size == 1 && tokens.last.type == :solidus
         | 
| 123 | 
            +
                    if tokens.current.type == :attribute_name
         | 
| 124 | 
            +
                      attributes_tokens << build_attribute_node(tokens)
         | 
| 125 | 
            +
                    elsif tokens.current.type == :attribute_quoted_value_start
         | 
| 126 | 
            +
                      attributes_tokens << build_nameless_attribute_node(tokens)
         | 
| 127 | 
            +
                    else
         | 
| 128 | 
            +
                      # todo: warn about ignored things
         | 
| 129 | 
            +
                      tokens.shift
         | 
| 130 | 
            +
                    end
         | 
| 131 | 
            +
                  end
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                  build_node(:tag_attributes, attributes_tokens) if attributes_tokens.any?
         | 
| 134 | 
            +
                end
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                def build_nameless_attribute_node(tokens)
         | 
| 137 | 
            +
                  value_node = build_attribute_value_node(tokens)
         | 
| 138 | 
            +
                  build_node(:attribute, [nil, nil, value_node])
         | 
| 139 | 
            +
                end
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                def build_attribute_node(tokens)
         | 
| 142 | 
            +
                  name_node = build_attribute_name_node(tokens)
         | 
| 143 | 
            +
                  shift_all(tokens, :whitespace)
         | 
| 144 | 
            +
                  equal_token = shift_single(tokens, :equal)
         | 
| 145 | 
            +
                  shift_all(tokens, :whitespace)
         | 
| 146 | 
            +
                  value_node = build_attribute_value_node(tokens) if equal_token.present?
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                  build_node(:attribute, [name_node, equal_token, value_node])
         | 
| 149 | 
            +
                end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                def build_attribute_name_node(tokens)
         | 
| 152 | 
            +
                  children = shift_all_with_interpolation(tokens, :attribute_name)
         | 
| 153 | 
            +
                  build_node(:attribute_name, children)
         | 
| 154 | 
            +
                end
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                def build_attribute_value_node(tokens)
         | 
| 157 | 
            +
                  children = shift_all_with_interpolation(tokens,
         | 
| 158 | 
            +
                    :attribute_quoted_value_start, :attribute_quoted_value,
         | 
| 159 | 
            +
                    :attribute_quoted_value_end, :attribute_unquoted_value
         | 
| 160 | 
            +
                  )
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                  build_node(:attribute_value, children)
         | 
| 163 | 
            +
                end
         | 
| 164 | 
            +
             | 
| 165 | 
            +
                def build_text_node(tokens)
         | 
| 166 | 
            +
                  text_tokens = shift_all_with_interpolation(tokens, :text)
         | 
| 167 | 
            +
                  build_node(:text, text_tokens)
         | 
| 168 | 
            +
                end
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                def build_node(type, tokens, pre: nil, post: nil)
         | 
| 171 | 
            +
                  BetterHtml::AST::Node.new(
         | 
| 172 | 
            +
                    type,
         | 
| 173 | 
            +
                    tokens.present? ? wrap_tokens(tokens) : [],
         | 
| 174 | 
            +
                    loc: tokens.present? ? build_location([pre, *tokens, post]) : empty_location
         | 
| 175 | 
            +
                  )
         | 
| 176 | 
            +
                end
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                def build_location(enumerable)
         | 
| 179 | 
            +
                  enumerable = enumerable.compact
         | 
| 180 | 
            +
                  raise ArgumentError, "cannot build location for #{enumerable.inspect}" unless enumerable.first && enumerable.last
         | 
| 181 | 
            +
                  Tokenizer::Location.new(@document, enumerable.first.loc.start, enumerable.last.loc.stop)
         | 
| 182 | 
            +
                end
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                def empty_location
         | 
| 185 | 
            +
                  Tokenizer::Location.new(@document, 0, 0)
         | 
| 186 | 
            +
                end
         | 
| 187 | 
            +
             | 
| 188 | 
            +
                def shift_all(tokens, *types)
         | 
| 189 | 
            +
                  [].tap do |items|
         | 
| 190 | 
            +
                    while tokens.any?
         | 
| 191 | 
            +
                      if types.include?(tokens.current.type)
         | 
| 192 | 
            +
                        items << tokens.shift
         | 
| 193 | 
            +
                      else
         | 
| 194 | 
            +
                        break
         | 
| 195 | 
            +
                      end
         | 
| 196 | 
            +
                    end
         | 
| 197 | 
            +
                  end
         | 
| 198 | 
            +
                end
         | 
| 199 | 
            +
             | 
| 200 | 
            +
                def shift_single(tokens, *types)
         | 
| 201 | 
            +
                  tokens.shift if tokens.any? && types.include?(tokens.current.type)
         | 
| 202 | 
            +
                end
         | 
| 203 | 
            +
             | 
| 204 | 
            +
                def shift_until(tokens, *types)
         | 
| 205 | 
            +
                  [].tap do |items|
         | 
| 206 | 
            +
                    while tokens.any?
         | 
| 207 | 
            +
                      if !types.include?(tokens.current.type)
         | 
| 208 | 
            +
                        items << tokens.shift
         | 
| 209 | 
            +
                      else
         | 
| 210 | 
            +
                        break
         | 
| 211 | 
            +
                      end
         | 
| 212 | 
            +
                    end
         | 
| 213 | 
            +
                  end
         | 
| 214 | 
            +
                end
         | 
| 215 | 
            +
             | 
| 216 | 
            +
                def build_interpolation_node(tokens)
         | 
| 217 | 
            +
                  if tokens.current.type == :erb_begin
         | 
| 218 | 
            +
                    build_erb_node(tokens)
         | 
| 219 | 
            +
                  elsif tokens.current.type == :lodash_begin
         | 
| 220 | 
            +
                    build_lodash_node(tokens)
         | 
| 221 | 
            +
                  else
         | 
| 222 | 
            +
                    tokens.shift
         | 
| 223 | 
            +
                  end
         | 
| 224 | 
            +
                end
         | 
| 225 | 
            +
             | 
| 226 | 
            +
                def shift_all_with_interpolation(tokens, *types)
         | 
| 227 | 
            +
                  types = [*INTERPOLATION_TYPES, *types]
         | 
| 228 | 
            +
                  [].tap do |result|
         | 
| 229 | 
            +
                    while tokens.any?
         | 
| 230 | 
            +
                      if types.include?(tokens.current.type)
         | 
| 231 | 
            +
                        result << build_interpolation_node(tokens)
         | 
| 232 | 
            +
                      else
         | 
| 233 | 
            +
                        break
         | 
| 234 | 
            +
                      end
         | 
| 235 | 
            +
                    end
         | 
| 236 | 
            +
                  end
         | 
| 237 | 
            +
                end
         | 
| 238 | 
            +
             | 
| 239 | 
            +
                def shift_until_with_interpolation(tokens, *types)
         | 
| 240 | 
            +
                  [].tap do |result|
         | 
| 241 | 
            +
                    while tokens.any?
         | 
| 242 | 
            +
                      if !types.include?(tokens.current.type)
         | 
| 243 | 
            +
                        result << build_interpolation_node(tokens)
         | 
| 244 | 
            +
                      else
         | 
| 245 | 
            +
                        break
         | 
| 246 | 
            +
                      end
         | 
| 247 | 
            +
                    end
         | 
| 248 | 
            +
                  end
         | 
| 249 | 
            +
                end
         | 
| 250 | 
            +
             | 
| 251 | 
            +
                def shift_between(tokens, start_type, end_type)
         | 
| 252 | 
            +
                  start_token = shift_single(tokens, start_type)
         | 
| 253 | 
            +
                  children = shift_until(tokens, end_type)
         | 
| 254 | 
            +
                  end_token = shift_single(tokens, end_type)
         | 
| 255 | 
            +
             | 
| 256 | 
            +
                  [start_token, children, end_token]
         | 
| 257 | 
            +
                end
         | 
| 258 | 
            +
             | 
| 259 | 
            +
                def shift_between_with_interpolation(tokens, start_type, end_type)
         | 
| 260 | 
            +
                  start_token = shift_single(tokens, start_type)
         | 
| 261 | 
            +
                  children = shift_until_with_interpolation(tokens, end_type)
         | 
| 262 | 
            +
                  end_token = shift_single(tokens, end_type)
         | 
| 263 | 
            +
             | 
| 264 | 
            +
                  [start_token, children, end_token]
         | 
| 265 | 
            +
                end
         | 
| 266 | 
            +
             | 
| 267 | 
            +
                def wrap_token(object)
         | 
| 268 | 
            +
                  return unless object
         | 
| 269 | 
            +
                  if object.is_a?(::AST::Node)
         | 
| 270 | 
            +
                    object
         | 
| 271 | 
            +
                  elsif [:text, :tag_name, :attribute_name, :attribute_quoted_value, :attribute_unquoted_value].include?(object.type)
         | 
| 272 | 
            +
                    object.loc.source
         | 
| 273 | 
            +
                  elsif [:attribute_quoted_value_start, :attribute_quoted_value_end].include?(object.type)
         | 
| 274 | 
            +
                    BetterHtml::AST::Node.new(:quote, [object.loc.source], loc: object.loc)
         | 
| 275 | 
            +
                  elsif [:indicator, :code].include?(object.type)
         | 
| 276 | 
            +
                    BetterHtml::AST::Node.new(object.type, [object.loc.source], loc: object.loc)
         | 
| 277 | 
            +
                  else
         | 
| 278 | 
            +
                    BetterHtml::AST::Node.new(object.type, [], loc: object.loc)
         | 
| 279 | 
            +
                  end
         | 
| 280 | 
            +
                end
         | 
| 281 | 
            +
             | 
| 282 | 
            +
                def wrap_tokens(enumerable)
         | 
| 283 | 
            +
                  enumerable.map { |object| wrap_token(object) }
         | 
| 284 | 
            +
                end
         | 
| 285 | 
            +
              end
         | 
| 286 | 
            +
            end
         | 
| @@ -22,14 +22,17 @@ module BetterHtml | |
| 22 22 | 
             
                  end
         | 
| 23 23 |  | 
| 24 24 | 
             
                  def initialize(ast)
         | 
| 25 | 
            -
                    raise ArgumentError, "expect first argument to be Parser::AST::Node" unless ast.is_a?(Parser::AST::Node)
         | 
| 25 | 
            +
                    raise ArgumentError, "expect first argument to be Parser::AST::Node" unless ast.is_a?(::Parser::AST::Node)
         | 
| 26 26 | 
             
                    @ast = ast
         | 
| 27 27 | 
             
                  end
         | 
| 28 28 |  | 
| 29 29 | 
             
                  def self.parse(code)
         | 
| 30 | 
            -
                    parser = Parser::CurrentRuby.new
         | 
| 31 | 
            -
                    parser.diagnostics. | 
| 32 | 
            -
                     | 
| 30 | 
            +
                    parser = ::Parser::CurrentRuby.new
         | 
| 31 | 
            +
                    parser.diagnostics.ignore_warnings = true
         | 
| 32 | 
            +
                    parser.diagnostics.all_errors_are_fatal = false
         | 
| 33 | 
            +
                    parser.diagnostics.consumer = nil
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                    buf = ::Parser::Source::Buffer.new('(string)')
         | 
| 33 36 | 
             
                    buf.source = code.sub(BLOCK_EXPR, '')
         | 
| 34 37 | 
             
                    parsed = parser.parse(buf)
         | 
| 35 38 | 
             
                    raise ParseError, "error parsing code: #{code.inspect}" unless parsed
         | 
| @@ -53,7 +56,7 @@ module BetterHtml | |
| 53 56 |  | 
| 54 57 | 
             
                  def each_child_node(current=@ast, only: nil, range: (0..-1))
         | 
| 55 58 | 
             
                    current.children[range].each do |child|
         | 
| 56 | 
            -
                      if child.is_a?(Parser::AST::Node) && node_match?(child, only)
         | 
| 59 | 
            +
                      if child.is_a?(::Parser::AST::Node) && node_match?(child, only)
         | 
| 57 60 | 
             
                        yield child
         | 
| 58 61 | 
             
                      end
         | 
| 59 62 | 
             
                    end
         | 
| @@ -1,6 +1,7 @@ | |
| 1 1 | 
             
            require 'better_html/test_helper/ruby_expr'
         | 
| 2 2 | 
             
            require 'better_html/test_helper/safety_error'
         | 
| 3 | 
            -
            require 'parser | 
| 3 | 
            +
            require 'better_html/parser'
         | 
| 4 | 
            +
            require 'better_html/tree/tag'
         | 
| 4 5 |  | 
| 5 6 | 
             
            module BetterHtml
         | 
| 6 7 | 
             
              module TestHelper
         | 
| @@ -58,7 +59,7 @@ EOF | |
| 58 59 | 
             
                      @errors = Errors.new
         | 
| 59 60 | 
             
                      @options = options.present? ? options.dup : {}
         | 
| 60 61 | 
             
                      @options[:template_language] ||= :html
         | 
| 61 | 
            -
                      @ | 
| 62 | 
            +
                      @parser = BetterHtml::Parser.new(data, @options.slice(:template_language))
         | 
| 62 63 | 
             
                      validate!
         | 
| 63 64 | 
             
                    end
         | 
| 64 65 |  | 
| @@ -67,86 +68,98 @@ EOF | |
| 67 68 | 
             
                    end
         | 
| 68 69 |  | 
| 69 70 | 
             
                    def validate!
         | 
| 70 | 
            -
                      @ | 
| 71 | 
            -
                         | 
| 72 | 
            -
                         | 
| 73 | 
            -
             | 
| 74 | 
            -
             | 
| 75 | 
            -
             | 
| 76 | 
            -
             | 
| 77 | 
            -
             | 
| 78 | 
            -
             | 
| 79 | 
            -
             | 
| 80 | 
            -
             | 
| 81 | 
            -
                               | 
| 71 | 
            +
                      @parser.nodes_with_type(:tag).each do |tag_node|
         | 
| 72 | 
            +
                        tag = Tree::Tag.from_node(tag_node)
         | 
| 73 | 
            +
                        next if tag.closing?
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                        validate_tag_attributes(tag)
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                        if tag.name == 'script'
         | 
| 78 | 
            +
                          index = @parser.ast.to_a.find_index(tag_node)
         | 
| 79 | 
            +
                          next_node = @parser.ast.to_a[index + 1]
         | 
| 80 | 
            +
                          if next_node.type == :text
         | 
| 81 | 
            +
                            if (tag.attributes['type']&.value || "text/javascript") == "text/javascript"
         | 
| 82 | 
            +
                              validate_script_tag_content(next_node)
         | 
| 82 83 | 
             
                            end
         | 
| 83 | 
            -
             | 
| 84 | 
            -
                            validate_javascript_tag_type(node) unless node.closing?
         | 
| 85 | 
            -
                          end
         | 
| 86 | 
            -
                        when BetterHtml::NodeIterator::Text
         | 
| 87 | 
            -
                          validate_text_content(node)
         | 
| 88 | 
            -
             | 
| 89 | 
            -
                          if @nodes.template_language == :javascript
         | 
| 90 | 
            -
                            validate_script_tag_content(node)
         | 
| 91 | 
            -
                            validate_no_statements(node)
         | 
| 92 | 
            -
                          else
         | 
| 93 | 
            -
                            validate_no_javascript_tag(node)
         | 
| 84 | 
            +
                            validate_no_statements(next_node) unless tag.attributes['type']&.value == "text/html"
         | 
| 94 85 | 
             
                          end
         | 
| 95 | 
            -
             | 
| 86 | 
            +
             | 
| 87 | 
            +
                          validate_javascript_tag_type(tag)
         | 
| 88 | 
            +
                        end
         | 
| 89 | 
            +
                      end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                      @parser.nodes_with_type(:text).each do |node|
         | 
| 92 | 
            +
                        validate_text_node(node)
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                        if @parser.template_language == :javascript
         | 
| 95 | 
            +
                          validate_script_tag_content(node)
         | 
| 96 96 | 
             
                          validate_no_statements(node)
         | 
| 97 | 
            +
                        else
         | 
| 98 | 
            +
                          validate_no_javascript_tag(node)
         | 
| 97 99 | 
             
                        end
         | 
| 98 100 | 
             
                      end
         | 
| 99 | 
            -
                    end
         | 
| 100 101 |  | 
| 101 | 
            -
             | 
| 102 | 
            -
             | 
| 103 | 
            -
                       | 
| 104 | 
            -
                      value == which
         | 
| 102 | 
            +
                      @parser.nodes_with_type(:cdata, :comment).each do |node|
         | 
| 103 | 
            +
                        validate_no_statements(node)
         | 
| 104 | 
            +
                      end
         | 
| 105 105 | 
             
                    end
         | 
| 106 106 |  | 
| 107 | 
            -
                    def  | 
| 108 | 
            -
                       | 
| 109 | 
            -
             | 
| 110 | 
            -
             | 
| 111 | 
            -
             | 
| 112 | 
            -
                           | 
| 113 | 
            -
             | 
| 114 | 
            -
                        )
         | 
| 107 | 
            +
                    def erb_nodes(node)
         | 
| 108 | 
            +
                      Enumerator.new do |yielder|
         | 
| 109 | 
            +
                        next if node.nil?
         | 
| 110 | 
            +
                        node.descendants(:erb).each do |erb_node|
         | 
| 111 | 
            +
                          indicator_node, _, code_node, _ = *erb_node
         | 
| 112 | 
            +
                          yielder.yield(erb_node, indicator_node, code_node)
         | 
| 113 | 
            +
                        end
         | 
| 115 114 | 
             
                      end
         | 
| 116 115 | 
             
                    end
         | 
| 117 116 |  | 
| 118 | 
            -
                    def  | 
| 119 | 
            -
                       | 
| 120 | 
            -
             | 
| 121 | 
            -
             | 
| 122 | 
            -
             | 
| 117 | 
            +
                    def validate_javascript_tag_type(tag)
         | 
| 118 | 
            +
                      return unless type_attribute = tag.attributes['type']
         | 
| 119 | 
            +
                      return if VALID_JAVASCRIPT_TAG_TYPES.include?(type_attribute.value)
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                      add_error(
         | 
| 122 | 
            +
                        "#{type_attribute.value} is not a valid type, valid types are #{VALID_JAVASCRIPT_TAG_TYPES.join(', ')}",
         | 
| 123 | 
            +
                        location: type_attribute.loc
         | 
| 124 | 
            +
                      )
         | 
| 125 | 
            +
                    end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                    def validate_tag_attributes(tag)
         | 
| 128 | 
            +
                      tag.attributes.each do |attribute|
         | 
| 129 | 
            +
                        erb_nodes(attribute.value_node).each do |erb_node, indicator_node, code_node|
         | 
| 130 | 
            +
                          next if indicator_node.nil?
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                          indicator = indicator_node.loc.source
         | 
| 133 | 
            +
                          source = code_node.loc.source
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                          if indicator == '='
         | 
| 123 136 | 
             
                            begin
         | 
| 124 | 
            -
                              expr = RubyExpr.parse( | 
| 125 | 
            -
                              validate_tag_expression( | 
| 137 | 
            +
                              expr = RubyExpr.parse(source)
         | 
| 138 | 
            +
                              validate_tag_expression(code_node, expr, attribute.name)
         | 
| 126 139 | 
             
                            rescue RubyExpr::ParseError
         | 
| 127 140 | 
             
                              nil
         | 
| 128 141 | 
             
                            end
         | 
| 129 | 
            -
                           | 
| 142 | 
            +
                          elsif indicator == '=='
         | 
| 130 143 | 
             
                            add_error(
         | 
| 131 144 | 
             
                              "erb interpolation with '<%==' inside html attribute is never safe",
         | 
| 132 | 
            -
                              location:  | 
| 145 | 
            +
                              location: erb_node.loc
         | 
| 133 146 | 
             
                            )
         | 
| 134 147 | 
             
                          end
         | 
| 135 148 | 
             
                        end
         | 
| 136 149 | 
             
                      end
         | 
| 137 150 | 
             
                    end
         | 
| 138 151 |  | 
| 139 | 
            -
                    def  | 
| 140 | 
            -
                       | 
| 141 | 
            -
                         | 
| 142 | 
            -
                         | 
| 143 | 
            -
             | 
| 144 | 
            -
             | 
| 145 | 
            -
             | 
| 146 | 
            -
             | 
| 147 | 
            -
                           | 
| 148 | 
            -
             | 
| 149 | 
            -
                           | 
| 152 | 
            +
                    def validate_text_node(text_node)
         | 
| 153 | 
            +
                      erb_nodes(text_node).each do |erb_node, indicator_node, code_node|
         | 
| 154 | 
            +
                        indicator = indicator_node&.loc&.source
         | 
| 155 | 
            +
                        next if indicator == '#'
         | 
| 156 | 
            +
                        source = code_node.loc.source
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                        begin
         | 
| 159 | 
            +
                          expr = RubyExpr.parse(source)
         | 
| 160 | 
            +
                          validate_ruby_helper(code_node, expr)
         | 
| 161 | 
            +
                        rescue RubyExpr::ParseError
         | 
| 162 | 
            +
                          nil
         | 
| 150 163 | 
             
                        end
         | 
| 151 164 | 
             
                      end
         | 
| 152 165 | 
             
                    end
         | 
| @@ -185,13 +198,13 @@ EOF | |
| 185 198 | 
             
                    def validate_tag_expression(parent_token, expr, attr_name)
         | 
| 186 199 | 
             
                      return if expr.static_value?
         | 
| 187 200 |  | 
| 188 | 
            -
                      if javascript_attribute_name?(attr_name) && expr.calls.empty?
         | 
| 201 | 
            +
                      if @config.javascript_attribute_name?(attr_name) && expr.calls.empty?
         | 
| 189 202 | 
             
                        add_error(
         | 
| 190 203 | 
             
                          "erb interpolation in javascript attribute must call '(...).to_json'",
         | 
| 191 | 
            -
                          location:  | 
| 204 | 
            +
                          location: Tokenizer::Location.new(
         | 
| 192 205 | 
             
                            @data,
         | 
| 193 | 
            -
                            parent_token. | 
| 194 | 
            -
                            parent_token. | 
| 206 | 
            +
                            parent_token.loc.start + expr.start,
         | 
| 207 | 
            +
                            parent_token.loc.start + expr.end - 1
         | 
| 195 208 | 
             
                          )
         | 
| 196 209 | 
             
                        )
         | 
| 197 210 | 
             
                        return
         | 
| @@ -201,57 +214,54 @@ EOF | |
| 201 214 | 
             
                        if call.method == :raw
         | 
| 202 215 | 
             
                          add_error(
         | 
| 203 216 | 
             
                            "erb interpolation with '<%= raw(...) %>' inside html attribute is never safe",
         | 
| 204 | 
            -
                            location:  | 
| 217 | 
            +
                            location: Tokenizer::Location.new(
         | 
| 205 218 | 
             
                              @data,
         | 
| 206 | 
            -
                              parent_token. | 
| 207 | 
            -
                              parent_token. | 
| 219 | 
            +
                              parent_token.loc.start + expr.start,
         | 
| 220 | 
            +
                              parent_token.loc.start + expr.end - 1
         | 
| 208 221 | 
             
                            )
         | 
| 209 222 | 
             
                          )
         | 
| 210 223 | 
             
                        elsif call.method == :html_safe
         | 
| 211 224 | 
             
                          add_error(
         | 
| 212 225 | 
             
                            "erb interpolation with '<%= (...).html_safe %>' inside html attribute is never safe",
         | 
| 213 | 
            -
                            location:  | 
| 226 | 
            +
                            location: Tokenizer::Location.new(
         | 
| 214 227 | 
             
                              @data,
         | 
| 215 | 
            -
                              parent_token. | 
| 216 | 
            -
                              parent_token. | 
| 228 | 
            +
                              parent_token.loc.start + expr.start,
         | 
| 229 | 
            +
                              parent_token.loc.start + expr.end - 1
         | 
| 217 230 | 
             
                            )
         | 
| 218 231 | 
             
                          )
         | 
| 219 | 
            -
                        elsif javascript_attribute_name?(attr_name) &&  | 
| 232 | 
            +
                        elsif @config.javascript_attribute_name?(attr_name) && !@config.javascript_safe_method?(call.method)
         | 
| 220 233 | 
             
                          add_error(
         | 
| 221 234 | 
             
                            "erb interpolation in javascript attribute must call '(...).to_json'",
         | 
| 222 | 
            -
                            location:  | 
| 235 | 
            +
                            location: Tokenizer::Location.new(
         | 
| 223 236 | 
             
                              @data,
         | 
| 224 | 
            -
                              parent_token. | 
| 225 | 
            -
                              parent_token. | 
| 237 | 
            +
                              parent_token.loc.start + expr.start,
         | 
| 238 | 
            +
                              parent_token.loc.start + expr.end - 1
         | 
| 226 239 | 
             
                            )
         | 
| 227 240 | 
             
                          )
         | 
| 228 241 | 
             
                        end
         | 
| 229 242 | 
             
                      end
         | 
| 230 243 | 
             
                    end
         | 
| 231 244 |  | 
| 232 | 
            -
                    def javascript_attribute_name?(name)
         | 
| 233 | 
            -
                      @config.javascript_attribute_names.any?{ |other| other === name.to_s }
         | 
| 234 | 
            -
                    end
         | 
| 235 | 
            -
             | 
| 236 | 
            -
                    def javascript_safe_method?(name)
         | 
| 237 | 
            -
                      @config.javascript_safe_methods.include?(name.to_s)
         | 
| 238 | 
            -
                    end
         | 
| 239 | 
            -
             | 
| 240 245 | 
             
                    def validate_script_tag_content(node)
         | 
| 241 | 
            -
                      node. | 
| 242 | 
            -
                         | 
| 243 | 
            -
                         | 
| 244 | 
            -
             | 
| 245 | 
            -
             | 
| 246 | 
            +
                      erb_nodes(node).each do |erb_node, indicator_node, code_node|
         | 
| 247 | 
            +
                        next unless indicator_node.present?
         | 
| 248 | 
            +
                        indicator = indicator_node.loc.source
         | 
| 249 | 
            +
                        next if indicator == '#'
         | 
| 250 | 
            +
                        source = code_node.loc.source
         | 
| 251 | 
            +
             | 
| 252 | 
            +
                        begin
         | 
| 253 | 
            +
                          expr = RubyExpr.parse(source)
         | 
| 254 | 
            +
                          validate_script_expression(erb_node, expr)
         | 
| 255 | 
            +
                        rescue RubyExpr::ParseError
         | 
| 246 256 | 
             
                        end
         | 
| 247 257 | 
             
                      end
         | 
| 248 258 | 
             
                    end
         | 
| 249 259 |  | 
| 250 | 
            -
                    def validate_script_expression( | 
| 260 | 
            +
                    def validate_script_expression(parent_node, expr)
         | 
| 251 261 | 
             
                      if expr.calls.empty?
         | 
| 252 262 | 
             
                        add_error(
         | 
| 253 263 | 
             
                          "erb interpolation in javascript tag must call '(...).to_json'",
         | 
| 254 | 
            -
                          location:  | 
| 264 | 
            +
                          location: parent_node.loc,
         | 
| 255 265 | 
             
                        )
         | 
| 256 266 | 
             
                        return
         | 
| 257 267 | 
             
                      end
         | 
| @@ -260,46 +270,49 @@ EOF | |
| 260 270 | 
             
                        if call.method == :raw
         | 
| 261 271 | 
             
                          call.arguments.each do |argument_node|
         | 
| 262 272 | 
             
                            arguments_expr = RubyExpr.new(argument_node)
         | 
| 263 | 
            -
                            validate_script_expression( | 
| 273 | 
            +
                            validate_script_expression(parent_node, arguments_expr)
         | 
| 264 274 | 
             
                          end
         | 
| 265 275 | 
             
                        elsif call.method == :html_safe
         | 
| 266 276 | 
             
                          instance_expr = RubyExpr.new(call.instance)
         | 
| 267 | 
            -
                          validate_script_expression( | 
| 268 | 
            -
                        elsif  | 
| 277 | 
            +
                          validate_script_expression(parent_node, instance_expr)
         | 
| 278 | 
            +
                        elsif !@config.javascript_safe_method?(call.method)
         | 
| 269 279 | 
             
                          add_error(
         | 
| 270 280 | 
             
                            "erb interpolation in javascript tag must call '(...).to_json'",
         | 
| 271 | 
            -
                            location:  | 
| 281 | 
            +
                            location: parent_node.loc,
         | 
| 272 282 | 
             
                          )
         | 
| 273 283 | 
             
                        end
         | 
| 274 284 | 
             
                      end
         | 
| 275 285 | 
             
                    end
         | 
| 276 286 |  | 
| 277 287 | 
             
                    def validate_no_statements(node)
         | 
| 278 | 
            -
                      node. | 
| 279 | 
            -
                         | 
| 280 | 
            -
             | 
| 281 | 
            -
             | 
| 282 | 
            -
             | 
| 283 | 
            -
             | 
| 284 | 
            -
             | 
| 288 | 
            +
                      erb_nodes(node).each do |erb_node, indicator_node, code_node|
         | 
| 289 | 
            +
                        next unless indicator_node.nil?
         | 
| 290 | 
            +
                        source = code_node.loc.source
         | 
| 291 | 
            +
                        next if /\A\s*end/m === source
         | 
| 292 | 
            +
             | 
| 293 | 
            +
                        add_error(
         | 
| 294 | 
            +
                          "erb statement not allowed here; did you mean '<%=' ?",
         | 
| 295 | 
            +
                          location: erb_node.loc,
         | 
| 296 | 
            +
                        )
         | 
| 285 297 | 
             
                      end
         | 
| 286 298 | 
             
                    end
         | 
| 287 299 |  | 
| 288 300 | 
             
                    def validate_no_javascript_tag(node)
         | 
| 289 | 
            -
                      node. | 
| 290 | 
            -
                         | 
| 291 | 
            -
                        if  | 
| 292 | 
            -
             | 
| 293 | 
            -
             | 
| 294 | 
            -
             | 
| 295 | 
            -
             | 
| 296 | 
            -
             | 
| 301 | 
            +
                      erb_nodes(node).each do |erb_node, indicator_node, code_node|
         | 
| 302 | 
            +
                        indicator = indicator_node&.loc&.source
         | 
| 303 | 
            +
                        next if indicator == '#'
         | 
| 304 | 
            +
                        source = code_node.loc.source
         | 
| 305 | 
            +
             | 
| 306 | 
            +
                        begin
         | 
| 307 | 
            +
                          expr = RubyExpr.parse(source)
         | 
| 308 | 
            +
             | 
| 297 309 | 
             
                          if expr.calls.size == 1 && expr.calls.first.method == :javascript_tag
         | 
| 298 310 | 
             
                            add_error(
         | 
| 299 311 | 
             
                              "'javascript_tag do' syntax is deprecated; use inline <script> instead",
         | 
| 300 | 
            -
                              location:  | 
| 312 | 
            +
                              location: erb_node.loc,
         | 
| 301 313 | 
             
                            )
         | 
| 302 314 | 
             
                          end
         | 
| 315 | 
            +
                        rescue RubyExpr::ParseError
         | 
| 303 316 | 
             
                        end
         | 
| 304 317 | 
             
                      end
         | 
| 305 318 | 
             
                    end
         |