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 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
|
# frozen_string_literal: true
begin
require 'erubis/tiny'
rescue LoadError
require 'erb'
end
require 'set'
require 'stringio'
require 'strscan'
module Hamlit
# A module containing various useful functions.
module HamlUtil
extend self
# Silence all output to STDERR within a block.
#
# @yield A block in which no output will be printed to STDERR
def silence_warnings
the_real_stderr, $stderr = $stderr, StringIO.new
yield
ensure
$stderr = the_real_stderr
end
## Rails XSS Safety
# Whether or not ActionView's XSS protection is available and enabled,
# as is the default for Rails 3.0+, and optional for version 2.3.5+.
# Overridden in haml/template.rb if this is the case.
#
# @return [Boolean]
def rails_xss_safe?
false
end
# Returns the given text, marked as being HTML-safe.
# With older versions of the Rails XSS-safety mechanism,
# this destructively modifies the HTML-safety of `text`.
#
# It only works if you are using ActiveSupport or the parameter `text`
# implements the #html_safe method.
#
# @param text [String, nil]
# @return [String, nil] `text`, marked as HTML-safe
def html_safe(text)
return unless text
text.html_safe
end
# Checks that the encoding of a string is valid
# and cleans up potential encoding gotchas like the UTF-8 BOM.
# If it's not, yields an error string describing the invalid character
# and the line on which it occurs.
#
# @param str [String] The string of which to check the encoding
# @yield [msg] A block in which an encoding error can be raised.
# Only yields if there is an encoding error
# @yieldparam msg [String] The error message to be raised
# @return [String] `str`, potentially with encoding gotchas like BOMs removed
def check_encoding(str)
if str.valid_encoding?
# Get rid of the Unicode BOM if possible
# Shortcut for UTF-8 which might be the majority case
if str.encoding == Encoding::UTF_8
return str.gsub(/\A\uFEFF/, '')
elsif str.encoding.name =~ /^UTF-(16|32)(BE|LE)?$/
return str.gsub(Regexp.new("\\A\uFEFF".encode(str.encoding)), '')
else
return str
end
end
encoding = str.encoding
newlines = Regexp.new("\r\n|\r|\n".encode(encoding).force_encoding(Encoding::ASCII_8BIT))
str.force_encoding(Encoding::ASCII_8BIT).split(newlines).each_with_index do |line, i|
begin
line.encode(encoding)
rescue Encoding::UndefinedConversionError => e
yield <<MSG.rstrip, i + 1
Invalid #{encoding.name} character #{e.error_char.dump}
MSG
end
end
return str
end
# Like {\#check\_encoding}, but also checks for a Ruby-style `-# coding:` comment
# at the beginning of the template and uses that encoding if it exists.
#
# The Haml encoding rules are simple.
# If a `-# coding:` comment exists,
# we assume that that's the original encoding of the document.
# Otherwise, we use whatever encoding Ruby has.
#
# Haml uses the same rules for parsing coding comments as Ruby.
# This means that it can understand Emacs-style comments
# (e.g. `-*- encoding: "utf-8" -*-`),
# and also that it cannot understand non-ASCII-compatible encodings
# such as `UTF-16` and `UTF-32`.
#
# @param str [String] The Haml template of which to check the encoding
# @yield [msg] A block in which an encoding error can be raised.
# Only yields if there is an encoding error
# @yieldparam msg [String] The error message to be raised
# @return [String] The original string encoded properly
# @raise [ArgumentError] if the document declares an unknown encoding
def check_haml_encoding(str, &block)
str = str.dup if str.frozen?
bom, encoding = parse_haml_magic_comment(str)
if encoding; str.force_encoding(encoding)
elsif bom; str.force_encoding(Encoding::UTF_8)
end
return check_encoding(str, &block)
end
# Like `Object#inspect`, but preserves non-ASCII characters rather than escaping them.
# This is necessary so that the precompiled Haml template can be `#encode`d into `@options[:encoding]`
# before being evaluated.
#
# @param obj {Object}
# @return {String}
def inspect_obj(obj)
case obj
when String
%Q!"#{obj.gsub(/[\x00-\x7F]+/) {|s| s.dump[1...-1]}}"!
when Symbol
":#{inspect_obj(obj.to_s)}"
else
obj.inspect
end
end
# Scans through a string looking for the interoplation-opening `#{`
# and, when it's found, yields the scanner to the calling code
# so it can handle it properly.
#
# The scanner will have any backslashes immediately in front of the `#{`
# as the second capture group (`scan[2]`),
# and the text prior to that as the first (`scan[1]`).
#
# @yieldparam scan [StringScanner] The scanner scanning through the string
# @return [String] The text remaining in the scanner after all `#{`s have been processed
def handle_interpolation(str)
scan = StringScanner.new(str)
yield scan while scan.scan(/(.*?)(\\*)#([\{@$])/)
scan.rest
end
# Moves a scanner through a balanced pair of characters.
# For example:
#
# Foo (Bar (Baz bang) bop) (Bang (bop bip))
# ^ ^
# from to
#
# @param scanner [StringScanner] The string scanner to move
# @param start [String] The character opening the balanced pair.
# @param finish [String] The character closing the balanced pair.
# @param count [Fixnum] The number of opening characters matched
# before calling this method
# @return [(String, String)] The string matched within the balanced pair
# and the rest of the string.
# `["Foo (Bar (Baz bang) bop)", " (Bang (bop bip))"]` in the example above.
def balance(scanner, start, finish, count = 0)
str = ''.dup
scanner = StringScanner.new(scanner) unless scanner.is_a? StringScanner
regexp = Regexp.new("(.*?)[\\#{start.chr}\\#{finish.chr}]", Regexp::MULTILINE)
while scanner.scan(regexp)
str << scanner.matched
count += 1 if scanner.matched[-1] == start
count -= 1 if scanner.matched[-1] == finish
return [str.strip, scanner.rest] if count == 0
end
end
# Formats a string for use in error messages about indentation.
#
# @param indentation [String] The string used for indentation
# @return [String] The name of the indentation (e.g. `"12 spaces"`, `"1 tab"`)
def human_indentation(indentation)
if !indentation.include?(?\t)
noun = 'space'
elsif !indentation.include?(?\s)
noun = 'tab'
else
return indentation.inspect
end
singular = indentation.length == 1
"#{indentation.length} #{noun}#{'s' unless singular}"
end
def contains_interpolation?(str)
/#[\{$@]/ === str
end
def unescape_interpolation(str, escape_html = nil)
res = ''.dup
rest = Hamlit::HamlUtil.handle_interpolation str.dump do |scan|
escapes = (scan[2].size - 1) / 2
char = scan[3] # '{', '@' or '$'
res << scan.matched[0...-3 - escapes]
if escapes % 2 == 1
res << "\##{char}"
else
interpolated = if char == '{'
balance(scan, ?{, ?}, 1)[0][0...-1]
else
scan.scan(/\w+/)
end
content = eval("\"#{interpolated}\"")
content = "#{char}#{content}" if char == '@' || char == '$'
content = "Hamlit::HamlHelpers.html_escape((#{content}))" if escape_html
res << "\#{#{content}}"
end
end
res + rest
end
private
# Parses a magic comment at the beginning of a Haml file.
# The parsing rules are basically the same as Ruby's.
#
# @return [(Boolean, String or nil)]
# Whether the document begins with a UTF-8 BOM,
# and the declared encoding of the document (or nil if none is declared)
def parse_haml_magic_comment(str)
scanner = StringScanner.new(str.dup.force_encoding(Encoding::ASCII_8BIT))
bom = scanner.scan(/\xEF\xBB\xBF/n)
return bom unless scanner.scan(/-\s*#\s*/n)
if (coding = try_parse_haml_emacs_magic_comment(scanner))
return bom, coding
end
return bom unless scanner.scan(/.*?coding[=:]\s*([\w-]+)/in)
return bom, scanner[1]
end
def try_parse_haml_emacs_magic_comment(scanner)
pos = scanner.pos
return unless scanner.scan(/.*?-\*-\s*/n)
# From Ruby's parse.y
return unless scanner.scan(/([^\s'":;]+)\s*:\s*("(?:\\.|[^"])*"|[^"\s;]+?)[\s;]*-\*-/n)
name, val = scanner[1], scanner[2]
return unless name =~ /(en)?coding/in
val = $1 if val =~ /^"(.*)"$/n
return val
ensure
scanner.pos = pos
end
end
end
|