class ActionView::Template::Handlers::ERB
def self.call(template, source)
def self.call(template, source) new.call(template, source) end
def call(template, source)
def call(template, source) # First, convert to BINARY, so in case the encoding is # wrong, we can still find an encoding tag # (<%# encoding %>) inside the String using a regular # expression template_source = source.b erb = template_source.gsub(ENCODING_TAG, "") encoding = $2 erb.force_encoding valid_encoding(source.dup, encoding) # Always make sure we return a String in the default_internal erb.encode! # Strip trailing newlines from the template if enabled erb.chomp! if strip_trailing_newlines options = { escape: (self.class.escape_ignore_list.include? template.type), trim: (self.class.erb_trim_mode == "-") } if ActionView::Base.annotate_rendered_view_with_filenames && template.format == :html options[:preamble] = "@output_buffer.safe_append='<!-- BEGIN #{template.short_identifier}\n-->';" options[:postamble] = "@output_buffer.safe_append='<!-- END #{template.short_identifier} -->';@output_buffer" end self.class.erb_implementation.new(erb, options).src end
def find_lineno_offset(compiled, source_lines, highlight, error_lineno)
Use the difference between the compiled and source sizes to
The compiled template is likely to be longer than the source.
chance of finding the correct line
Searches in reverse from the backtrace lineno so we have a better
Return the offset between the error lineno and the source lineno.
def find_lineno_offset(compiled, source_lines, highlight, error_lineno) first_index = error_lineno - 1 - compiled.size + source_lines.size first_index = 0 if first_index < 0 last_index = error_lineno - 1 last_index = source_lines.size - 1 if last_index >= source_lines.size last_index.downto(first_index) do |line_index| next unless source_lines[line_index].include?(highlight) return error_lineno - 1 - line_index end raise LocationParsingError, "Couldn't find code snippet" end
def find_offset(compiled, source_tokens, error_column)
the current token from looping past the next token if they both
a match for the current token has been found. This is to prevent
Prioritize matching the next token over the current token once
c. Otherwise: Advance 1 byte
b. If A: test error_column or advance scanner.
a. If B: start over with next token set (B, C).
2. Find a match for B or A:
1. Find a match for A: test error_column or advance scanner.
For example, if we want to find tokens A, B, C, we do the following:
a match of the first token before matching either token.
Iterate consecutive pairs of CODE or TEXT tokens, requiring
original source template.
contains the error_column, then return the offset compared to the
Find which token in the source template spans the byte range that
def find_offset(compiled, source_tokens, error_column) compiled = StringScanner.new(compiled) offset_source_tokens(source_tokens).each_cons(2) do |(name, str, offset), (_, next_str, _)| matched_str = false until compiled.eos? if matched_str && next_str && compiled.match?(next_str) break elsif compiled.match?(str) matched_str = true if name == :CODE && compiled.pos <= error_column && compiled.pos + str.bytesize >= error_column return compiled.pos - offset end compiled.pos += str.bytesize else compiled.pos += 1 end end end raise LocationParsingError, "Couldn't find code snippet" end
def handles_encoding?
def handles_encoding? true end
def offset_source_tokens(source_tokens)
def offset_source_tokens(source_tokens) source_offset = 0 with_offset = source_tokens.filter_map do |name, str| result = [:CODE, str, source_offset] if name == :CODE || name == :PLAIN result = [:TEXT, str, source_offset] if name == :TEXT source_offset += str.bytesize result end with_offset << [:EOS, nil, source_offset] end
def supports_streaming?
def supports_streaming? true end
def translate_location(spot, _backtrace_location, source)
Translate an error location returned by ErrorHighlight to the correct
def translate_location(spot, _backtrace_location, source) compiled = spot[:script_lines] highlight = compiled[spot[:first_lineno] - 1]&.byteslice((spot[:first_column] - 1)...spot[:last_column]) return nil if highlight.blank? source_lines = source.lines lineno_delta = find_lineno_offset(compiled, source_lines, highlight, spot[:first_lineno]) tokens = ::ERB::Util.tokenize(source_lines[spot[:first_lineno] - lineno_delta - 1]) column_delta = find_offset(spot[:snippet], tokens, spot[:first_column]) spot[:first_lineno] -= lineno_delta spot[:last_lineno] -= lineno_delta spot[:first_column] -= column_delta spot[:last_column] -= column_delta spot[:script_lines] = source_lines spot rescue NotImplementedError, LocationParsingError nil end
def valid_encoding(string, encoding)
def valid_encoding(string, encoding) # If a magic encoding comment was found, tag the # String with this encoding. This is for a case # where the original String was assumed to be, # for instance, UTF-8, but a magic comment # proved otherwise string.force_encoding(encoding) if encoding # If the String is valid, return the encoding we found return string.encoding if string.valid_encoding? # Otherwise, raise an exception raise WrongEncodingError.new(string, string.encoding) end