hamlit 2.9.3
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/.gitignore +16 -0
- data/.travis.yml +45 -0
- data/CHANGELOG.md +676 -0
- data/Gemfile +28 -0
- data/LICENSE.txt +44 -0
- data/README.md +150 -0
- data/REFERENCE.md +266 -0
- data/Rakefile +117 -0
- data/benchmark/boolean_attribute.haml +6 -0
- data/benchmark/class_attribute.haml +5 -0
- data/benchmark/common_attribute.haml +3 -0
- data/benchmark/data_attribute.haml +4 -0
- data/benchmark/dynamic_attributes/boolean_attribute.haml +4 -0
- data/benchmark/dynamic_attributes/class_attribute.haml +4 -0
- data/benchmark/dynamic_attributes/common_attribute.haml +2 -0
- data/benchmark/dynamic_attributes/data_attribute.haml +2 -0
- data/benchmark/dynamic_attributes/id_attribute.haml +2 -0
- data/benchmark/dynamic_boolean_attribute.haml +4 -0
- data/benchmark/etc/attribute_builder.haml +5 -0
- data/benchmark/etc/real_sample.haml +888 -0
- data/benchmark/etc/real_sample.rb +11 -0
- data/benchmark/etc/static_analyzer.haml +1 -0
- data/benchmark/etc/string_interpolation.haml +2 -0
- data/benchmark/etc/tags.haml +3 -0
- data/benchmark/etc/tags_loop.haml +2 -0
- data/benchmark/ext/build_data.rb +17 -0
- data/benchmark/ext/build_id.rb +13 -0
- data/benchmark/id_attribute.haml +3 -0
- data/benchmark/plain.haml +4 -0
- data/benchmark/script.haml +4 -0
- data/benchmark/slim/LICENSE +21 -0
- data/benchmark/slim/context.rb +11 -0
- data/benchmark/slim/run-benchmarks.rb +94 -0
- data/benchmark/slim/view.erb +23 -0
- data/benchmark/slim/view.haml +18 -0
- data/benchmark/slim/view.slim +17 -0
- data/benchmark/utils/benchmark_ips_extension.rb +43 -0
- data/bin/bench +77 -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/hamlit +6 -0
- data/ext/hamlit/extconf.rb +10 -0
- data/ext/hamlit/hamlit.c +553 -0
- data/ext/hamlit/hescape.c +108 -0
- data/ext/hamlit/hescape.h +20 -0
- data/hamlit.gemspec +45 -0
- data/lib/hamlit.rb +11 -0
- data/lib/hamlit/attribute_builder.rb +173 -0
- data/lib/hamlit/attribute_compiler.rb +123 -0
- data/lib/hamlit/attribute_parser.rb +110 -0
- data/lib/hamlit/cli.rb +130 -0
- data/lib/hamlit/compiler.rb +97 -0
- data/lib/hamlit/compiler/children_compiler.rb +112 -0
- data/lib/hamlit/compiler/comment_compiler.rb +36 -0
- data/lib/hamlit/compiler/doctype_compiler.rb +46 -0
- data/lib/hamlit/compiler/script_compiler.rb +102 -0
- data/lib/hamlit/compiler/silent_script_compiler.rb +24 -0
- data/lib/hamlit/compiler/tag_compiler.rb +74 -0
- data/lib/hamlit/engine.rb +37 -0
- data/lib/hamlit/error.rb +15 -0
- data/lib/hamlit/escapable.rb +13 -0
- data/lib/hamlit/filters.rb +75 -0
- data/lib/hamlit/filters/base.rb +12 -0
- data/lib/hamlit/filters/cdata.rb +20 -0
- data/lib/hamlit/filters/coffee.rb +17 -0
- data/lib/hamlit/filters/css.rb +33 -0
- data/lib/hamlit/filters/erb.rb +10 -0
- data/lib/hamlit/filters/escaped.rb +22 -0
- data/lib/hamlit/filters/javascript.rb +33 -0
- data/lib/hamlit/filters/less.rb +20 -0
- data/lib/hamlit/filters/markdown.rb +10 -0
- data/lib/hamlit/filters/plain.rb +29 -0
- data/lib/hamlit/filters/preserve.rb +22 -0
- data/lib/hamlit/filters/ruby.rb +10 -0
- data/lib/hamlit/filters/sass.rb +15 -0
- data/lib/hamlit/filters/scss.rb +15 -0
- data/lib/hamlit/filters/text_base.rb +25 -0
- data/lib/hamlit/filters/tilt_base.rb +49 -0
- data/lib/hamlit/force_escapable.rb +29 -0
- data/lib/hamlit/helpers.rb +15 -0
- data/lib/hamlit/html.rb +14 -0
- data/lib/hamlit/identity.rb +13 -0
- data/lib/hamlit/object_ref.rb +30 -0
- data/lib/hamlit/parser.rb +49 -0
- data/lib/hamlit/parser/MIT-LICENSE +20 -0
- data/lib/hamlit/parser/README.md +30 -0
- data/lib/hamlit/parser/haml_buffer.rb +348 -0
- data/lib/hamlit/parser/haml_compiler.rb +553 -0
- data/lib/hamlit/parser/haml_error.rb +61 -0
- data/lib/hamlit/parser/haml_helpers.rb +727 -0
- data/lib/hamlit/parser/haml_options.rb +286 -0
- data/lib/hamlit/parser/haml_parser.rb +800 -0
- data/lib/hamlit/parser/haml_util.rb +288 -0
- data/lib/hamlit/parser/haml_xss_mods.rb +109 -0
- data/lib/hamlit/rails_helpers.rb +51 -0
- data/lib/hamlit/rails_template.rb +59 -0
- data/lib/hamlit/railtie.rb +10 -0
- data/lib/hamlit/ruby_expression.rb +32 -0
- data/lib/hamlit/string_splitter.rb +88 -0
- data/lib/hamlit/template.rb +28 -0
- data/lib/hamlit/utils.rb +18 -0
- data/lib/hamlit/version.rb +4 -0
- metadata +361 -0
@@ -0,0 +1,286 @@
|
|
1
|
+
require 'hamlit/parser/haml_parser'
|
2
|
+
require 'hamlit/parser/haml_compiler'
|
3
|
+
require 'hamlit/parser/haml_error'
|
4
|
+
|
5
|
+
module Hamlit
|
6
|
+
# This class encapsulates all of the configuration options that Haml
|
7
|
+
# understands. Please see the {file:REFERENCE.md#options Haml Reference} to
|
8
|
+
# learn how to set the options.
|
9
|
+
class HamlOptions
|
10
|
+
|
11
|
+
@defaults = {
|
12
|
+
:attr_wrapper => "'",
|
13
|
+
:autoclose => %w(area base basefont br col command embed frame
|
14
|
+
hr img input isindex keygen link menuitem meta
|
15
|
+
param source track wbr),
|
16
|
+
:encoding => "UTF-8",
|
17
|
+
:escape_attrs => true,
|
18
|
+
:escape_html => false,
|
19
|
+
:filename => '(haml)',
|
20
|
+
:format => :html5,
|
21
|
+
:hyphenate_data_attrs => true,
|
22
|
+
:line => 1,
|
23
|
+
:mime_type => 'text/html',
|
24
|
+
:preserve => %w(textarea pre code),
|
25
|
+
:remove_whitespace => false,
|
26
|
+
:suppress_eval => false,
|
27
|
+
:ugly => false,
|
28
|
+
:cdata => false,
|
29
|
+
:parser_class => ::Hamlit::HamlParser,
|
30
|
+
:compiler_class => ::Hamlit::HamlCompiler,
|
31
|
+
:trace => false
|
32
|
+
}
|
33
|
+
|
34
|
+
@valid_formats = [:html4, :html5, :xhtml]
|
35
|
+
|
36
|
+
@buffer_option_keys = [:autoclose, :preserve, :attr_wrapper, :ugly, :format,
|
37
|
+
:encoding, :escape_html, :escape_attrs, :hyphenate_data_attrs, :cdata]
|
38
|
+
|
39
|
+
# The default option values.
|
40
|
+
# @return Hash
|
41
|
+
def self.defaults
|
42
|
+
@defaults
|
43
|
+
end
|
44
|
+
|
45
|
+
# An array of valid values for the `:format` option.
|
46
|
+
# @return Array
|
47
|
+
def self.valid_formats
|
48
|
+
@valid_formats
|
49
|
+
end
|
50
|
+
|
51
|
+
# An array of keys that will be used to provide a hash of options to
|
52
|
+
# {Haml::Buffer}.
|
53
|
+
# @return Hash
|
54
|
+
def self.buffer_option_keys
|
55
|
+
@buffer_option_keys
|
56
|
+
end
|
57
|
+
|
58
|
+
# The character that should wrap element attributes. This defaults to `'`
|
59
|
+
# (an apostrophe). Characters of this type within the attributes will be
|
60
|
+
# escaped (e.g. by replacing them with `'`) if the character is an
|
61
|
+
# apostrophe or a quotation mark.
|
62
|
+
attr_reader :attr_wrapper
|
63
|
+
|
64
|
+
# A list of tag names that should be automatically self-closed if they have
|
65
|
+
# no content. This can also contain regular expressions that match tag names
|
66
|
+
# (or any object which responds to `#===`). Defaults to `['meta', 'img',
|
67
|
+
# 'link', 'br', 'hr', 'input', 'area', 'param', 'col', 'base']`.
|
68
|
+
attr_accessor :autoclose
|
69
|
+
|
70
|
+
# The encoding to use for the HTML output.
|
71
|
+
# This can be a string or an `Encoding` Object. Note that Haml **does not**
|
72
|
+
# automatically re-encode Ruby values; any strings coming from outside the
|
73
|
+
# application should be converted before being passed into the Haml
|
74
|
+
# template. Defaults to `Encoding.default_internal`; if that's not set,
|
75
|
+
# defaults to the encoding of the Haml template; if that's `US-ASCII`,
|
76
|
+
# defaults to `"UTF-8"`.
|
77
|
+
attr_reader :encoding
|
78
|
+
|
79
|
+
# Sets whether or not to escape HTML-sensitive characters in attributes. If
|
80
|
+
# this is true, all HTML-sensitive characters in attributes are escaped. If
|
81
|
+
# it's set to false, no HTML-sensitive characters in attributes are escaped.
|
82
|
+
# If it's set to `:once`, existing HTML escape sequences are preserved, but
|
83
|
+
# other HTML-sensitive characters are escaped.
|
84
|
+
#
|
85
|
+
# Defaults to `true`.
|
86
|
+
attr_accessor :escape_attrs
|
87
|
+
|
88
|
+
# Sets whether or not to escape HTML-sensitive characters in script. If this
|
89
|
+
# is true, `=` behaves like {file:REFERENCE.md#escaping_html `&=`};
|
90
|
+
# otherwise, it behaves like {file:REFERENCE.md#unescaping_html `!=`}. Note
|
91
|
+
# that if this is set, `!=` should be used for yielding to subtemplates and
|
92
|
+
# rendering partials. See also {file:REFERENCE.md#escaping_html Escaping HTML} and
|
93
|
+
# {file:REFERENCE.md#unescaping_html Unescaping HTML}.
|
94
|
+
#
|
95
|
+
# Defaults to false.
|
96
|
+
attr_accessor :escape_html
|
97
|
+
|
98
|
+
# The name of the Haml file being parsed.
|
99
|
+
# This is only used as information when exceptions are raised. This is
|
100
|
+
# automatically assigned when working through ActionView, so it's really
|
101
|
+
# only useful for the user to assign when dealing with Haml programatically.
|
102
|
+
attr_accessor :filename
|
103
|
+
|
104
|
+
# If set to `true`, Haml will convert underscores to hyphens in all
|
105
|
+
# {file:REFERENCE.md#html5_custom_data_attributes Custom Data Attributes} As
|
106
|
+
# of Haml 4.0, this defaults to `true`.
|
107
|
+
attr_accessor :hyphenate_data_attrs
|
108
|
+
|
109
|
+
# The line offset of the Haml template being parsed. This is useful for
|
110
|
+
# inline templates, similar to the last argument to `Kernel#eval`.
|
111
|
+
attr_accessor :line
|
112
|
+
|
113
|
+
# Determines the output format. The default is `:html5`. The other options
|
114
|
+
# are `:html4` and `:xhtml`. If the output is set to XHTML, then Haml
|
115
|
+
# automatically generates self-closing tags and wraps the output of the
|
116
|
+
# Javascript and CSS-like filters inside CDATA. When the output is set to
|
117
|
+
# `:html5` or `:html4`, XML prologs are ignored. In all cases, an appropriate
|
118
|
+
# doctype is generated from `!!!`.
|
119
|
+
#
|
120
|
+
# If the mime_type of the template being rendered is `text/xml` then a
|
121
|
+
# format of `:xhtml` will be used even if the global output format is set to
|
122
|
+
# `:html4` or `:html5`.
|
123
|
+
attr :format
|
124
|
+
|
125
|
+
# The mime type that the rendered document will be served with. If this is
|
126
|
+
# set to `text/xml` then the format will be overridden to `:xhtml` even if
|
127
|
+
# it has set to `:html4` or `:html5`.
|
128
|
+
attr_accessor :mime_type
|
129
|
+
|
130
|
+
# A list of tag names that should automatically have their newlines
|
131
|
+
# preserved using the {Haml::Helpers#preserve} helper. This means that any
|
132
|
+
# content given on the same line as the tag will be preserved. For example,
|
133
|
+
# `%textarea= "Foo\nBar"` compiles to `<textarea>Foo
Bar</textarea>`.
|
134
|
+
# Defaults to `['textarea', 'pre']`. See also
|
135
|
+
# {file:REFERENCE.md#whitespace_preservation Whitespace Preservation}.
|
136
|
+
attr_accessor :preserve
|
137
|
+
|
138
|
+
# If set to `true`, all tags are treated as if both
|
139
|
+
# {file:REFERENCE.md#whitespace_removal__and_ whitespace removal} options
|
140
|
+
# were present. Use with caution as this may cause whitespace-related
|
141
|
+
# formatting errors.
|
142
|
+
#
|
143
|
+
# Defaults to `false`.
|
144
|
+
attr_reader :remove_whitespace
|
145
|
+
|
146
|
+
# Whether or not attribute hashes and Ruby scripts designated by `=` or `~`
|
147
|
+
# should be evaluated. If this is `true`, said scripts are rendered as empty
|
148
|
+
# strings.
|
149
|
+
#
|
150
|
+
# Defaults to `false`.
|
151
|
+
attr_accessor :suppress_eval
|
152
|
+
|
153
|
+
# If set to `true`, Haml makes no attempt to properly indent or format the
|
154
|
+
# HTML output. This significantly improves rendering performance but makes
|
155
|
+
# viewing the source unpleasant.
|
156
|
+
#
|
157
|
+
# Defaults to `true` in Rails production mode, and `false` everywhere else.
|
158
|
+
attr_accessor :ugly
|
159
|
+
|
160
|
+
# Whether to include CDATA sections around javascript and css blocks when
|
161
|
+
# using the `:javascript` or `:css` filters.
|
162
|
+
#
|
163
|
+
# This option also affects the `:sass`, `:scss`, `:less` and `:coffeescript`
|
164
|
+
# filters.
|
165
|
+
#
|
166
|
+
# Defaults to `false` for html, `true` for xhtml. Cannot be changed when using
|
167
|
+
# xhtml.
|
168
|
+
attr_accessor :cdata
|
169
|
+
|
170
|
+
# The parser class to use. Defaults to Haml::Parser.
|
171
|
+
attr_accessor :parser_class
|
172
|
+
|
173
|
+
# The compiler class to use. Defaults to Haml::Compiler.
|
174
|
+
attr_accessor :compiler_class
|
175
|
+
|
176
|
+
# Enable template tracing. If true, it will add a 'data-trace' attribute to
|
177
|
+
# each tag generated by Haml. The value of the attribute will be the
|
178
|
+
# source template name and the line number from which the tag was generated,
|
179
|
+
# separated by a colon. On Rails applications, the path given will be a
|
180
|
+
# relative path as from the views directory. On non-Rails applications,
|
181
|
+
# the path will be the full path.
|
182
|
+
attr_accessor :trace
|
183
|
+
|
184
|
+
def initialize(values = {}, &block)
|
185
|
+
defaults.each {|k, v| instance_variable_set :"@#{k}", v}
|
186
|
+
values.each {|k, v| send("#{k}=", v) if defaults.has_key?(k) && !v.nil?}
|
187
|
+
yield if block_given?
|
188
|
+
end
|
189
|
+
|
190
|
+
# Retrieve an option value.
|
191
|
+
# @param key The value to retrieve.
|
192
|
+
def [](key)
|
193
|
+
send key
|
194
|
+
end
|
195
|
+
|
196
|
+
# Set an option value.
|
197
|
+
# @param key The key to set.
|
198
|
+
# @param value The value to set for the key.
|
199
|
+
def []=(key, value)
|
200
|
+
send "#{key}=", value
|
201
|
+
end
|
202
|
+
|
203
|
+
[:escape_attrs, :hyphenate_data_attrs, :remove_whitespace, :suppress_eval,
|
204
|
+
:ugly].each do |method|
|
205
|
+
class_eval(<<-END)
|
206
|
+
def #{method}?
|
207
|
+
!! @#{method}
|
208
|
+
end
|
209
|
+
END
|
210
|
+
end
|
211
|
+
|
212
|
+
# @return [Boolean] Whether or not the format is XHTML.
|
213
|
+
def xhtml?
|
214
|
+
not html?
|
215
|
+
end
|
216
|
+
|
217
|
+
# @return [Boolean] Whether or not the format is any flavor of HTML.
|
218
|
+
def html?
|
219
|
+
html4? or html5?
|
220
|
+
end
|
221
|
+
|
222
|
+
# @return [Boolean] Whether or not the format is HTML4.
|
223
|
+
def html4?
|
224
|
+
format == :html4
|
225
|
+
end
|
226
|
+
|
227
|
+
# @return [Boolean] Whether or not the format is HTML5.
|
228
|
+
def html5?
|
229
|
+
format == :html5
|
230
|
+
end
|
231
|
+
|
232
|
+
def attr_wrapper=(value)
|
233
|
+
@attr_wrapper = value || self.class.defaults[:attr_wrapper]
|
234
|
+
end
|
235
|
+
|
236
|
+
# Undef :format to suppress warning. It's defined above with the `:attr`
|
237
|
+
# macro in order to make it appear in Yard's list of instance attributes.
|
238
|
+
undef :format
|
239
|
+
def format
|
240
|
+
mime_type == "text/xml" ? :xhtml : @format
|
241
|
+
end
|
242
|
+
|
243
|
+
def format=(value)
|
244
|
+
unless self.class.valid_formats.include?(value)
|
245
|
+
raise ::Hamlit::HamlError, "Invalid output format #{value.inspect}"
|
246
|
+
end
|
247
|
+
@format = value
|
248
|
+
end
|
249
|
+
|
250
|
+
undef :cdata
|
251
|
+
def cdata
|
252
|
+
xhtml? || @cdata
|
253
|
+
end
|
254
|
+
|
255
|
+
def remove_whitespace=(value)
|
256
|
+
@ugly = true if value
|
257
|
+
@remove_whitespace = value
|
258
|
+
end
|
259
|
+
|
260
|
+
def encoding=(value)
|
261
|
+
return unless value
|
262
|
+
@encoding = value.is_a?(Encoding) ? value.name : value.to_s
|
263
|
+
@encoding = "UTF-8" if @encoding.upcase == "US-ASCII"
|
264
|
+
end
|
265
|
+
|
266
|
+
# Returns a subset of options: those that {Haml::Buffer} cares about.
|
267
|
+
# All of the values here are such that when `#inspect` is called on the hash,
|
268
|
+
# it can be `Kernel#eval`ed to get the same result back.
|
269
|
+
#
|
270
|
+
# See {file:REFERENCE.md#options the Haml options documentation}.
|
271
|
+
#
|
272
|
+
# @return [{Symbol => Object}] The options hash
|
273
|
+
def for_buffer
|
274
|
+
self.class.buffer_option_keys.inject({}) do |hash, key|
|
275
|
+
hash[key] = send(key)
|
276
|
+
hash
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
private
|
281
|
+
|
282
|
+
def defaults
|
283
|
+
self.class.defaults
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
@@ -0,0 +1,800 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
require 'hamlit/parser/haml_util'
|
3
|
+
require 'hamlit/parser/haml_error'
|
4
|
+
|
5
|
+
module Hamlit
|
6
|
+
class HamlParser
|
7
|
+
include ::Hamlit::HamlUtil
|
8
|
+
|
9
|
+
attr_reader :root
|
10
|
+
|
11
|
+
# Designates an XHTML/XML element.
|
12
|
+
ELEMENT = ?%
|
13
|
+
|
14
|
+
# Designates a `<div>` element with the given class.
|
15
|
+
DIV_CLASS = ?.
|
16
|
+
|
17
|
+
# Designates a `<div>` element with the given id.
|
18
|
+
DIV_ID = ?#
|
19
|
+
|
20
|
+
# Designates an XHTML/XML comment.
|
21
|
+
COMMENT = ?/
|
22
|
+
|
23
|
+
# Designates an XHTML doctype or script that is never HTML-escaped.
|
24
|
+
DOCTYPE = ?!
|
25
|
+
|
26
|
+
# Designates script, the result of which is output.
|
27
|
+
SCRIPT = ?=
|
28
|
+
|
29
|
+
# Designates script that is always HTML-escaped.
|
30
|
+
SANITIZE = ?&
|
31
|
+
|
32
|
+
# Designates script, the result of which is flattened and output.
|
33
|
+
FLAT_SCRIPT = ?~
|
34
|
+
|
35
|
+
# Designates script which is run but not output.
|
36
|
+
SILENT_SCRIPT = ?-
|
37
|
+
|
38
|
+
# When following SILENT_SCRIPT, designates a comment that is not output.
|
39
|
+
SILENT_COMMENT = ?#
|
40
|
+
|
41
|
+
# Designates a non-parsed line.
|
42
|
+
ESCAPE = ?\\
|
43
|
+
|
44
|
+
# Designates a block of filtered text.
|
45
|
+
FILTER = ?:
|
46
|
+
|
47
|
+
# Designates a non-parsed line. Not actually a character.
|
48
|
+
PLAIN_TEXT = -1
|
49
|
+
|
50
|
+
# Keeps track of the ASCII values of the characters that begin a
|
51
|
+
# specially-interpreted line.
|
52
|
+
SPECIAL_CHARACTERS = [
|
53
|
+
ELEMENT,
|
54
|
+
DIV_CLASS,
|
55
|
+
DIV_ID,
|
56
|
+
COMMENT,
|
57
|
+
DOCTYPE,
|
58
|
+
SCRIPT,
|
59
|
+
SANITIZE,
|
60
|
+
FLAT_SCRIPT,
|
61
|
+
SILENT_SCRIPT,
|
62
|
+
ESCAPE,
|
63
|
+
FILTER
|
64
|
+
]
|
65
|
+
|
66
|
+
# The value of the character that designates that a line is part
|
67
|
+
# of a multiline string.
|
68
|
+
MULTILINE_CHAR_VALUE = ?|
|
69
|
+
|
70
|
+
# Regex to check for blocks with spaces around arguments. Not to be confused
|
71
|
+
# with multiline script.
|
72
|
+
# For example:
|
73
|
+
# foo.each do | bar |
|
74
|
+
# = bar
|
75
|
+
#
|
76
|
+
BLOCK_WITH_SPACES = /do\s*\|\s*[^\|]*\s+\|\z/
|
77
|
+
|
78
|
+
MID_BLOCK_KEYWORDS = %w[else elsif rescue ensure end when]
|
79
|
+
START_BLOCK_KEYWORDS = %w[if begin case unless]
|
80
|
+
# Try to parse assignments to block starters as best as possible
|
81
|
+
START_BLOCK_KEYWORD_REGEX = /(?:\w+(?:,\s*\w+)*\s*=\s*)?(#{START_BLOCK_KEYWORDS.join('|')})/
|
82
|
+
BLOCK_KEYWORD_REGEX = /^-?\s*(?:(#{MID_BLOCK_KEYWORDS.join('|')})|#{START_BLOCK_KEYWORD_REGEX.source})\b/
|
83
|
+
|
84
|
+
# The Regex that matches a Doctype command.
|
85
|
+
DOCTYPE_REGEX = /(\d(?:\.\d)?)?\s*([a-z]*)\s*([^ ]+)?/i
|
86
|
+
|
87
|
+
# The Regex that matches a literal string or symbol value
|
88
|
+
LITERAL_VALUE_REGEX = /:(\w*)|(["'])((?!\\|\#\{|\#@|\#\$|\2).|\\.)*\2/
|
89
|
+
|
90
|
+
ID_KEY = 'id'.freeze
|
91
|
+
CLASS_KEY = 'class'.freeze
|
92
|
+
|
93
|
+
def initialize(template, options)
|
94
|
+
@options = options
|
95
|
+
# Record the indent levels of "if" statements to validate the subsequent
|
96
|
+
# elsif and else statements are indented at the appropriate level.
|
97
|
+
@script_level_stack = []
|
98
|
+
@template_index = 0
|
99
|
+
@template_tabs = 0
|
100
|
+
|
101
|
+
match = template.rstrip.scan(/(([ \t]+)?(.*?))(?:\Z|\r\n|\r|\n)/m)
|
102
|
+
# discard the last match which is always blank
|
103
|
+
match.pop
|
104
|
+
@template = match.each_with_index.map do |(full, whitespace, text), index|
|
105
|
+
Line.new(whitespace, text.rstrip, full, index, self, false)
|
106
|
+
end
|
107
|
+
# Append special end-of-document marker
|
108
|
+
@template << Line.new(nil, '-#', '-#', @template.size, self, true)
|
109
|
+
end
|
110
|
+
|
111
|
+
def parse
|
112
|
+
@root = @parent = ParseNode.new(:root)
|
113
|
+
@flat = false
|
114
|
+
@filter_buffer = nil
|
115
|
+
@indentation = nil
|
116
|
+
@line = next_line
|
117
|
+
|
118
|
+
raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:indenting_at_start), @line.index) if @line.tabs != 0
|
119
|
+
|
120
|
+
loop do
|
121
|
+
next_line
|
122
|
+
|
123
|
+
process_indent(@line) unless @line.text.empty?
|
124
|
+
|
125
|
+
if flat?
|
126
|
+
text = @line.full.dup
|
127
|
+
text = "" unless text.gsub!(/^#{@flat_spaces}/, '')
|
128
|
+
@filter_buffer << "#{text}\n"
|
129
|
+
@line = @next_line
|
130
|
+
next
|
131
|
+
end
|
132
|
+
|
133
|
+
@tab_up = nil
|
134
|
+
process_line(@line) unless @line.text.empty?
|
135
|
+
if block_opened? || @tab_up
|
136
|
+
@template_tabs += 1
|
137
|
+
@parent = @parent.children.last
|
138
|
+
end
|
139
|
+
|
140
|
+
if !flat? && @next_line.tabs - @line.tabs > 1
|
141
|
+
raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:deeper_indenting, @next_line.tabs - @line.tabs), @next_line.index)
|
142
|
+
end
|
143
|
+
|
144
|
+
@line = @next_line
|
145
|
+
end
|
146
|
+
# Close all the open tags
|
147
|
+
close until @parent.type == :root
|
148
|
+
@root
|
149
|
+
rescue ::Hamlit::HamlError => e
|
150
|
+
e.backtrace.unshift "#{@options.filename}:#{(e.line ? e.line + 1 : @line.index + 1) + @options.line - 1}"
|
151
|
+
raise
|
152
|
+
end
|
153
|
+
|
154
|
+
def compute_tabs(line)
|
155
|
+
return 0 if line.text.empty? || !line.whitespace
|
156
|
+
|
157
|
+
if @indentation.nil?
|
158
|
+
@indentation = line.whitespace
|
159
|
+
|
160
|
+
if @indentation.include?(?\s) && @indentation.include?(?\t)
|
161
|
+
raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:cant_use_tabs_and_spaces), line.index)
|
162
|
+
end
|
163
|
+
|
164
|
+
@flat_spaces = @indentation * (@template_tabs+1) if flat?
|
165
|
+
return 1
|
166
|
+
end
|
167
|
+
|
168
|
+
tabs = line.whitespace.length / @indentation.length
|
169
|
+
return tabs if line.whitespace == @indentation * tabs
|
170
|
+
return @template_tabs + 1 if flat? && line.whitespace =~ /^#{@flat_spaces}/
|
171
|
+
|
172
|
+
message = ::Hamlit::HamlError.message(:inconsistent_indentation,
|
173
|
+
human_indentation(line.whitespace),
|
174
|
+
human_indentation(@indentation)
|
175
|
+
)
|
176
|
+
raise ::Hamlit::HamlSyntaxError.new(message, line.index)
|
177
|
+
end
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
# @private
|
182
|
+
class Line < Struct.new(:whitespace, :text, :full, :index, :parser, :eod)
|
183
|
+
alias_method :eod?, :eod
|
184
|
+
|
185
|
+
# @private
|
186
|
+
def tabs
|
187
|
+
@tabs ||= parser.compute_tabs(self)
|
188
|
+
end
|
189
|
+
|
190
|
+
def strip!(from)
|
191
|
+
self.text = text[from..-1]
|
192
|
+
self.text.lstrip!
|
193
|
+
self
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# @private
|
198
|
+
class ParseNode < Struct.new(:type, :line, :value, :parent, :children)
|
199
|
+
def initialize(*args)
|
200
|
+
super
|
201
|
+
self.children ||= []
|
202
|
+
end
|
203
|
+
|
204
|
+
def inspect
|
205
|
+
%Q[(#{type} #{value.inspect}#{children.each_with_object('') {|c, s| s << "\n#{c.inspect.gsub!(/^/, ' ')}"}})]
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# Processes and deals with lowering indentation.
|
210
|
+
def process_indent(line)
|
211
|
+
return unless line.tabs <= @template_tabs && @template_tabs > 0
|
212
|
+
|
213
|
+
to_close = @template_tabs - line.tabs
|
214
|
+
to_close.times {|i| close unless to_close - 1 - i == 0 && continuation_script?(line.text)}
|
215
|
+
end
|
216
|
+
|
217
|
+
def continuation_script?(text)
|
218
|
+
text[0] == SILENT_SCRIPT && mid_block_keyword?(text)
|
219
|
+
end
|
220
|
+
|
221
|
+
def mid_block_keyword?(text)
|
222
|
+
MID_BLOCK_KEYWORDS.include?(block_keyword(text))
|
223
|
+
end
|
224
|
+
|
225
|
+
# Processes a single line of Haml.
|
226
|
+
#
|
227
|
+
# This method doesn't return anything; it simply processes the line and
|
228
|
+
# adds the appropriate code to `@precompiled`.
|
229
|
+
def process_line(line)
|
230
|
+
case line.text[0]
|
231
|
+
when DIV_CLASS; push div(line)
|
232
|
+
when DIV_ID
|
233
|
+
return push plain(line) if %w[{ @ $].include?(line.text[1])
|
234
|
+
push div(line)
|
235
|
+
when ELEMENT; push tag(line)
|
236
|
+
when COMMENT; push comment(line.text[1..-1].lstrip)
|
237
|
+
when SANITIZE
|
238
|
+
return push plain(line.strip!(3), :escape_html) if line.text[1, 2] == '=='
|
239
|
+
return push script(line.strip!(2), :escape_html) if line.text[1] == SCRIPT
|
240
|
+
return push flat_script(line.strip!(2), :escape_html) if line.text[1] == FLAT_SCRIPT
|
241
|
+
return push plain(line.strip!(1), :escape_html) if line.text[1] == ?\s || line.text[1..2] == '#{'
|
242
|
+
push plain(line)
|
243
|
+
when SCRIPT
|
244
|
+
return push plain(line.strip!(2)) if line.text[1] == SCRIPT
|
245
|
+
line.text = line.text[1..-1]
|
246
|
+
push script(line)
|
247
|
+
when FLAT_SCRIPT; push flat_script(line.strip!(1))
|
248
|
+
when SILENT_SCRIPT
|
249
|
+
return push haml_comment(line.text[2..-1]) if line.text[1] == SILENT_COMMENT
|
250
|
+
push silent_script(line)
|
251
|
+
when FILTER; push filter(line.text[1..-1].downcase)
|
252
|
+
when DOCTYPE
|
253
|
+
return push doctype(line.text) if line.text[0, 3] == '!!!'
|
254
|
+
return push plain(line.strip!(3), false) if line.text[1, 2] == '=='
|
255
|
+
return push script(line.strip!(2), false) if line.text[1] == SCRIPT
|
256
|
+
return push flat_script(line.strip!(2), false) if line.text[1] == FLAT_SCRIPT
|
257
|
+
return push plain(line.strip!(1), false) if line.text[1] == ?\s || line.text[1..2] == '#{'
|
258
|
+
push plain(line)
|
259
|
+
when ESCAPE
|
260
|
+
line.text = line.text[1..-1]
|
261
|
+
push plain(line)
|
262
|
+
else; push plain(line)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def block_keyword(text)
|
267
|
+
return unless keyword = text.scan(BLOCK_KEYWORD_REGEX)[0]
|
268
|
+
keyword[0] || keyword[1]
|
269
|
+
end
|
270
|
+
|
271
|
+
def push(node)
|
272
|
+
@parent.children << node
|
273
|
+
node.parent = @parent
|
274
|
+
end
|
275
|
+
|
276
|
+
def plain(line, escape_html = nil)
|
277
|
+
if block_opened?
|
278
|
+
raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_plain), @next_line.index)
|
279
|
+
end
|
280
|
+
|
281
|
+
unless contains_interpolation?(line.text)
|
282
|
+
return ParseNode.new(:plain, line.index + 1, :text => line.text)
|
283
|
+
end
|
284
|
+
|
285
|
+
escape_html = @options.escape_html if escape_html.nil?
|
286
|
+
line.text = ::Hamlit::HamlUtil.unescape_interpolation(line.text)
|
287
|
+
script(line, false).tap { |n| n.value[:escape_interpolation] = true if escape_html }
|
288
|
+
end
|
289
|
+
|
290
|
+
def script(line, escape_html = nil, preserve = false)
|
291
|
+
raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:no_ruby_code, '=')) if line.text.empty?
|
292
|
+
line = handle_ruby_multiline(line)
|
293
|
+
escape_html = @options.escape_html if escape_html.nil?
|
294
|
+
|
295
|
+
keyword = block_keyword(line.text)
|
296
|
+
check_push_script_stack(keyword)
|
297
|
+
|
298
|
+
ParseNode.new(:script, line.index + 1, :text => line.text, :escape_html => escape_html,
|
299
|
+
:preserve => preserve, :keyword => keyword)
|
300
|
+
end
|
301
|
+
|
302
|
+
def flat_script(line, escape_html = nil)
|
303
|
+
raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:no_ruby_code, '~')) if line.text.empty?
|
304
|
+
script(line, escape_html, :preserve)
|
305
|
+
end
|
306
|
+
|
307
|
+
def silent_script(line)
|
308
|
+
raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:no_end), line.index) if line.text[1..-1].strip == 'end'
|
309
|
+
|
310
|
+
line = handle_ruby_multiline(line)
|
311
|
+
keyword = block_keyword(line.text)
|
312
|
+
|
313
|
+
check_push_script_stack(keyword)
|
314
|
+
|
315
|
+
if ["else", "elsif", "when"].include?(keyword)
|
316
|
+
if @script_level_stack.empty?
|
317
|
+
raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:missing_if, keyword), @line.index)
|
318
|
+
end
|
319
|
+
|
320
|
+
if keyword == 'when' and !@script_level_stack.last[2]
|
321
|
+
if @script_level_stack.last[1] + 1 == @line.tabs
|
322
|
+
@script_level_stack.last[1] += 1
|
323
|
+
end
|
324
|
+
@script_level_stack.last[2] = true
|
325
|
+
end
|
326
|
+
|
327
|
+
if @script_level_stack.last[1] != @line.tabs
|
328
|
+
message = ::Hamlit::HamlError.message(:bad_script_indent, keyword, @script_level_stack.last[1], @line.tabs)
|
329
|
+
raise ::Hamlit::HamlSyntaxError.new(message, @line.index)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
ParseNode.new(:silent_script, @line.index + 1,
|
334
|
+
:text => line.text[1..-1], :keyword => keyword)
|
335
|
+
end
|
336
|
+
|
337
|
+
def check_push_script_stack(keyword)
|
338
|
+
if ["if", "case", "unless"].include?(keyword)
|
339
|
+
# @script_level_stack contents are arrays of form
|
340
|
+
# [:keyword, stack_level, other_info]
|
341
|
+
@script_level_stack.push([keyword.to_sym, @line.tabs])
|
342
|
+
@script_level_stack.last << false if keyword == 'case'
|
343
|
+
@tab_up = true
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
def haml_comment(text)
|
348
|
+
if filter_opened?
|
349
|
+
@flat = true
|
350
|
+
@filter_buffer = String.new
|
351
|
+
@filter_buffer << "#{text}\n" unless text.empty?
|
352
|
+
text = @filter_buffer
|
353
|
+
# If we don't know the indentation by now, it'll be set in Line#tabs
|
354
|
+
@flat_spaces = @indentation * (@template_tabs+1) if @indentation
|
355
|
+
end
|
356
|
+
|
357
|
+
ParseNode.new(:haml_comment, @line.index + 1, :text => text)
|
358
|
+
end
|
359
|
+
|
360
|
+
def tag(line)
|
361
|
+
tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
|
362
|
+
nuke_inner_whitespace, action, value, last_line = parse_tag(line.text)
|
363
|
+
|
364
|
+
preserve_tag = @options.preserve.include?(tag_name)
|
365
|
+
nuke_inner_whitespace ||= preserve_tag
|
366
|
+
preserve_tag = false if @options.ugly
|
367
|
+
escape_html = (action == '&' || (action != '!' && @options.escape_html))
|
368
|
+
|
369
|
+
case action
|
370
|
+
when '/'; self_closing = true
|
371
|
+
when '~'; parse = preserve_script = true
|
372
|
+
when '='
|
373
|
+
parse = true
|
374
|
+
if value[0] == ?=
|
375
|
+
value = ::Hamlit::HamlUtil.unescape_interpolation(value[1..-1].strip)
|
376
|
+
escape_interpolation = true if escape_html
|
377
|
+
escape_html = false
|
378
|
+
end
|
379
|
+
when '&', '!'
|
380
|
+
if value[0] == ?= || value[0] == ?~
|
381
|
+
parse = true
|
382
|
+
preserve_script = (value[0] == ?~)
|
383
|
+
if value[1] == ?=
|
384
|
+
value = ::Hamlit::HamlUtil.unescape_interpolation(value[2..-1].strip)
|
385
|
+
escape_interpolation = true if escape_html
|
386
|
+
escape_html = false
|
387
|
+
else
|
388
|
+
value = value[1..-1].strip
|
389
|
+
end
|
390
|
+
elsif contains_interpolation?(value)
|
391
|
+
value = ::Hamlit::HamlUtil.unescape_interpolation(value)
|
392
|
+
escape_interpolation = true if escape_html
|
393
|
+
parse = true
|
394
|
+
escape_html = false
|
395
|
+
end
|
396
|
+
else
|
397
|
+
if contains_interpolation?(value)
|
398
|
+
value = ::Hamlit::HamlUtil.unescape_interpolation(value)
|
399
|
+
escape_interpolation = true if escape_html
|
400
|
+
parse = true
|
401
|
+
escape_html = false
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
attributes = ::Hamlit::HamlParser.parse_class_and_id(attributes)
|
406
|
+
attributes_list = []
|
407
|
+
|
408
|
+
if attributes_hashes[:new]
|
409
|
+
static_attributes, attributes_hash = attributes_hashes[:new]
|
410
|
+
::Hamlit::HamlBuffer.merge_attrs(attributes, static_attributes) if static_attributes
|
411
|
+
attributes_list << attributes_hash
|
412
|
+
end
|
413
|
+
|
414
|
+
if attributes_hashes[:old]
|
415
|
+
static_attributes = parse_static_hash(attributes_hashes[:old])
|
416
|
+
::Hamlit::HamlBuffer.merge_attrs(attributes, static_attributes) if static_attributes
|
417
|
+
attributes_list << attributes_hashes[:old] unless static_attributes || @options.suppress_eval
|
418
|
+
end
|
419
|
+
|
420
|
+
attributes_list.compact!
|
421
|
+
|
422
|
+
raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_self_closing), @next_line.index) if block_opened? && self_closing
|
423
|
+
raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:no_ruby_code, action), last_line - 1) if parse && value.empty?
|
424
|
+
raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:self_closing_content), last_line - 1) if self_closing && !value.empty?
|
425
|
+
|
426
|
+
if block_opened? && !value.empty? && !is_ruby_multiline?(value)
|
427
|
+
raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_line, tag_name), @next_line.index)
|
428
|
+
end
|
429
|
+
|
430
|
+
self_closing ||= !!(!block_opened? && value.empty? && @options.autoclose.any? {|t| t === tag_name})
|
431
|
+
value = nil if value.empty? && (block_opened? || self_closing)
|
432
|
+
line.text = value
|
433
|
+
line = handle_ruby_multiline(line) if parse
|
434
|
+
|
435
|
+
ParseNode.new(:tag, line.index + 1, :name => tag_name, :attributes => attributes,
|
436
|
+
:attributes_hashes => attributes_list, :self_closing => self_closing,
|
437
|
+
:nuke_inner_whitespace => nuke_inner_whitespace,
|
438
|
+
:nuke_outer_whitespace => nuke_outer_whitespace, :object_ref => object_ref,
|
439
|
+
:escape_html => escape_html, :preserve_tag => preserve_tag,
|
440
|
+
:preserve_script => preserve_script, :parse => parse, :value => line.text,
|
441
|
+
:escape_interpolation => escape_interpolation)
|
442
|
+
end
|
443
|
+
|
444
|
+
# Renders a line that creates an XHTML tag and has an implicit div because of
|
445
|
+
# `.` or `#`.
|
446
|
+
def div(line)
|
447
|
+
line.text = "%div#{line.text}"
|
448
|
+
tag(line)
|
449
|
+
end
|
450
|
+
|
451
|
+
# Renders an XHTML comment.
|
452
|
+
def comment(text)
|
453
|
+
if text[0..1] == '!['
|
454
|
+
revealed = true
|
455
|
+
text = text[1..-1]
|
456
|
+
else
|
457
|
+
revealed = false
|
458
|
+
end
|
459
|
+
|
460
|
+
conditional, text = balance(text, ?[, ?]) if text[0] == ?[
|
461
|
+
text.strip!
|
462
|
+
|
463
|
+
if contains_interpolation?(text)
|
464
|
+
parse = true
|
465
|
+
text = slow_unescape_interpolation(text)
|
466
|
+
else
|
467
|
+
parse = false
|
468
|
+
end
|
469
|
+
|
470
|
+
if block_opened? && !text.empty?
|
471
|
+
raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_content), @next_line.index)
|
472
|
+
end
|
473
|
+
|
474
|
+
ParseNode.new(:comment, @line.index + 1, :conditional => conditional, :text => text, :revealed => revealed, :parse => parse)
|
475
|
+
end
|
476
|
+
|
477
|
+
# Renders an XHTML doctype or XML shebang.
|
478
|
+
def doctype(text)
|
479
|
+
raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_header), @next_line.index) if block_opened?
|
480
|
+
version, type, encoding = text[3..-1].strip.downcase.scan(DOCTYPE_REGEX)[0]
|
481
|
+
ParseNode.new(:doctype, @line.index + 1, :version => version, :type => type, :encoding => encoding)
|
482
|
+
end
|
483
|
+
|
484
|
+
def filter(name)
|
485
|
+
raise ::Hamlit::HamlError.new(::Hamlit::HamlError.message(:invalid_filter_name, name)) unless name =~ /^\w+$/
|
486
|
+
|
487
|
+
if filter_opened?
|
488
|
+
@flat = true
|
489
|
+
@filter_buffer = String.new
|
490
|
+
# If we don't know the indentation by now, it'll be set in Line#tabs
|
491
|
+
@flat_spaces = @indentation * (@template_tabs+1) if @indentation
|
492
|
+
end
|
493
|
+
|
494
|
+
ParseNode.new(:filter, @line.index + 1, :name => name, :text => @filter_buffer)
|
495
|
+
end
|
496
|
+
|
497
|
+
def close
|
498
|
+
node, @parent = @parent, @parent.parent
|
499
|
+
@template_tabs -= 1
|
500
|
+
send("close_#{node.type}", node) if respond_to?("close_#{node.type}", :include_private)
|
501
|
+
end
|
502
|
+
|
503
|
+
def close_filter(_)
|
504
|
+
close_flat_section
|
505
|
+
end
|
506
|
+
|
507
|
+
def close_haml_comment(_)
|
508
|
+
close_flat_section
|
509
|
+
end
|
510
|
+
|
511
|
+
def close_flat_section
|
512
|
+
@flat = false
|
513
|
+
@flat_spaces = nil
|
514
|
+
@filter_buffer = nil
|
515
|
+
end
|
516
|
+
|
517
|
+
def close_silent_script(node)
|
518
|
+
@script_level_stack.pop if ["if", "case", "unless"].include? node.value[:keyword]
|
519
|
+
|
520
|
+
# Post-process case statements to normalize the nesting of "when" clauses
|
521
|
+
return unless node.value[:keyword] == "case"
|
522
|
+
return unless first = node.children.first
|
523
|
+
return unless first.type == :silent_script && first.value[:keyword] == "when"
|
524
|
+
return if first.children.empty?
|
525
|
+
# If the case node has a "when" child with children, it's the
|
526
|
+
# only child. Then we want to put everything nested beneath it
|
527
|
+
# beneath the case itself (just like "if").
|
528
|
+
node.children = [first, *first.children]
|
529
|
+
first.children = []
|
530
|
+
end
|
531
|
+
|
532
|
+
alias :close_script :close_silent_script
|
533
|
+
|
534
|
+
# This is a class method so it can be accessed from {Haml::Helpers}.
|
535
|
+
#
|
536
|
+
# Iterates through the classes and ids supplied through `.`
|
537
|
+
# and `#` syntax, and returns a hash with them as attributes,
|
538
|
+
# that can then be merged with another attributes hash.
|
539
|
+
def self.parse_class_and_id(list)
|
540
|
+
attributes = {}
|
541
|
+
return attributes if list.empty?
|
542
|
+
|
543
|
+
list.scan(/([#.])([-:_a-zA-Z0-9]+)/) do |type, property|
|
544
|
+
case type
|
545
|
+
when '.'
|
546
|
+
if attributes[CLASS_KEY]
|
547
|
+
attributes[CLASS_KEY] += " "
|
548
|
+
else
|
549
|
+
attributes[CLASS_KEY] = ""
|
550
|
+
end
|
551
|
+
attributes[CLASS_KEY] += property
|
552
|
+
when '#'; attributes[ID_KEY] = property
|
553
|
+
end
|
554
|
+
end
|
555
|
+
attributes
|
556
|
+
end
|
557
|
+
|
558
|
+
def parse_static_hash(text)
|
559
|
+
attributes = {}
|
560
|
+
return attributes if text.empty?
|
561
|
+
|
562
|
+
scanner = StringScanner.new(text)
|
563
|
+
scanner.scan(/\s+/)
|
564
|
+
until scanner.eos?
|
565
|
+
return unless key = scanner.scan(LITERAL_VALUE_REGEX)
|
566
|
+
return unless scanner.scan(/\s*=>\s*/)
|
567
|
+
return unless value = scanner.scan(LITERAL_VALUE_REGEX)
|
568
|
+
return unless scanner.scan(/\s*(?:,|$)\s*/)
|
569
|
+
attributes[eval(key).to_s] = eval(value).to_s
|
570
|
+
end
|
571
|
+
attributes
|
572
|
+
end
|
573
|
+
|
574
|
+
# Parses a line into tag_name, attributes, attributes_hash, object_ref, action, value
|
575
|
+
def parse_tag(text)
|
576
|
+
match = text.scan(/%([-:\w]+)([-:\w.#]*)(.+)?/)[0]
|
577
|
+
raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:invalid_tag, text)) unless match
|
578
|
+
|
579
|
+
tag_name, attributes, rest = match
|
580
|
+
|
581
|
+
if !attributes.empty? && (attributes =~ /[.#](\.|#|\z)/)
|
582
|
+
raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_element))
|
583
|
+
end
|
584
|
+
|
585
|
+
new_attributes_hash = old_attributes_hash = last_line = nil
|
586
|
+
object_ref = :nil
|
587
|
+
attributes_hashes = {}
|
588
|
+
while rest && !rest.empty?
|
589
|
+
case rest[0]
|
590
|
+
when ?{
|
591
|
+
break if old_attributes_hash
|
592
|
+
old_attributes_hash, rest, last_line = parse_old_attributes(rest)
|
593
|
+
attributes_hashes[:old] = old_attributes_hash
|
594
|
+
when ?(
|
595
|
+
break if new_attributes_hash
|
596
|
+
new_attributes_hash, rest, last_line = parse_new_attributes(rest)
|
597
|
+
attributes_hashes[:new] = new_attributes_hash
|
598
|
+
when ?[
|
599
|
+
break unless object_ref == :nil
|
600
|
+
object_ref, rest = balance(rest, ?[, ?])
|
601
|
+
else; break
|
602
|
+
end
|
603
|
+
end
|
604
|
+
|
605
|
+
if rest && !rest.empty?
|
606
|
+
nuke_whitespace, action, value = rest.scan(/(<>|><|[><])?([=\/\~&!])?(.*)?/)[0]
|
607
|
+
if nuke_whitespace
|
608
|
+
nuke_outer_whitespace = nuke_whitespace.include? '>'
|
609
|
+
nuke_inner_whitespace = nuke_whitespace.include? '<'
|
610
|
+
end
|
611
|
+
end
|
612
|
+
|
613
|
+
if @options.remove_whitespace
|
614
|
+
nuke_outer_whitespace = true
|
615
|
+
nuke_inner_whitespace = true
|
616
|
+
end
|
617
|
+
|
618
|
+
if value.nil?
|
619
|
+
value = ''
|
620
|
+
else
|
621
|
+
value.strip!
|
622
|
+
end
|
623
|
+
[tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
|
624
|
+
nuke_inner_whitespace, action, value, last_line || @line.index + 1]
|
625
|
+
end
|
626
|
+
|
627
|
+
def parse_old_attributes(text)
|
628
|
+
text = text.dup
|
629
|
+
last_line = @line.index + 1
|
630
|
+
|
631
|
+
begin
|
632
|
+
attributes_hash, rest = balance(text, ?{, ?})
|
633
|
+
rescue ::Hamlit::HamlSyntaxError => e
|
634
|
+
if text.strip[-1] == ?, && e.message == ::Hamlit::HamlError.message(:unbalanced_brackets)
|
635
|
+
text << "\n#{@next_line.text}"
|
636
|
+
last_line += 1
|
637
|
+
next_line
|
638
|
+
retry
|
639
|
+
end
|
640
|
+
|
641
|
+
raise e
|
642
|
+
end
|
643
|
+
|
644
|
+
attributes_hash = attributes_hash[1...-1] if attributes_hash
|
645
|
+
return attributes_hash, rest, last_line
|
646
|
+
end
|
647
|
+
|
648
|
+
def parse_new_attributes(text)
|
649
|
+
scanner = StringScanner.new(text)
|
650
|
+
last_line = @line.index + 1
|
651
|
+
attributes = {}
|
652
|
+
|
653
|
+
scanner.scan(/\(\s*/)
|
654
|
+
loop do
|
655
|
+
name, value = parse_new_attribute(scanner)
|
656
|
+
break if name.nil?
|
657
|
+
|
658
|
+
if name == false
|
659
|
+
scanned = ::Hamlit::HamlUtil.balance(text, ?(, ?))
|
660
|
+
text = scanned ? scanned.first : text
|
661
|
+
raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:invalid_attribute_list, text.inspect), last_line - 1)
|
662
|
+
end
|
663
|
+
attributes[name] = value
|
664
|
+
scanner.scan(/\s*/)
|
665
|
+
|
666
|
+
if scanner.eos?
|
667
|
+
text << " #{@next_line.text}"
|
668
|
+
last_line += 1
|
669
|
+
next_line
|
670
|
+
scanner.scan(/\s*/)
|
671
|
+
end
|
672
|
+
end
|
673
|
+
|
674
|
+
static_attributes = {}
|
675
|
+
dynamic_attributes = "{"
|
676
|
+
attributes.each do |name, (type, val)|
|
677
|
+
if type == :static
|
678
|
+
static_attributes[name] = val
|
679
|
+
else
|
680
|
+
dynamic_attributes << "#{inspect_obj(name)} => #{val},"
|
681
|
+
end
|
682
|
+
end
|
683
|
+
dynamic_attributes << "}"
|
684
|
+
dynamic_attributes = nil if dynamic_attributes == "{}"
|
685
|
+
|
686
|
+
return [static_attributes, dynamic_attributes], scanner.rest, last_line
|
687
|
+
end
|
688
|
+
|
689
|
+
def parse_new_attribute(scanner)
|
690
|
+
unless name = scanner.scan(/[-:\w]+/)
|
691
|
+
return if scanner.scan(/\)/)
|
692
|
+
return false
|
693
|
+
end
|
694
|
+
|
695
|
+
scanner.scan(/\s*/)
|
696
|
+
return name, [:static, true] unless scanner.scan(/=/) #/end
|
697
|
+
|
698
|
+
scanner.scan(/\s*/)
|
699
|
+
unless quote = scanner.scan(/["']/)
|
700
|
+
return false unless var = scanner.scan(/(@@?|\$)?\w+/)
|
701
|
+
return name, [:dynamic, var]
|
702
|
+
end
|
703
|
+
|
704
|
+
re = /((?:\\.|\#(?!\{)|[^#{quote}\\#])*)(#{quote}|#\{)/
|
705
|
+
content = []
|
706
|
+
loop do
|
707
|
+
return false unless scanner.scan(re)
|
708
|
+
content << [:str, scanner[1].gsub(/\\(.)/, '\1')]
|
709
|
+
break if scanner[2] == quote
|
710
|
+
content << [:ruby, balance(scanner, ?{, ?}, 1).first[0...-1]]
|
711
|
+
end
|
712
|
+
|
713
|
+
return name, [:static, content.first[1]] if content.size == 1
|
714
|
+
return name, [:dynamic,
|
715
|
+
%!"#{content.each_with_object('') {|(t, v), s| s << (t == :str ? inspect_obj(v)[1...-1] : "\#{#{v}}")}}"!]
|
716
|
+
end
|
717
|
+
|
718
|
+
def next_line
|
719
|
+
line = @template.shift || raise(StopIteration)
|
720
|
+
|
721
|
+
# `flat?' here is a little outdated,
|
722
|
+
# so we have to manually check if either the previous or current line
|
723
|
+
# closes the flat block, as well as whether a new block is opened.
|
724
|
+
line_defined = instance_variable_defined?(:@line)
|
725
|
+
@line.tabs if line_defined
|
726
|
+
unless (flat? && !closes_flat?(line) && !closes_flat?(@line)) ||
|
727
|
+
(line_defined && @line.text[0] == ?: && line.full =~ %r[^#{@line.full[/^\s+/]}\s])
|
728
|
+
return next_line if line.text.empty?
|
729
|
+
|
730
|
+
handle_multiline(line)
|
731
|
+
end
|
732
|
+
|
733
|
+
@next_line = line
|
734
|
+
end
|
735
|
+
|
736
|
+
def closes_flat?(line)
|
737
|
+
line && !line.text.empty? && line.full !~ /^#{@flat_spaces}/
|
738
|
+
end
|
739
|
+
|
740
|
+
def handle_multiline(line)
|
741
|
+
return unless is_multiline?(line.text)
|
742
|
+
line.text.slice!(-1)
|
743
|
+
loop do
|
744
|
+
new_line = @template.first
|
745
|
+
break if new_line.eod?
|
746
|
+
next @template.shift if new_line.text.strip.empty?
|
747
|
+
break unless is_multiline?(new_line.text.strip)
|
748
|
+
line.text << new_line.text.strip[0...-1]
|
749
|
+
@template.shift
|
750
|
+
end
|
751
|
+
end
|
752
|
+
|
753
|
+
# Checks whether or not `line` is in a multiline sequence.
|
754
|
+
def is_multiline?(text)
|
755
|
+
text && text.length > 1 && text[-1] == MULTILINE_CHAR_VALUE && text[-2] == ?\s && text !~ BLOCK_WITH_SPACES
|
756
|
+
end
|
757
|
+
|
758
|
+
def handle_ruby_multiline(line)
|
759
|
+
line.text.rstrip!
|
760
|
+
return line unless is_ruby_multiline?(line.text)
|
761
|
+
begin
|
762
|
+
# Use already fetched @next_line in the first loop. Otherwise, fetch next
|
763
|
+
new_line = new_line.nil? ? @next_line : @template.shift
|
764
|
+
break if new_line.eod?
|
765
|
+
next if new_line.text.empty?
|
766
|
+
line.text << " #{new_line.text.rstrip}"
|
767
|
+
end while is_ruby_multiline?(new_line.text)
|
768
|
+
next_line
|
769
|
+
line
|
770
|
+
end
|
771
|
+
|
772
|
+
# `text' is a Ruby multiline block if it:
|
773
|
+
# - ends with a comma
|
774
|
+
# - but not "?," which is a character literal
|
775
|
+
# (however, "x?," is a method call and not a literal)
|
776
|
+
# - and not "?\," which is a character literal
|
777
|
+
def is_ruby_multiline?(text)
|
778
|
+
text && text.length > 1 && text[-1] == ?, &&
|
779
|
+
!((text[-3, 2] =~ /\W\?/) || text[-3, 2] == "?\\")
|
780
|
+
end
|
781
|
+
|
782
|
+
def balance(*args)
|
783
|
+
::Hamlit::HamlUtil.balance(*args) or raise(::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:unbalanced_brackets)))
|
784
|
+
end
|
785
|
+
|
786
|
+
def block_opened?
|
787
|
+
@next_line.tabs > @line.tabs
|
788
|
+
end
|
789
|
+
|
790
|
+
# Same semantics as block_opened?, except that block_opened? uses Line#tabs,
|
791
|
+
# which doesn't interact well with filter lines
|
792
|
+
def filter_opened?
|
793
|
+
@next_line.full =~ (@indentation ? /^#{@indentation * (@template_tabs + 1)}/ : /^\s/)
|
794
|
+
end
|
795
|
+
|
796
|
+
def flat?
|
797
|
+
@flat
|
798
|
+
end
|
799
|
+
end
|
800
|
+
end
|