File: string_splitter.rb

package info (click to toggle)
ruby-temple 0.10.4-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 476 kB
  • sloc: ruby: 3,347; makefile: 6
file content (142 lines) | stat: -rw-r--r-- 3,967 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
141
142
# frozen_string_literal: true
begin
  require 'ripper'
rescue LoadError
end

module Temple
  module Filters
    # Compile [:dynamic, "foo#{bar}"] to [:multi, [:static, 'foo'], [:dynamic, 'bar']]
    class StringSplitter < Filter
      if defined?(Ripper) && 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(FilterError, "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(FilterError, "Expected :on_tstring_beg but got: #{type}")
            end

            _, type, end_str = tokens.pop
            if type != :on_tstring_end
              raise(FilterError, "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
end