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
|