File: string_splitter.rb

package info (click to toggle)
ruby-haml 6.3.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,004 kB
  • sloc: ruby: 9,908; sh: 23; makefile: 11
file content (140 lines) | stat: -rw-r--r-- 3,781 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
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
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# frozen_string_literal: true
begin
  require 'ripper'
rescue LoadError
end

module Haml
  # Compile [:dynamic, "foo#{bar}"] to [:multi, [:static, 'foo'], [:dynamic, 'bar']]
  class StringSplitter < Temple::Filter
    if defined?(Ripper) && RUBY_VERSION >= "2.0.0" && Ripper.respond_to?(:lex)
      class << self
        # `code` param must be valid string literal
        def compile(code)
          [].tap do |exps|
            tokens = Ripper.lex(code.strip)
            tokens.pop while tokens.last && [:on_comment, :on_sp].include?(tokens.last[1])

            if tokens.size < 2
              raise(Haml::InternalError, "Expected token size >= 2 but got: #{tokens.size}")
            end
            compile_tokens!(exps, tokens)
          end
        end

        private

        def strip_quotes!(tokens)
          _, type, beg_str = tokens.shift
          if type != :on_tstring_beg
            raise(Haml::InternalError, "Expected :on_tstring_beg but got: #{type}")
          end

          _, type, end_str = tokens.pop
          if type != :on_tstring_end
            raise(Haml::InternalError, "Expected :on_tstring_end but got: #{type}")
          end

          [beg_str, end_str]
        end

        def compile_tokens!(exps, tokens)
          beg_str, end_str = strip_quotes!(tokens)

          until tokens.empty?
            _, type, str = tokens.shift

            case type
            when :on_tstring_content
              beg_str, end_str = escape_quotes(beg_str, end_str)
              exps << [:static, eval("#{beg_str}#{str}#{end_str}").to_s]
            when :on_embexpr_beg
              embedded = shift_balanced_embexpr(tokens)
              exps << [:dynamic, embedded] unless embedded.empty?
            end
          end
        end

        # Some quotes are split-unsafe. Replace such quotes with null characters.
        def escape_quotes(beg_str, end_str)
          case [beg_str[-1], end_str]
          when ['(', ')'], ['[', ']'], ['{', '}']
            [beg_str.sub(/.\z/) { "\0" }, "\0"]
          else
            [beg_str, end_str]
          end
        end

        def shift_balanced_embexpr(tokens)
          String.new.tap do |embedded|
            embexpr_open = 1

            until tokens.empty?
              _, type, str = tokens.shift
              case type
              when :on_embexpr_beg
                embexpr_open += 1
              when :on_embexpr_end
                embexpr_open -= 1
                break if embexpr_open == 0
              end

              embedded << str
            end
          end
        end
      end

      def on_dynamic(code)
        return [:dynamic, code] unless string_literal?(code)
        return [:dynamic, code] if code.include?("\n")

        temple = [:multi]
        StringSplitter.compile(code).each do |type, content|
          case type
          when :static
            temple << [:static, content]
          when :dynamic
            temple << on_dynamic(content)
          end
        end
        temple
      end

      private

      def string_literal?(code)
        return false if SyntaxChecker.syntax_error?(code)

        type, instructions = Ripper.sexp(code)
        return false if type != :program
        return false if instructions.size > 1

        type, _ = instructions.first
        type == :string_literal
      end

      class SyntaxChecker < Ripper
        class ParseError < StandardError; end

        def self.syntax_error?(code)
          self.new(code).parse
          false
        rescue ParseError
          true
        end

        private

        def on_parse_error(*)
          raise ParseError
        end
      end
    else
      # Do nothing if ripper is unavailable
      def call(ast)
        ast
      end
    end
  end
end