mirror of
https://github.com/haml/haml.git
synced 2022-11-09 12:33:31 -05:00
e3bd326b3d
Close https://github.com/haml/haml/issues/1096 Applying https://github.com/judofyr/temple/pull/138 to Haml's fork until it's merged to upstream.
140 lines
3.7 KiB
Ruby
140 lines
3.7 KiB
Ruby
# 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
|