haml 6.0.0.beta.1-java
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/FUNDING.yml +1 -0
- data/.github/workflows/test.yml +40 -0
- data/.gitignore +19 -0
- data/CHANGELOG.md +1515 -0
- data/FAQ.md +147 -0
- data/Gemfile +23 -0
- data/MIT-LICENSE +20 -0
- data/README.md +210 -0
- data/REFERENCE.md +1380 -0
- data/Rakefile +116 -0
- data/bin/bench +66 -0
- data/bin/console +11 -0
- data/bin/ruby +3 -0
- data/bin/setup +7 -0
- data/bin/stackprof +27 -0
- data/bin/test +24 -0
- data/exe/haml +6 -0
- data/ext/haml/extconf.rb +10 -0
- data/ext/haml/haml.c +537 -0
- data/ext/haml/hescape.c +108 -0
- data/ext/haml/hescape.h +20 -0
- data/haml.gemspec +47 -0
- data/lib/haml/ambles.rb +20 -0
- data/lib/haml/attribute_builder.rb +175 -0
- data/lib/haml/attribute_compiler.rb +128 -0
- data/lib/haml/attribute_parser.rb +110 -0
- data/lib/haml/cli.rb +154 -0
- data/lib/haml/compiler/children_compiler.rb +126 -0
- data/lib/haml/compiler/comment_compiler.rb +39 -0
- data/lib/haml/compiler/doctype_compiler.rb +46 -0
- data/lib/haml/compiler/script_compiler.rb +116 -0
- data/lib/haml/compiler/silent_script_compiler.rb +24 -0
- data/lib/haml/compiler/tag_compiler.rb +76 -0
- data/lib/haml/compiler.rb +97 -0
- data/lib/haml/dynamic_merger.rb +67 -0
- data/lib/haml/engine.rb +53 -0
- data/lib/haml/error.rb +16 -0
- data/lib/haml/escapable.rb +13 -0
- data/lib/haml/filters/base.rb +12 -0
- data/lib/haml/filters/cdata.rb +20 -0
- data/lib/haml/filters/coffee.rb +17 -0
- data/lib/haml/filters/css.rb +33 -0
- data/lib/haml/filters/erb.rb +10 -0
- data/lib/haml/filters/escaped.rb +22 -0
- data/lib/haml/filters/javascript.rb +33 -0
- data/lib/haml/filters/less.rb +20 -0
- data/lib/haml/filters/markdown.rb +11 -0
- data/lib/haml/filters/plain.rb +29 -0
- data/lib/haml/filters/preserve.rb +22 -0
- data/lib/haml/filters/ruby.rb +10 -0
- data/lib/haml/filters/sass.rb +15 -0
- data/lib/haml/filters/scss.rb +15 -0
- data/lib/haml/filters/text_base.rb +25 -0
- data/lib/haml/filters/tilt_base.rb +49 -0
- data/lib/haml/filters.rb +75 -0
- data/lib/haml/force_escapable.rb +29 -0
- data/lib/haml/haml_error.rb +66 -0
- data/lib/haml/helpers.rb +15 -0
- data/lib/haml/html.rb +22 -0
- data/lib/haml/identity.rb +13 -0
- data/lib/haml/object_ref.rb +30 -0
- data/lib/haml/parser.rb +986 -0
- data/lib/haml/rails_helpers.rb +51 -0
- data/lib/haml/rails_template.rb +55 -0
- data/lib/haml/railtie.rb +15 -0
- data/lib/haml/ruby_expression.rb +32 -0
- data/lib/haml/string_splitter.rb +20 -0
- data/lib/haml/template.rb +20 -0
- data/lib/haml/temple_line_counter.rb +31 -0
- data/lib/haml/util.rb +260 -0
- data/lib/haml/version.rb +4 -0
- data/lib/haml.rb +13 -0
- metadata +359 -0
data/lib/haml/parser.rb
ADDED
@@ -0,0 +1,986 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ripper'
|
4
|
+
require 'strscan'
|
5
|
+
require 'haml/haml_error'
|
6
|
+
require 'haml/util'
|
7
|
+
|
8
|
+
module Haml
|
9
|
+
class Parser
|
10
|
+
include Haml::Util
|
11
|
+
|
12
|
+
attr_reader :root
|
13
|
+
|
14
|
+
# Designates an XHTML/XML element.
|
15
|
+
ELEMENT = ?%
|
16
|
+
|
17
|
+
# Designates a `<div>` element with the given class.
|
18
|
+
DIV_CLASS = ?.
|
19
|
+
|
20
|
+
# Designates a `<div>` element with the given id.
|
21
|
+
DIV_ID = ?#
|
22
|
+
|
23
|
+
# Designates an XHTML/XML comment.
|
24
|
+
COMMENT = ?/
|
25
|
+
|
26
|
+
# Designates an XHTML doctype or script that is never HTML-escaped.
|
27
|
+
DOCTYPE = ?!
|
28
|
+
|
29
|
+
# Designates script, the result of which is output.
|
30
|
+
SCRIPT = ?=
|
31
|
+
|
32
|
+
# Designates script that is always HTML-escaped.
|
33
|
+
SANITIZE = ?&
|
34
|
+
|
35
|
+
# Designates script, the result of which is flattened and output.
|
36
|
+
FLAT_SCRIPT = ?~
|
37
|
+
|
38
|
+
# Designates script which is run but not output.
|
39
|
+
SILENT_SCRIPT = ?-
|
40
|
+
|
41
|
+
# When following SILENT_SCRIPT, designates a comment that is not output.
|
42
|
+
SILENT_COMMENT = ?#
|
43
|
+
|
44
|
+
# Designates a non-parsed line.
|
45
|
+
ESCAPE = ?\\
|
46
|
+
|
47
|
+
# Designates a block of filtered text.
|
48
|
+
FILTER = ?:
|
49
|
+
|
50
|
+
# Designates a non-parsed line. Not actually a character.
|
51
|
+
PLAIN_TEXT = -1
|
52
|
+
|
53
|
+
# Keeps track of the ASCII values of the characters that begin a
|
54
|
+
# specially-interpreted line.
|
55
|
+
SPECIAL_CHARACTERS = [
|
56
|
+
ELEMENT,
|
57
|
+
DIV_CLASS,
|
58
|
+
DIV_ID,
|
59
|
+
COMMENT,
|
60
|
+
DOCTYPE,
|
61
|
+
SCRIPT,
|
62
|
+
SANITIZE,
|
63
|
+
FLAT_SCRIPT,
|
64
|
+
SILENT_SCRIPT,
|
65
|
+
ESCAPE,
|
66
|
+
FILTER
|
67
|
+
].freeze
|
68
|
+
|
69
|
+
# The value of the character that designates that a line is part
|
70
|
+
# of a multiline string.
|
71
|
+
MULTILINE_CHAR_VALUE = ?|
|
72
|
+
|
73
|
+
# Regex to check for blocks with spaces around arguments. Not to be confused
|
74
|
+
# with multiline script.
|
75
|
+
# For example:
|
76
|
+
# foo.each do | bar |
|
77
|
+
# = bar
|
78
|
+
#
|
79
|
+
BLOCK_WITH_SPACES = /do\s*\|\s*[^\|]*\s+\|\z/
|
80
|
+
|
81
|
+
MID_BLOCK_KEYWORDS = %w[else elsif rescue ensure end when].freeze
|
82
|
+
START_BLOCK_KEYWORDS = %w[if begin case unless].freeze
|
83
|
+
# Try to parse assignments to block starters as best as possible
|
84
|
+
START_BLOCK_KEYWORD_REGEX = /(?:\w+(?:,\s*\w+)*\s*=\s*)?(#{START_BLOCK_KEYWORDS.join('|')})/
|
85
|
+
BLOCK_KEYWORD_REGEX = /^-?\s*(?:(#{MID_BLOCK_KEYWORDS.join('|')})|#{START_BLOCK_KEYWORD_REGEX.source})\b/
|
86
|
+
|
87
|
+
# The Regex that matches a Doctype command.
|
88
|
+
DOCTYPE_REGEX = /(\d(?:\.\d)?)?\s*([a-z]*)\s*([^ ]+)?/i
|
89
|
+
|
90
|
+
# The Regex that matches a literal string or symbol value
|
91
|
+
LITERAL_VALUE_REGEX = /:(\w*)|(["'])((?!\\|\#\{|\#@|\#\$|\2).|\\.)*\2/
|
92
|
+
|
93
|
+
ID_KEY = 'id'.freeze
|
94
|
+
CLASS_KEY = 'class'.freeze
|
95
|
+
|
96
|
+
# Used for scanning old attributes, substituting the first '{'
|
97
|
+
METHOD_CALL_PREFIX = 'a('
|
98
|
+
|
99
|
+
def initialize(options)
|
100
|
+
@options = ParserOptions.new(options)
|
101
|
+
# Record the indent levels of "if" statements to validate the subsequent
|
102
|
+
# elsif and else statements are indented at the appropriate level.
|
103
|
+
@script_level_stack = []
|
104
|
+
@template_index = 0
|
105
|
+
@template_tabs = 0
|
106
|
+
end
|
107
|
+
|
108
|
+
def call(template)
|
109
|
+
template = Haml::Util.check_haml_encoding(template) do |msg, line|
|
110
|
+
raise Haml::Error.new(msg, line)
|
111
|
+
end
|
112
|
+
|
113
|
+
match = template.rstrip.scan(/(([ \t]+)?(.*?))(?:\Z|\r\n|\r|\n)/m)
|
114
|
+
# discard the last match which is always blank
|
115
|
+
match.pop
|
116
|
+
@template = match.each_with_index.map do |(full, whitespace, text), index|
|
117
|
+
Line.new(whitespace, text.rstrip, full, index, self, false)
|
118
|
+
end
|
119
|
+
# Append special end-of-document marker
|
120
|
+
@template << Line.new(nil, '-#', '-#', @template.size, self, true)
|
121
|
+
|
122
|
+
@root = @parent = ParseNode.new(:root)
|
123
|
+
@flat = false
|
124
|
+
@filter_buffer = nil
|
125
|
+
@indentation = nil
|
126
|
+
@line = next_line
|
127
|
+
|
128
|
+
raise HamlSyntaxError.new(HamlError.message(:indenting_at_start), @line.index) if @line.tabs != 0
|
129
|
+
|
130
|
+
loop do
|
131
|
+
next_line
|
132
|
+
|
133
|
+
process_indent(@line) unless @line.text.empty?
|
134
|
+
|
135
|
+
if flat?
|
136
|
+
text = @line.full.dup
|
137
|
+
text = "" unless text.gsub!(/^#{@flat_spaces}/, '')
|
138
|
+
@filter_buffer << "#{text}\n"
|
139
|
+
@line = @next_line
|
140
|
+
next
|
141
|
+
end
|
142
|
+
|
143
|
+
@tab_up = nil
|
144
|
+
process_line(@line) unless @line.text.empty?
|
145
|
+
if block_opened? || @tab_up
|
146
|
+
@template_tabs += 1
|
147
|
+
@parent = @parent.children.last
|
148
|
+
end
|
149
|
+
|
150
|
+
if !flat? && @next_line.tabs - @line.tabs > 1
|
151
|
+
raise HamlSyntaxError.new(HamlError.message(:deeper_indenting, @next_line.tabs - @line.tabs), @next_line.index)
|
152
|
+
end
|
153
|
+
|
154
|
+
@line = @next_line
|
155
|
+
end
|
156
|
+
# Close all the open tags
|
157
|
+
close until @parent.type == :root
|
158
|
+
@root
|
159
|
+
rescue Haml::HamlError => e
|
160
|
+
e.backtrace.unshift "#{@options.filename}:#{(e.line ? e.line + 1 : @line.index + 1) + @options.line - 1}"
|
161
|
+
error_with_lineno(e)
|
162
|
+
end
|
163
|
+
|
164
|
+
def compute_tabs(line)
|
165
|
+
return 0 if line.text.empty? || !line.whitespace
|
166
|
+
|
167
|
+
if @indentation.nil?
|
168
|
+
@indentation = line.whitespace
|
169
|
+
|
170
|
+
if @indentation.include?(?\s) && @indentation.include?(?\t)
|
171
|
+
raise HamlSyntaxError.new(HamlError.message(:cant_use_tabs_and_spaces), line.index)
|
172
|
+
end
|
173
|
+
|
174
|
+
@flat_spaces = @indentation * (@template_tabs+1) if flat?
|
175
|
+
return 1
|
176
|
+
end
|
177
|
+
|
178
|
+
tabs = line.whitespace.length / @indentation.length
|
179
|
+
return tabs if line.whitespace == @indentation * tabs
|
180
|
+
return @template_tabs + 1 if flat? && line.whitespace =~ /^#{@flat_spaces}/
|
181
|
+
|
182
|
+
message = HamlError.message(:inconsistent_indentation,
|
183
|
+
human_indentation(line.whitespace),
|
184
|
+
human_indentation(@indentation)
|
185
|
+
)
|
186
|
+
raise HamlSyntaxError.new(message, line.index)
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
def error_with_lineno(error)
|
192
|
+
return error if error.line
|
193
|
+
|
194
|
+
trace = error.backtrace.first
|
195
|
+
return error unless trace
|
196
|
+
|
197
|
+
line = trace.match(/\d+\z/).to_s.to_i
|
198
|
+
HamlSyntaxError.new(error.message, line)
|
199
|
+
end
|
200
|
+
|
201
|
+
# @private
|
202
|
+
Line = Struct.new(:whitespace, :text, :full, :index, :parser, :eod) do
|
203
|
+
alias_method :eod?, :eod
|
204
|
+
|
205
|
+
# @private
|
206
|
+
def tabs
|
207
|
+
@tabs ||= parser.compute_tabs(self)
|
208
|
+
end
|
209
|
+
|
210
|
+
def strip!(from)
|
211
|
+
self.text = text[from..-1]
|
212
|
+
self.text.lstrip!
|
213
|
+
self
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# @private
|
218
|
+
ParseNode = Struct.new(:type, :line, :value, :parent, :children) do
|
219
|
+
def initialize(*args)
|
220
|
+
super
|
221
|
+
self.children ||= []
|
222
|
+
end
|
223
|
+
|
224
|
+
def inspect
|
225
|
+
%Q[(#{type} #{value.inspect}#{children.each_with_object(''.dup) {|c, s| s << "\n#{c.inspect.gsub!(/^/, ' ')}"}})].dup
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# @param [String] new - Hash literal including dynamic values.
|
230
|
+
# @param [String] old - Hash literal including dynamic values or Ruby literal of multiple Hashes which MUST be interpreted as method's last arguments.
|
231
|
+
DynamicAttributes = Struct.new(:new, :old) do
|
232
|
+
undef :old=
|
233
|
+
def old=(value)
|
234
|
+
unless value =~ /\A{.*}\z/m
|
235
|
+
raise ArgumentError.new('Old attributes must start with "{" and end with "}"')
|
236
|
+
end
|
237
|
+
self[:old] = value
|
238
|
+
end
|
239
|
+
|
240
|
+
# This will be a literal for Haml::HamlBuffer#attributes's last argument, `attributes_hashes`.
|
241
|
+
def to_literal
|
242
|
+
[new, stripped_old].compact.join(', ')
|
243
|
+
end
|
244
|
+
|
245
|
+
private
|
246
|
+
|
247
|
+
# For `%foo{ { foo: 1 }, bar: 2 }`, :old is "{ { foo: 1 }, bar: 2 }" and this method returns " { foo: 1 }, bar: 2 " for last argument.
|
248
|
+
def stripped_old
|
249
|
+
return nil if old.nil?
|
250
|
+
old.sub!(/\A{/, '').sub!(/}\z/m, '')
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# Processes and deals with lowering indentation.
|
255
|
+
def process_indent(line)
|
256
|
+
return unless line.tabs <= @template_tabs && @template_tabs > 0
|
257
|
+
|
258
|
+
to_close = @template_tabs - line.tabs
|
259
|
+
to_close.times {|i| close unless to_close - 1 - i == 0 && continuation_script?(line.text)}
|
260
|
+
end
|
261
|
+
|
262
|
+
def continuation_script?(text)
|
263
|
+
text[0] == SILENT_SCRIPT && mid_block_keyword?(text)
|
264
|
+
end
|
265
|
+
|
266
|
+
def mid_block_keyword?(text)
|
267
|
+
MID_BLOCK_KEYWORDS.include?(block_keyword(text))
|
268
|
+
end
|
269
|
+
|
270
|
+
# Processes a single line of Haml.
|
271
|
+
#
|
272
|
+
# This method doesn't return anything; it simply processes the line and
|
273
|
+
# adds the appropriate code to `@precompiled`.
|
274
|
+
def process_line(line)
|
275
|
+
case line.text[0]
|
276
|
+
when DIV_CLASS; push div(line)
|
277
|
+
when DIV_ID
|
278
|
+
return push plain(line) if %w[{ @ $].include?(line.text[1])
|
279
|
+
push div(line)
|
280
|
+
when ELEMENT; push tag(line)
|
281
|
+
when COMMENT; push comment(line.text[1..-1].lstrip)
|
282
|
+
when SANITIZE
|
283
|
+
return push plain(line.strip!(3), :escape_html) if line.text[1, 2] == '=='
|
284
|
+
return push script(line.strip!(2), :escape_html) if line.text[1] == SCRIPT
|
285
|
+
return push flat_script(line.strip!(2), :escape_html) if line.text[1] == FLAT_SCRIPT
|
286
|
+
return push plain(line.strip!(1), :escape_html) if line.text[1] == ?\s || line.text[1..2] == '#{'
|
287
|
+
push plain(line)
|
288
|
+
when SCRIPT
|
289
|
+
return push plain(line.strip!(2)) if line.text[1] == SCRIPT
|
290
|
+
line.text = line.text[1..-1]
|
291
|
+
push script(line)
|
292
|
+
when FLAT_SCRIPT; push flat_script(line.strip!(1))
|
293
|
+
when SILENT_SCRIPT
|
294
|
+
return push haml_comment(line.text[2..-1]) if line.text[1] == SILENT_COMMENT
|
295
|
+
push silent_script(line)
|
296
|
+
when FILTER; push filter(line.text[1..-1].downcase)
|
297
|
+
when DOCTYPE
|
298
|
+
return push doctype(line.text) if line.text[0, 3] == '!!!'
|
299
|
+
return push plain(line.strip!(3), false) if line.text[1, 2] == '=='
|
300
|
+
return push script(line.strip!(2), false) if line.text[1] == SCRIPT
|
301
|
+
return push flat_script(line.strip!(2), false) if line.text[1] == FLAT_SCRIPT
|
302
|
+
return push plain(line.strip!(1), false) if line.text[1] == ?\s || line.text[1..2] == '#{'
|
303
|
+
push plain(line)
|
304
|
+
when ESCAPE
|
305
|
+
line.text = line.text[1..-1]
|
306
|
+
push plain(line)
|
307
|
+
else; push plain(line)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def block_keyword(text)
|
312
|
+
return unless (keyword = text.scan(BLOCK_KEYWORD_REGEX)[0])
|
313
|
+
keyword[0] || keyword[1]
|
314
|
+
end
|
315
|
+
|
316
|
+
def push(node)
|
317
|
+
@parent.children << node
|
318
|
+
node.parent = @parent
|
319
|
+
end
|
320
|
+
|
321
|
+
def plain(line, escape_html = nil)
|
322
|
+
if block_opened?
|
323
|
+
raise HamlSyntaxError.new(HamlError.message(:illegal_nesting_plain), @next_line.index)
|
324
|
+
end
|
325
|
+
|
326
|
+
unless Util.contains_interpolation?(line.text)
|
327
|
+
return ParseNode.new(:plain, line.index + 1, :text => line.text)
|
328
|
+
end
|
329
|
+
|
330
|
+
escape_html = @options.escape_html && @options.mime_type != 'text/plain' if escape_html.nil?
|
331
|
+
line.text = Util.unescape_interpolation(line.text)
|
332
|
+
script(line, false).tap { |n| n.value[:escape_interpolation] = true if escape_html }
|
333
|
+
end
|
334
|
+
|
335
|
+
def script(line, escape_html = nil, preserve = false)
|
336
|
+
raise HamlSyntaxError.new(HamlError.message(:no_ruby_code, '=')) if line.text.empty?
|
337
|
+
line = handle_ruby_multiline(line)
|
338
|
+
escape_html = @options.escape_html if escape_html.nil?
|
339
|
+
|
340
|
+
keyword = block_keyword(line.text)
|
341
|
+
check_push_script_stack(keyword)
|
342
|
+
|
343
|
+
ParseNode.new(:script, line.index + 1, :text => line.text, :escape_html => escape_html,
|
344
|
+
:preserve => preserve, :keyword => keyword)
|
345
|
+
end
|
346
|
+
|
347
|
+
def flat_script(line, escape_html = nil)
|
348
|
+
raise HamlSyntaxError.new(HamlError.message(:no_ruby_code, '~')) if line.text.empty?
|
349
|
+
script(line, escape_html, :preserve)
|
350
|
+
end
|
351
|
+
|
352
|
+
def silent_script(line)
|
353
|
+
raise HamlSyntaxError.new(HamlError.message(:no_end), line.index) if line.text[1..-1].strip == 'end'
|
354
|
+
|
355
|
+
line = handle_ruby_multiline(line)
|
356
|
+
keyword = block_keyword(line.text)
|
357
|
+
|
358
|
+
check_push_script_stack(keyword)
|
359
|
+
|
360
|
+
if ["else", "elsif", "when"].include?(keyword)
|
361
|
+
if @script_level_stack.empty?
|
362
|
+
raise Haml::HamlSyntaxError.new(HamlError.message(:missing_if, keyword), @line.index)
|
363
|
+
end
|
364
|
+
|
365
|
+
if keyword == 'when' and !@script_level_stack.last[2]
|
366
|
+
if @script_level_stack.last[1] + 1 == @line.tabs
|
367
|
+
@script_level_stack.last[1] += 1
|
368
|
+
end
|
369
|
+
@script_level_stack.last[2] = true
|
370
|
+
end
|
371
|
+
|
372
|
+
if @script_level_stack.last[1] != @line.tabs
|
373
|
+
message = HamlError.message(:bad_script_indent, keyword, @script_level_stack.last[1], @line.tabs)
|
374
|
+
raise Haml::HamlSyntaxError.new(message, @line.index)
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
ParseNode.new(:silent_script, @line.index + 1,
|
379
|
+
:text => line.text[1..-1], :keyword => keyword)
|
380
|
+
end
|
381
|
+
|
382
|
+
def check_push_script_stack(keyword)
|
383
|
+
if ["if", "case", "unless"].include?(keyword)
|
384
|
+
# @script_level_stack contents are arrays of form
|
385
|
+
# [:keyword, stack_level, other_info]
|
386
|
+
@script_level_stack.push([keyword.to_sym, @line.tabs])
|
387
|
+
@script_level_stack.last << false if keyword == 'case'
|
388
|
+
@tab_up = true
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
def haml_comment(text)
|
393
|
+
if filter_opened?
|
394
|
+
@flat = true
|
395
|
+
@filter_buffer = String.new
|
396
|
+
@filter_buffer << "#{text}\n" unless text.empty?
|
397
|
+
text = @filter_buffer
|
398
|
+
# If we don't know the indentation by now, it'll be set in Line#tabs
|
399
|
+
@flat_spaces = @indentation * (@template_tabs+1) if @indentation
|
400
|
+
end
|
401
|
+
|
402
|
+
ParseNode.new(:haml_comment, @line.index + 1, :text => text)
|
403
|
+
end
|
404
|
+
|
405
|
+
def tag(line)
|
406
|
+
tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
|
407
|
+
nuke_inner_whitespace, action, value, last_line = parse_tag(line.text)
|
408
|
+
|
409
|
+
preserve_tag = @options.preserve.include?(tag_name)
|
410
|
+
nuke_inner_whitespace ||= preserve_tag
|
411
|
+
escape_html = (action == '&' || (action != '!' && @options.escape_html))
|
412
|
+
|
413
|
+
case action
|
414
|
+
when '/'; self_closing = true
|
415
|
+
when '~'; parse = preserve_script = true
|
416
|
+
when '='
|
417
|
+
parse = true
|
418
|
+
if value[0] == ?=
|
419
|
+
value = Util.unescape_interpolation(value[1..-1].strip)
|
420
|
+
escape_interpolation = true if escape_html
|
421
|
+
escape_html = false
|
422
|
+
end
|
423
|
+
when '&', '!'
|
424
|
+
if value[0] == ?= || value[0] == ?~
|
425
|
+
parse = true
|
426
|
+
preserve_script = (value[0] == ?~)
|
427
|
+
if value[1] == ?=
|
428
|
+
value = Util.unescape_interpolation(value[2..-1].strip)
|
429
|
+
escape_interpolation = true if escape_html
|
430
|
+
escape_html = false
|
431
|
+
else
|
432
|
+
value = value[1..-1].strip
|
433
|
+
end
|
434
|
+
elsif Util.contains_interpolation?(value)
|
435
|
+
value = Util.unescape_interpolation(value)
|
436
|
+
escape_interpolation = true if escape_html
|
437
|
+
parse = true
|
438
|
+
escape_html = false
|
439
|
+
end
|
440
|
+
else
|
441
|
+
if Util.contains_interpolation?(value)
|
442
|
+
value = Util.unescape_interpolation(value, escape_html)
|
443
|
+
parse = true
|
444
|
+
escape_html = false
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
attributes = Parser.parse_class_and_id(attributes)
|
449
|
+
dynamic_attributes = DynamicAttributes.new
|
450
|
+
|
451
|
+
if attributes_hashes[:new]
|
452
|
+
static_attributes, attributes_hash = attributes_hashes[:new]
|
453
|
+
AttributeMerger.merge_attributes!(attributes, static_attributes) if static_attributes
|
454
|
+
dynamic_attributes.new = attributes_hash
|
455
|
+
end
|
456
|
+
|
457
|
+
if attributes_hashes[:old]
|
458
|
+
static_attributes = parse_static_hash(attributes_hashes[:old])
|
459
|
+
AttributeMerger.merge_attributes!(attributes, static_attributes) if static_attributes
|
460
|
+
dynamic_attributes.old = attributes_hashes[:old] unless static_attributes || @options.suppress_eval
|
461
|
+
end
|
462
|
+
|
463
|
+
raise HamlSyntaxError.new(HamlError.message(:illegal_nesting_self_closing), @next_line.index) if block_opened? && self_closing
|
464
|
+
raise HamlSyntaxError.new(HamlError.message(:no_ruby_code, action), last_line - 1) if parse && value.empty?
|
465
|
+
raise HamlSyntaxError.new(HamlError.message(:self_closing_content), last_line - 1) if self_closing && !value.empty?
|
466
|
+
|
467
|
+
if block_opened? && !value.empty? && !is_ruby_multiline?(value)
|
468
|
+
raise HamlSyntaxError.new(HamlError.message(:illegal_nesting_line, tag_name), @next_line.index)
|
469
|
+
end
|
470
|
+
|
471
|
+
self_closing ||= !!(!block_opened? && value.empty? && @options.autoclose.any? {|t| t === tag_name})
|
472
|
+
value = nil if value.empty? && (block_opened? || self_closing)
|
473
|
+
line.text = value
|
474
|
+
line = handle_ruby_multiline(line) if parse
|
475
|
+
|
476
|
+
ParseNode.new(:tag, line.index + 1, :name => tag_name, :attributes => attributes,
|
477
|
+
:dynamic_attributes => dynamic_attributes, :self_closing => self_closing,
|
478
|
+
:nuke_inner_whitespace => nuke_inner_whitespace,
|
479
|
+
:nuke_outer_whitespace => nuke_outer_whitespace, :object_ref => object_ref,
|
480
|
+
:escape_html => escape_html, :preserve_tag => preserve_tag,
|
481
|
+
:preserve_script => preserve_script, :parse => parse, :value => line.text,
|
482
|
+
:escape_interpolation => escape_interpolation)
|
483
|
+
end
|
484
|
+
|
485
|
+
# Renders a line that creates an XHTML tag and has an implicit div because of
|
486
|
+
# `.` or `#`.
|
487
|
+
def div(line)
|
488
|
+
line.text = "%div#{line.text}"
|
489
|
+
tag(line)
|
490
|
+
end
|
491
|
+
|
492
|
+
# Renders an XHTML comment.
|
493
|
+
def comment(text)
|
494
|
+
if text[0..1] == '!['
|
495
|
+
revealed = true
|
496
|
+
text = text[1..-1]
|
497
|
+
else
|
498
|
+
revealed = false
|
499
|
+
end
|
500
|
+
|
501
|
+
conditional, text = balance(text, ?[, ?]) if text[0] == ?[
|
502
|
+
text.strip!
|
503
|
+
|
504
|
+
if Util.contains_interpolation?(text)
|
505
|
+
parse = true
|
506
|
+
text = Util.unescape_interpolation(text)
|
507
|
+
else
|
508
|
+
parse = false
|
509
|
+
end
|
510
|
+
|
511
|
+
if block_opened? && !text.empty?
|
512
|
+
raise HamlSyntaxError.new(Haml::HamlError.message(:illegal_nesting_content), @next_line.index)
|
513
|
+
end
|
514
|
+
|
515
|
+
ParseNode.new(:comment, @line.index + 1, :conditional => conditional, :text => text, :revealed => revealed, :parse => parse)
|
516
|
+
end
|
517
|
+
|
518
|
+
# Renders an XHTML doctype or XML shebang.
|
519
|
+
def doctype(text)
|
520
|
+
raise HamlSyntaxError.new(HamlError.message(:illegal_nesting_header), @next_line.index) if block_opened?
|
521
|
+
version, type, encoding = text[3..-1].strip.downcase.scan(DOCTYPE_REGEX)[0]
|
522
|
+
ParseNode.new(:doctype, @line.index + 1, :version => version, :type => type, :encoding => encoding)
|
523
|
+
end
|
524
|
+
|
525
|
+
def filter(name)
|
526
|
+
raise HamlError.new(HamlError.message(:invalid_filter_name, name)) unless name =~ /^\w+$/
|
527
|
+
|
528
|
+
if filter_opened?
|
529
|
+
@flat = true
|
530
|
+
@filter_buffer = String.new
|
531
|
+
# If we don't know the indentation by now, it'll be set in Line#tabs
|
532
|
+
@flat_spaces = @indentation * (@template_tabs+1) if @indentation
|
533
|
+
end
|
534
|
+
|
535
|
+
ParseNode.new(:filter, @line.index + 1, :name => name, :text => @filter_buffer)
|
536
|
+
end
|
537
|
+
|
538
|
+
def close
|
539
|
+
node, @parent = @parent, @parent.parent
|
540
|
+
@template_tabs -= 1
|
541
|
+
send("close_#{node.type}", node) if respond_to?("close_#{node.type}", :include_private)
|
542
|
+
end
|
543
|
+
|
544
|
+
def close_filter(_)
|
545
|
+
close_flat_section
|
546
|
+
end
|
547
|
+
|
548
|
+
def close_haml_comment(_)
|
549
|
+
close_flat_section
|
550
|
+
end
|
551
|
+
|
552
|
+
def close_flat_section
|
553
|
+
@flat = false
|
554
|
+
@flat_spaces = nil
|
555
|
+
@filter_buffer = nil
|
556
|
+
end
|
557
|
+
|
558
|
+
def close_silent_script(node)
|
559
|
+
@script_level_stack.pop if ["if", "case", "unless"].include? node.value[:keyword]
|
560
|
+
|
561
|
+
# Post-process case statements to normalize the nesting of "when" clauses
|
562
|
+
return unless node.value[:keyword] == "case"
|
563
|
+
return unless (first = node.children.first)
|
564
|
+
return unless first.type == :silent_script && first.value[:keyword] == "when"
|
565
|
+
return if first.children.empty?
|
566
|
+
# If the case node has a "when" child with children, it's the
|
567
|
+
# only child. Then we want to put everything nested beneath it
|
568
|
+
# beneath the case itself (just like "if").
|
569
|
+
node.children = [first, *first.children]
|
570
|
+
first.children = []
|
571
|
+
end
|
572
|
+
|
573
|
+
alias :close_script :close_silent_script
|
574
|
+
|
575
|
+
# This is a class method so it can be accessed from {Haml::HamlHelpers}.
|
576
|
+
#
|
577
|
+
# Iterates through the classes and ids supplied through `.`
|
578
|
+
# and `#` syntax, and returns a hash with them as attributes,
|
579
|
+
# that can then be merged with another attributes hash.
|
580
|
+
def self.parse_class_and_id(list)
|
581
|
+
attributes = {}
|
582
|
+
return attributes if list.empty?
|
583
|
+
|
584
|
+
list.scan(/([#.])([-:_a-zA-Z0-9\@]+)/) do |type, property|
|
585
|
+
case type
|
586
|
+
when '.'
|
587
|
+
if attributes[CLASS_KEY]
|
588
|
+
attributes[CLASS_KEY] += " "
|
589
|
+
else
|
590
|
+
attributes[CLASS_KEY] = ""
|
591
|
+
end
|
592
|
+
attributes[CLASS_KEY] += property
|
593
|
+
when '#'; attributes[ID_KEY] = property
|
594
|
+
end
|
595
|
+
end
|
596
|
+
attributes
|
597
|
+
end
|
598
|
+
|
599
|
+
# This method doesn't use Haml::HamlAttributeParser because currently it depends on Ripper and Rubinius doesn't provide it.
|
600
|
+
# Ideally this logic should be placed in Haml::HamlAttributeParser instead of here and this method should use it.
|
601
|
+
#
|
602
|
+
# @param [String] text - Hash literal or text inside old attributes
|
603
|
+
# @return [Hash,nil] - Return nil if text is not static Hash literal
|
604
|
+
def parse_static_hash(text)
|
605
|
+
attributes = {}
|
606
|
+
return attributes if text.empty?
|
607
|
+
|
608
|
+
text = text[1...-1] # strip brackets
|
609
|
+
scanner = StringScanner.new(text)
|
610
|
+
scanner.scan(/\s+/)
|
611
|
+
until scanner.eos?
|
612
|
+
return unless (key = scanner.scan(LITERAL_VALUE_REGEX))
|
613
|
+
return unless scanner.scan(/\s*=>\s*/)
|
614
|
+
return unless (value = scanner.scan(LITERAL_VALUE_REGEX))
|
615
|
+
return unless scanner.scan(/\s*(?:,|$)\s*/)
|
616
|
+
attributes[eval(key).to_s] = eval(value).to_s
|
617
|
+
end
|
618
|
+
attributes
|
619
|
+
end
|
620
|
+
|
621
|
+
# Parses a line into tag_name, attributes, attributes_hash, object_ref, action, value
|
622
|
+
def parse_tag(text)
|
623
|
+
match = text.scan(/%([-:\w]+)([-:\w.#\@]*)(.+)?/)[0]
|
624
|
+
raise HamlSyntaxError.new(HamlError.message(:invalid_tag, text)) unless match
|
625
|
+
|
626
|
+
tag_name, attributes, rest = match
|
627
|
+
|
628
|
+
if !attributes.empty? && (attributes =~ /[.#](\.|#|\z)/)
|
629
|
+
raise HamlSyntaxError.new(HamlError.message(:illegal_element))
|
630
|
+
end
|
631
|
+
|
632
|
+
new_attributes_hash = old_attributes_hash = last_line = nil
|
633
|
+
object_ref = :nil
|
634
|
+
attributes_hashes = {}
|
635
|
+
while rest && !rest.empty?
|
636
|
+
case rest[0]
|
637
|
+
when ?{
|
638
|
+
break if old_attributes_hash
|
639
|
+
old_attributes_hash, rest, last_line = parse_old_attributes(rest)
|
640
|
+
attributes_hashes[:old] = old_attributes_hash
|
641
|
+
when ?(
|
642
|
+
break if new_attributes_hash
|
643
|
+
new_attributes_hash, rest, last_line = parse_new_attributes(rest)
|
644
|
+
attributes_hashes[:new] = new_attributes_hash
|
645
|
+
when ?[
|
646
|
+
break unless object_ref == :nil
|
647
|
+
object_ref, rest = balance(rest, ?[, ?])
|
648
|
+
else; break
|
649
|
+
end
|
650
|
+
end
|
651
|
+
|
652
|
+
if rest && !rest.empty?
|
653
|
+
nuke_whitespace, action, value = rest.scan(/(<>|><|[><])?([=\/\~&!])?(.*)?/)[0]
|
654
|
+
if nuke_whitespace
|
655
|
+
nuke_outer_whitespace = nuke_whitespace.include? '>'
|
656
|
+
nuke_inner_whitespace = nuke_whitespace.include? '<'
|
657
|
+
end
|
658
|
+
end
|
659
|
+
|
660
|
+
if @options.remove_whitespace
|
661
|
+
nuke_outer_whitespace = true
|
662
|
+
nuke_inner_whitespace = true
|
663
|
+
end
|
664
|
+
|
665
|
+
if value.nil?
|
666
|
+
value = ''
|
667
|
+
else
|
668
|
+
value.strip!
|
669
|
+
end
|
670
|
+
[tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
|
671
|
+
nuke_inner_whitespace, action, value, last_line || @line.index + 1]
|
672
|
+
end
|
673
|
+
|
674
|
+
# @return [String] attributes_hash - Hash literal starting with `{` and ending with `}`
|
675
|
+
# @return [String] rest
|
676
|
+
# @return [Integer] last_line
|
677
|
+
def parse_old_attributes(text)
|
678
|
+
last_line = @line.index + 1
|
679
|
+
|
680
|
+
begin
|
681
|
+
# Old attributes often look like a valid Hash literal, but it sometimes allow code like
|
682
|
+
# `{ hash, foo: bar }`, which is compiled to `_hamlout.attributes({}, nil, hash, foo: bar)`.
|
683
|
+
#
|
684
|
+
# To scan such code correctly, this scans `a( hash, foo: bar }` instead, stops when there is
|
685
|
+
# 1 more :on_embexpr_end (the last '}') than :on_embexpr_beg, and resurrects '{' afterwards.
|
686
|
+
balanced, rest = balance_tokens(text.sub(?{, METHOD_CALL_PREFIX), :on_embexpr_beg, :on_embexpr_end, count: 1)
|
687
|
+
attributes_hash = balanced.sub(METHOD_CALL_PREFIX, ?{)
|
688
|
+
rescue HamlSyntaxError => e
|
689
|
+
if e.message == HamlError.message(:unbalanced_brackets) && !@template.empty?
|
690
|
+
text << "\n#{@next_line.text}"
|
691
|
+
last_line += 1
|
692
|
+
next_line
|
693
|
+
retry
|
694
|
+
end
|
695
|
+
|
696
|
+
raise e
|
697
|
+
end
|
698
|
+
|
699
|
+
return attributes_hash, rest, last_line
|
700
|
+
end
|
701
|
+
|
702
|
+
# @return [Array<Hash,String,nil>] - [static_attributes (Hash), dynamic_attributes (nil or String starting with `{` and ending with `}`)]
|
703
|
+
# @return [String] rest
|
704
|
+
# @return [Integer] last_line
|
705
|
+
def parse_new_attributes(text)
|
706
|
+
scanner = StringScanner.new(text)
|
707
|
+
last_line = @line.index + 1
|
708
|
+
attributes = {}
|
709
|
+
|
710
|
+
scanner.scan(/\(\s*/)
|
711
|
+
loop do
|
712
|
+
name, value = parse_new_attribute(scanner)
|
713
|
+
break if name.nil?
|
714
|
+
|
715
|
+
if name == false
|
716
|
+
scanned = Haml::Util.balance(text, ?(, ?))
|
717
|
+
text = scanned ? scanned.first : text
|
718
|
+
raise Haml::HamlSyntaxError.new(HamlError.message(:invalid_attribute_list, text.inspect), last_line - 1)
|
719
|
+
end
|
720
|
+
attributes[name] = value
|
721
|
+
scanner.scan(/\s*/)
|
722
|
+
|
723
|
+
if scanner.eos?
|
724
|
+
text << " #{@next_line.text}"
|
725
|
+
last_line += 1
|
726
|
+
next_line
|
727
|
+
scanner.scan(/\s*/)
|
728
|
+
end
|
729
|
+
end
|
730
|
+
|
731
|
+
static_attributes = {}
|
732
|
+
dynamic_attributes = "{".dup
|
733
|
+
attributes.each do |name, (type, val)|
|
734
|
+
if type == :static
|
735
|
+
static_attributes[name] = val
|
736
|
+
else
|
737
|
+
dynamic_attributes << "#{Util.inspect_obj(name)} => #{val},"
|
738
|
+
end
|
739
|
+
end
|
740
|
+
dynamic_attributes << "}"
|
741
|
+
dynamic_attributes = nil if dynamic_attributes == "{}"
|
742
|
+
|
743
|
+
return [static_attributes, dynamic_attributes], scanner.rest, last_line
|
744
|
+
end
|
745
|
+
|
746
|
+
def parse_new_attribute(scanner)
|
747
|
+
unless (name = scanner.scan(/[-:\w]+/))
|
748
|
+
return if scanner.scan(/\)/)
|
749
|
+
return false
|
750
|
+
end
|
751
|
+
|
752
|
+
scanner.scan(/\s*/)
|
753
|
+
return name, [:static, true] unless scanner.scan(/=/) #/end
|
754
|
+
|
755
|
+
scanner.scan(/\s*/)
|
756
|
+
unless (quote = scanner.scan(/["']/))
|
757
|
+
return false unless (var = scanner.scan(/(@@?|\$)?\w+/))
|
758
|
+
return name, [:dynamic, var]
|
759
|
+
end
|
760
|
+
|
761
|
+
re = /((?:\\.|\#(?!\{)|[^#{quote}\\#])*)(#{quote}|#\{)/
|
762
|
+
content = []
|
763
|
+
loop do
|
764
|
+
return false unless scanner.scan(re)
|
765
|
+
content << [:str, scanner[1].gsub(/\\(.)/, '\1')]
|
766
|
+
break if scanner[2] == quote
|
767
|
+
content << [:ruby, balance(scanner, ?{, ?}, 1).first[0...-1]]
|
768
|
+
end
|
769
|
+
|
770
|
+
return name, [:static, content.first[1]] if content.size == 1
|
771
|
+
return name, [:dynamic,
|
772
|
+
%!"#{content.each_with_object(''.dup) {|(t, v), s| s << (t == :str ? Util.inspect_obj(v)[1...-1] : "\#{#{v}}")}}"!]
|
773
|
+
end
|
774
|
+
|
775
|
+
def next_line
|
776
|
+
line = @template.shift || raise(StopIteration)
|
777
|
+
|
778
|
+
# `flat?' here is a little outdated,
|
779
|
+
# so we have to manually check if either the previous or current line
|
780
|
+
# closes the flat block, as well as whether a new block is opened.
|
781
|
+
line_defined = instance_variable_defined?(:@line)
|
782
|
+
@line.tabs if line_defined
|
783
|
+
unless (flat? && !closes_flat?(line) && !closes_flat?(@line)) ||
|
784
|
+
(line_defined && @line.text[0] == ?: && line.full =~ %r[^#{@line.full[/^\s+/]}\s])
|
785
|
+
return next_line if line.text.empty?
|
786
|
+
|
787
|
+
handle_multiline(line)
|
788
|
+
end
|
789
|
+
|
790
|
+
@next_line = line
|
791
|
+
end
|
792
|
+
|
793
|
+
def closes_flat?(line)
|
794
|
+
line && !line.text.empty? && line.full !~ /^#{@flat_spaces}/
|
795
|
+
end
|
796
|
+
|
797
|
+
def handle_multiline(line)
|
798
|
+
return unless is_multiline?(line.text)
|
799
|
+
line.text.slice!(-1)
|
800
|
+
loop do
|
801
|
+
new_line = @template.first
|
802
|
+
break if new_line.eod?
|
803
|
+
next @template.shift if new_line.text.strip.empty?
|
804
|
+
break unless is_multiline?(new_line.text.strip)
|
805
|
+
line.text << new_line.text.strip[0...-1]
|
806
|
+
@template.shift
|
807
|
+
end
|
808
|
+
end
|
809
|
+
|
810
|
+
# Checks whether or not `line` is in a multiline sequence.
|
811
|
+
def is_multiline?(text)
|
812
|
+
text && text.length > 1 && text[-1] == MULTILINE_CHAR_VALUE && text[-2] == ?\s && text !~ BLOCK_WITH_SPACES
|
813
|
+
end
|
814
|
+
|
815
|
+
def handle_ruby_multiline(line)
|
816
|
+
line.text.rstrip!
|
817
|
+
return line unless is_ruby_multiline?(line.text)
|
818
|
+
begin
|
819
|
+
# Use already fetched @next_line in the first loop. Otherwise, fetch next
|
820
|
+
new_line = new_line.nil? ? @next_line : @template.shift
|
821
|
+
break if new_line.eod?
|
822
|
+
next if new_line.text.empty?
|
823
|
+
line.text << " #{new_line.text.rstrip}"
|
824
|
+
end while is_ruby_multiline?(new_line.text)
|
825
|
+
next_line
|
826
|
+
line
|
827
|
+
end
|
828
|
+
|
829
|
+
# `text' is a Ruby multiline block if it:
|
830
|
+
# - ends with a comma
|
831
|
+
# - but not "?," which is a character literal
|
832
|
+
# (however, "x?," is a method call and not a literal)
|
833
|
+
# - and not "?\," which is a character literal
|
834
|
+
def is_ruby_multiline?(text)
|
835
|
+
text && text.length > 1 && text[-1] == ?, &&
|
836
|
+
!((text[-3, 2] =~ /\W\?/) || text[-3, 2] == "?\\")
|
837
|
+
end
|
838
|
+
|
839
|
+
def balance(*args)
|
840
|
+
Haml::Util.balance(*args) or raise(HamlSyntaxError.new(HamlError.message(:unbalanced_brackets)))
|
841
|
+
end
|
842
|
+
|
843
|
+
# Unlike #balance, this balances Ripper tokens to balance something like `{ a: "}" }` correctly.
|
844
|
+
def balance_tokens(buf, start, finish, count: 0)
|
845
|
+
text = ''.dup
|
846
|
+
Ripper.lex(buf).each do |_, token, str|
|
847
|
+
text << str
|
848
|
+
case token
|
849
|
+
when start
|
850
|
+
count += 1
|
851
|
+
when finish
|
852
|
+
count -= 1
|
853
|
+
end
|
854
|
+
|
855
|
+
if count == 0
|
856
|
+
return text, buf.sub(text, '')
|
857
|
+
end
|
858
|
+
end
|
859
|
+
raise HamlSyntaxError.new(HamlError.message(:unbalanced_brackets))
|
860
|
+
end
|
861
|
+
|
862
|
+
def block_opened?
|
863
|
+
@next_line.tabs > @line.tabs
|
864
|
+
end
|
865
|
+
|
866
|
+
# Same semantics as block_opened?, except that block_opened? uses Line#tabs,
|
867
|
+
# which doesn't interact well with filter lines
|
868
|
+
def filter_opened?
|
869
|
+
@next_line.full =~ (@indentation ? /^#{@indentation * (@template_tabs + 1)}/ : /^\s/)
|
870
|
+
end
|
871
|
+
|
872
|
+
def flat?
|
873
|
+
@flat
|
874
|
+
end
|
875
|
+
|
876
|
+
class << AttributeMerger = Object.new
|
877
|
+
# Merges two attribute hashes.
|
878
|
+
# This is the same as `to.merge!(from)`,
|
879
|
+
# except that it merges id, class, and data attributes.
|
880
|
+
#
|
881
|
+
# ids are concatenated with `"_"`,
|
882
|
+
# and classes are concatenated with `" "`.
|
883
|
+
# data hashes are simply merged.
|
884
|
+
#
|
885
|
+
# Destructively modifies `to`.
|
886
|
+
#
|
887
|
+
# @param to [{String => String,Hash}] The attribute hash to merge into
|
888
|
+
# @param from [{String => Object}] The attribute hash to merge from
|
889
|
+
# @return [{String => String,Hash}] `to`, after being merged
|
890
|
+
def merge_attributes!(to, from)
|
891
|
+
from.keys.each do |key|
|
892
|
+
to[key] = merge_value(key, to[key], from[key])
|
893
|
+
end
|
894
|
+
to
|
895
|
+
end
|
896
|
+
|
897
|
+
private
|
898
|
+
|
899
|
+
# @return [String, nil]
|
900
|
+
def filter_and_join(value, separator)
|
901
|
+
return '' if (value.respond_to?(:empty?) && value.empty?)
|
902
|
+
|
903
|
+
if value.is_a?(Array)
|
904
|
+
value = value.flatten
|
905
|
+
value.map! {|item| item ? item.to_s : nil}
|
906
|
+
value.compact!
|
907
|
+
value = value.join(separator)
|
908
|
+
else
|
909
|
+
value = value ? value.to_s : nil
|
910
|
+
end
|
911
|
+
!value.nil? && !value.empty? && value
|
912
|
+
end
|
913
|
+
|
914
|
+
# Merge a couple of values to one attribute value. No destructive operation.
|
915
|
+
#
|
916
|
+
# @param to [String,Hash,nil]
|
917
|
+
# @param from [Object]
|
918
|
+
# @return [String,Hash]
|
919
|
+
def merge_value(key, to, from)
|
920
|
+
if from.kind_of?(Hash) || to.kind_of?(Hash)
|
921
|
+
from = { nil => from } if !from.is_a?(Hash)
|
922
|
+
to = { nil => to } if !to.is_a?(Hash)
|
923
|
+
to.merge(from)
|
924
|
+
elsif key == 'id'
|
925
|
+
merged_id = filter_and_join(from, '_')
|
926
|
+
if to && merged_id
|
927
|
+
merged_id = "#{to}_#{merged_id}"
|
928
|
+
elsif to || merged_id
|
929
|
+
merged_id ||= to
|
930
|
+
end
|
931
|
+
merged_id
|
932
|
+
elsif key == 'class'
|
933
|
+
merged_class = filter_and_join(from, ' ')
|
934
|
+
if to && merged_class
|
935
|
+
merged_class = (to.split(' ') | merged_class.split(' ')).join(' ')
|
936
|
+
elsif to || merged_class
|
937
|
+
merged_class ||= to
|
938
|
+
end
|
939
|
+
merged_class
|
940
|
+
else
|
941
|
+
from
|
942
|
+
end
|
943
|
+
end
|
944
|
+
end
|
945
|
+
private_constant :AttributeMerger
|
946
|
+
|
947
|
+
class ParserOptions
|
948
|
+
# A list of options that are actually used in the parser
|
949
|
+
AVAILABLE_OPTIONS = %i[
|
950
|
+
autoclose
|
951
|
+
escape_html
|
952
|
+
filename
|
953
|
+
line
|
954
|
+
mime_type
|
955
|
+
preserve
|
956
|
+
remove_whitespace
|
957
|
+
suppress_eval
|
958
|
+
].each do |option|
|
959
|
+
attr_reader option
|
960
|
+
end
|
961
|
+
|
962
|
+
DEFAULTS = {
|
963
|
+
autoclose: %w(area base basefont br col command embed frame
|
964
|
+
hr img input isindex keygen link menuitem meta
|
965
|
+
param source track wbr),
|
966
|
+
escape_html: false,
|
967
|
+
filename: '(haml)',
|
968
|
+
line: 1,
|
969
|
+
mime_type: 'text/html',
|
970
|
+
preserve: %w(textarea pre code),
|
971
|
+
remove_whitespace: false,
|
972
|
+
suppress_eval: false,
|
973
|
+
}
|
974
|
+
|
975
|
+
def initialize(values = {})
|
976
|
+
DEFAULTS.each {|k, v| instance_variable_set :"@#{k}", v}
|
977
|
+
AVAILABLE_OPTIONS.each do |key|
|
978
|
+
if values.key?(key)
|
979
|
+
instance_variable_set :"@#{key}", values[key]
|
980
|
+
end
|
981
|
+
end
|
982
|
+
end
|
983
|
+
end
|
984
|
+
private_constant :ParserOptions
|
985
|
+
end
|
986
|
+
end
|