lib/tilt/template.rb in tilt-1.3.7 vs lib/tilt/template.rb in tilt-1.4.0

- old
+ new

@@ -63,15 +63,41 @@ # used on 1.9 to set the encoding if it is not set elsewhere (like a magic comment) # currently only used if template compiles to ruby @default_encoding = @options.delete :default_encoding # load template data and prepare (uses binread to avoid encoding issues) - @reader = block || lambda { |t| File.respond_to?(:binread) ? File.binread(@file) : File.read(@file) } + @reader = block || lambda { |t| read_template_file } @data = @reader.call(self) + + if @data.respond_to?(:force_encoding) + @data.force_encoding(default_encoding) if default_encoding + + if !@data.valid_encoding? + raise Encoding::InvalidByteSequenceError, "#{eval_file} is not valid #{@data.encoding}" + end + end + prepare end + # The encoding of the source data. Defaults to the + # default_encoding-option if present. You may override this method + # in your template class if you have a better hint of the data's + # encoding. + def default_encoding + @default_encoding + end + + def read_template_file + data = File.open(file, 'rb') { |io| io.read } + if data.respond_to?(:force_encoding) + # Set it to the default external (without verifying) + data.force_encoding(Encoding.default_external) if Encoding.default_external + end + data + end + # Render the template in the given scope with the locals specified. If a # block is given, it is typically available within the template via # +yield+. def render(scope=Object.new, locals={}, &block) evaluate scope, locals || {}, &block @@ -154,30 +180,33 @@ # offset. In most cases, overriding the #precompiled_template method is # easier and more appropriate. def precompiled(locals) preamble = precompiled_preamble(locals) template = precompiled_template(locals) - magic_comment = extract_magic_comment(template) - if magic_comment - # Magic comment e.g. "# coding: utf-8" has to be in the first line. - # So we copy the magic comment to the first line. - preamble = magic_comment + "\n" + preamble + postamble = precompiled_postamble(locals) + source = '' + + # Ensure that our generated source code has the same encoding as the + # the source code generated by the template engine. + if source.respond_to?(:force_encoding) + template_encoding = extract_encoding(template) + + source.force_encoding(template_encoding) + template.force_encoding(template_encoding) end - parts = [ - preamble, - template, - precompiled_postamble(locals) - ] - [parts.join("\n"), preamble.count("\n") + 1] + + source << preamble << "\n" << template << "\n" << postamble + + [source, preamble.count("\n")+1] end # A string containing the (Ruby) source code for the template. The - # default Template#evaluate implementation requires either this method - # or the #precompiled method be overridden. When defined, the base - # Template guarantees correct file/line handling, locals support, custom - # scopes, and support for template compilation when the scope object - # allows it. + # default Template#evaluate implementation requires either this + # method or the #precompiled method be overridden. When defined, + # the base Template guarantees correct file/line handling, locals + # support, custom scopes, proper encoding, and support for template + # compilation. def precompiled_template(locals) raise NotImplementedError end # Generates preamble code for initializing template state, and performing @@ -210,12 +239,17 @@ private def compile_template_method(locals) source, offset = precompiled(locals) method_name = "__tilt_#{Thread.current.object_id.abs}" - method_source = <<-RUBY - #{extract_magic_comment source} + method_source = "" + + if method_source.respond_to?(:force_encoding) + method_source.force_encoding(source.encoding) + end + + method_source << <<-RUBY TOPOBJECT.class_eval do def #{method_name}(locals) Thread.current[:tilt_vars] = [self, locals] class << self this, locals = Thread.current[:tilt_vars] @@ -232,15 +266,24 @@ method = TOPOBJECT.instance_method(method_name) TOPOBJECT.class_eval { remove_method(method_name) } method end + def extract_encoding(script) + extract_magic_comment(script) || script.encoding + end + def extract_magic_comment(script) - comment = script.slice(/\A[ \t]*\#.*coding\s*[=:]\s*([[:alnum:]\-_]+).*$/) - if comment && !%w[ascii-8bit binary].include?($1.downcase) - comment - elsif @default_encoding - "# coding: #{@default_encoding}" + binary script do + script[/\A[ \t]*\#.*coding\s*[=:]\s*([[:alnum:]\-_]+).*$/n, 1] end + end + + def binary(string) + original_encoding = string.encoding + string.force_encoding(Encoding::BINARY) + yield + ensure + string.force_encoding(original_encoding) end end end