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 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
|
require 'hamlit/parser/haml_helpers'
require 'hamlit/parser/haml_util'
require 'hamlit/parser/haml_compiler'
module Hamlit
# This class is used only internally. It holds the buffer of HTML that
# is eventually output as the resulting document.
# It's called from within the precompiled code,
# and helps reduce the amount of processing done within `instance_eval`ed code.
class HamlBuffer
ID_KEY = 'id'.freeze
CLASS_KEY = 'class'.freeze
DATA_KEY = 'data'.freeze
include ::Hamlit::HamlHelpers
include ::Hamlit::HamlUtil
# The string that holds the compiled HTML. This is aliased as
# `_erbout` for compatibility with ERB-specific code.
#
# @return [String]
attr_accessor :buffer
# The options hash passed in from {Haml::Engine}.
#
# @return [{String => Object}]
# @see Haml::Options#for_buffer
attr_accessor :options
# The {Buffer} for the enclosing Haml document.
# This is set for partials and similar sorts of nested templates.
# It's `nil` at the top level (see \{#toplevel?}).
#
# @return [Buffer]
attr_accessor :upper
# nil if there's no capture_haml block running,
# and the position at which it's beginning the capture if there is one.
#
# @return [Fixnum, nil]
attr_accessor :capture_position
# @return [Boolean]
# @see #active?
attr_writer :active
# @return [Boolean] Whether or not the format is XHTML
def xhtml?
not html?
end
# @return [Boolean] Whether or not the format is any flavor of HTML
def html?
html4? or html5?
end
# @return [Boolean] Whether or not the format is HTML4
def html4?
@options[:format] == :html4
end
# @return [Boolean] Whether or not the format is HTML5.
def html5?
@options[:format] == :html5
end
# @return [Boolean] Whether or not this buffer is a top-level template,
# as opposed to a nested partial
def toplevel?
upper.nil?
end
# Whether or not this buffer is currently being used to render a Haml template.
# Returns `false` if a subtemplate is being rendered,
# even if it's a subtemplate of this buffer's template.
#
# @return [Boolean]
def active?
@active
end
# @return [Fixnum] The current indentation level of the document
def tabulation
@real_tabs + @tabulation
end
# Sets the current tabulation of the document.
#
# @param val [Fixnum] The new tabulation
def tabulation=(val)
val = val - @real_tabs
@tabulation = val > -1 ? val : 0
end
# @param upper [Buffer] The parent buffer
# @param options [{Symbol => Object}] An options hash.
# See {Haml::Engine#options\_for\_buffer}
def initialize(upper = nil, options = {})
@active = true
@upper = upper
@options = options
@buffer = new_encoded_string
@tabulation = 0
# The number of tabs that Engine thinks we should have
# @real_tabs + @tabulation is the number of tabs actually output
@real_tabs = 0
end
# Appends text to the buffer, properly tabulated.
# Also modifies the document's indentation.
#
# @param text [String] The text to append
# @param tab_change [Fixnum] The number of tabs by which to increase
# or decrease the document's indentation
# @param dont_tab_up [Boolean] If true, don't indent the first line of `text`
def push_text(text, tab_change, dont_tab_up)
if @tabulation > 0
# Have to push every line in by the extra user set tabulation.
# Don't push lines with just whitespace, though,
# because that screws up precompiled indentation.
text.gsub!(/^(?!\s+$)/m, tabs)
text.sub!(tabs, '') if dont_tab_up
end
@real_tabs += tab_change
@buffer << text
end
# Modifies the indentation of the document.
#
# @param tab_change [Fixnum] The number of tabs by which to increase
# or decrease the document's indentation
def adjust_tabs(tab_change)
@real_tabs += tab_change
end
# the number of arguments here is insane, but passing in an options hash instead of named arguments
# causes a significant performance regression
def format_script(result, preserve_script, in_tag, preserve_tag, escape_html, nuke_inner_whitespace, interpolated, ugly)
result_name = escape_html ? html_escape(result.to_s) : result.to_s
if ugly
result = nuke_inner_whitespace ? result_name.strip : result_name
result = preserve(result, preserve_script, preserve_tag)
fix_textareas!(result) if toplevel? && result.include?('<textarea')
return result
end
# If we're interpolated,
# then the custom tabulation is handled in #push_text.
# The easiest way to avoid it here is to reset @tabulation.
if interpolated
old_tabulation = @tabulation
@tabulation = 0
end
in_tag_no_nuke = in_tag && !nuke_inner_whitespace
preserved_no_nuke = in_tag_no_nuke && preserve_tag
tabulation = !preserved_no_nuke && @real_tabs
result = nuke_inner_whitespace ? result_name.strip : result_name.rstrip
result = preserve(result, preserve_script, preserve_tag)
has_newline = !preserved_no_nuke && result.include?("\n")
if in_tag_no_nuke && (preserve_tag || !has_newline)
@real_tabs -= 1
@tabulation = old_tabulation if interpolated
return result
end
unless preserved_no_nuke
# Precompiled tabulation may be wrong
result = "#{tabs}#{result}" if !interpolated && !in_tag && @tabulation > 0
if has_newline
result.gsub! "\n", "\n#{tabs(tabulation)}"
# Add tabulation if it wasn't precompiled
result = "#{tabs(tabulation)}#{result}" if in_tag_no_nuke
end
fix_textareas!(result) if toplevel? && result.include?('<textarea')
if in_tag_no_nuke
result = "\n#{result}\n#{tabs(tabulation-1)}"
@real_tabs -= 1
end
@tabulation = old_tabulation if interpolated
result
end
end
def attributes(class_id, obj_ref, *attributes_hashes)
attributes = class_id
attributes_hashes.each do |old|
self.class.merge_attrs(attributes, Hash[old.map {|k, v| [k.to_s, v]}])
end
self.class.merge_attrs(attributes, parse_object_ref(obj_ref)) if obj_ref
::Hamlit::HamlCompiler.build_attributes(
html?, @options[:attr_wrapper], @options[:escape_attrs], @options[:hyphenate_data_attrs], attributes)
end
# Remove the whitespace from the right side of the buffer string.
# Doesn't do anything if we're at the beginning of a capture_haml block.
def rstrip!
if capture_position.nil?
buffer.rstrip!
return
end
buffer << buffer.slice!(capture_position..-1).rstrip
end
# Merges two attribute hashes.
# This is the same as `to.merge!(from)`,
# except that it merges id, class, and data attributes.
#
# ids are concatenated with `"_"`,
# and classes are concatenated with `" "`.
# data hashes are simply merged.
#
# Destructively modifies both `to` and `from`.
#
# @param to [{String => String}] The attribute hash to merge into
# @param from [{String => #to_s}] The attribute hash to merge from
# @return [{String => String}] `to`, after being merged
def self.merge_attrs(to, from)
from[ID_KEY] = ::Hamlit::HamlCompiler.filter_and_join(from[ID_KEY], '_') if from[ID_KEY]
if to[ID_KEY] && from[ID_KEY]
to[ID_KEY] << "_#{from.delete(ID_KEY)}"
elsif to[ID_KEY] || from[ID_KEY]
from[ID_KEY] ||= to[ID_KEY]
end
from[CLASS_KEY] = ::Hamlit::HamlCompiler.filter_and_join(from[CLASS_KEY], ' ') if from[CLASS_KEY]
if to[CLASS_KEY] && from[CLASS_KEY]
# Make sure we don't duplicate class names
from[CLASS_KEY] = (from[CLASS_KEY].to_s.split(' ') | to[CLASS_KEY].split(' ')).sort.join(' ')
elsif to[CLASS_KEY] || from[CLASS_KEY]
from[CLASS_KEY] ||= to[CLASS_KEY]
end
from.keys.each do |key|
next unless from[key].kind_of?(Hash) || to[key].kind_of?(Hash)
from_data = from.delete(key)
# forces to_data & from_data into a hash
from_data = { nil => from_data } if from_data && !from_data.is_a?(Hash)
to[key] = { nil => to[key] } if to[key] && !to[key].is_a?(Hash)
if from_data && !to[key]
to[key] = from_data
elsif from_data && to[key]
to[key].merge! from_data
end
end
to.merge!(from)
end
private
def preserve(result, preserve_script, preserve_tag)
return ::Hamlit::HamlHelpers.preserve(result) if preserve_tag
return ::Hamlit::HamlHelpers.find_and_preserve(result, options[:preserve]) if preserve_script
result
end
# Works like #{find_and_preserve}, but allows the first newline after a
# preserved opening tag to remain unencoded, and then outdents the content.
# This change was motivated primarily by the change in Rails 3.2.3 to emit
# a newline after textarea helpers.
#
# @param input [String] The text to process
# @since Haml 4.0.1
# @private
def fix_textareas!(input)
pattern = /<(textarea)([^>]*)>(\n|
)(.*?)<\/textarea>/im
input.gsub!(pattern) do |s|
match = pattern.match(s)
content = match[4]
if match[3] == '
'
content.sub!(/\A /, ' ')
else
content.sub!(/\A[ ]*/, '')
end
"<#{match[1]}#{match[2]}>\n#{content}</#{match[1]}>"
end
end
def new_encoded_string
"".encode(Encoding.find(options[:encoding]))
end
@@tab_cache = {}
# Gets `count` tabs. Mostly for internal use.
def tabs(count = 0)
tabs = [count + @tabulation, 0].max
@@tab_cache[tabs] ||= ' ' * tabs
end
# Takes an array of objects and uses the class and id of the first
# one to create an attributes hash.
# The second object, if present, is used as a prefix,
# just like you can do with `dom_id()` and `dom_class()` in Rails
def parse_object_ref(ref)
prefix = ref[1]
ref = ref[0]
# Let's make sure the value isn't nil. If it is, return the default Hash.
return {} if ref.nil?
class_name =
if ref.respond_to?(:haml_object_ref)
ref.haml_object_ref
else
underscore(ref.class)
end
ref_id =
if ref.respond_to?(:to_key)
key = ref.to_key
key.join('_') unless key.nil?
else
ref.id
end
id = "#{class_name}_#{ref_id || 'new'}"
if prefix
class_name = "#{ prefix }_#{ class_name}"
id = "#{ prefix }_#{ id }"
end
{ID_KEY => id, CLASS_KEY => class_name}
end
# Changes a word from camel case to underscores.
# Based on the method of the same name in Rails' Inflector,
# but copied here so it'll run properly without Rails.
def underscore(camel_cased_word)
word = camel_cased_word.to_s.dup
word.gsub!(/::/, '_')
word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
word.tr!('-', '_')
word.downcase!
word
end
end
end
|