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.
Files changed (107) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.travis.yml +45 -0
  4. data/CHANGELOG.md +676 -0
  5. data/Gemfile +28 -0
  6. data/LICENSE.txt +44 -0
  7. data/README.md +150 -0
  8. data/REFERENCE.md +266 -0
  9. data/Rakefile +117 -0
  10. data/benchmark/boolean_attribute.haml +6 -0
  11. data/benchmark/class_attribute.haml +5 -0
  12. data/benchmark/common_attribute.haml +3 -0
  13. data/benchmark/data_attribute.haml +4 -0
  14. data/benchmark/dynamic_attributes/boolean_attribute.haml +4 -0
  15. data/benchmark/dynamic_attributes/class_attribute.haml +4 -0
  16. data/benchmark/dynamic_attributes/common_attribute.haml +2 -0
  17. data/benchmark/dynamic_attributes/data_attribute.haml +2 -0
  18. data/benchmark/dynamic_attributes/id_attribute.haml +2 -0
  19. data/benchmark/dynamic_boolean_attribute.haml +4 -0
  20. data/benchmark/etc/attribute_builder.haml +5 -0
  21. data/benchmark/etc/real_sample.haml +888 -0
  22. data/benchmark/etc/real_sample.rb +11 -0
  23. data/benchmark/etc/static_analyzer.haml +1 -0
  24. data/benchmark/etc/string_interpolation.haml +2 -0
  25. data/benchmark/etc/tags.haml +3 -0
  26. data/benchmark/etc/tags_loop.haml +2 -0
  27. data/benchmark/ext/build_data.rb +17 -0
  28. data/benchmark/ext/build_id.rb +13 -0
  29. data/benchmark/id_attribute.haml +3 -0
  30. data/benchmark/plain.haml +4 -0
  31. data/benchmark/script.haml +4 -0
  32. data/benchmark/slim/LICENSE +21 -0
  33. data/benchmark/slim/context.rb +11 -0
  34. data/benchmark/slim/run-benchmarks.rb +94 -0
  35. data/benchmark/slim/view.erb +23 -0
  36. data/benchmark/slim/view.haml +18 -0
  37. data/benchmark/slim/view.slim +17 -0
  38. data/benchmark/utils/benchmark_ips_extension.rb +43 -0
  39. data/bin/bench +77 -0
  40. data/bin/console +11 -0
  41. data/bin/ruby +3 -0
  42. data/bin/setup +7 -0
  43. data/bin/stackprof +27 -0
  44. data/bin/test +24 -0
  45. data/exe/hamlit +6 -0
  46. data/ext/hamlit/extconf.rb +10 -0
  47. data/ext/hamlit/hamlit.c +553 -0
  48. data/ext/hamlit/hescape.c +108 -0
  49. data/ext/hamlit/hescape.h +20 -0
  50. data/hamlit.gemspec +45 -0
  51. data/lib/hamlit.rb +11 -0
  52. data/lib/hamlit/attribute_builder.rb +173 -0
  53. data/lib/hamlit/attribute_compiler.rb +123 -0
  54. data/lib/hamlit/attribute_parser.rb +110 -0
  55. data/lib/hamlit/cli.rb +130 -0
  56. data/lib/hamlit/compiler.rb +97 -0
  57. data/lib/hamlit/compiler/children_compiler.rb +112 -0
  58. data/lib/hamlit/compiler/comment_compiler.rb +36 -0
  59. data/lib/hamlit/compiler/doctype_compiler.rb +46 -0
  60. data/lib/hamlit/compiler/script_compiler.rb +102 -0
  61. data/lib/hamlit/compiler/silent_script_compiler.rb +24 -0
  62. data/lib/hamlit/compiler/tag_compiler.rb +74 -0
  63. data/lib/hamlit/engine.rb +37 -0
  64. data/lib/hamlit/error.rb +15 -0
  65. data/lib/hamlit/escapable.rb +13 -0
  66. data/lib/hamlit/filters.rb +75 -0
  67. data/lib/hamlit/filters/base.rb +12 -0
  68. data/lib/hamlit/filters/cdata.rb +20 -0
  69. data/lib/hamlit/filters/coffee.rb +17 -0
  70. data/lib/hamlit/filters/css.rb +33 -0
  71. data/lib/hamlit/filters/erb.rb +10 -0
  72. data/lib/hamlit/filters/escaped.rb +22 -0
  73. data/lib/hamlit/filters/javascript.rb +33 -0
  74. data/lib/hamlit/filters/less.rb +20 -0
  75. data/lib/hamlit/filters/markdown.rb +10 -0
  76. data/lib/hamlit/filters/plain.rb +29 -0
  77. data/lib/hamlit/filters/preserve.rb +22 -0
  78. data/lib/hamlit/filters/ruby.rb +10 -0
  79. data/lib/hamlit/filters/sass.rb +15 -0
  80. data/lib/hamlit/filters/scss.rb +15 -0
  81. data/lib/hamlit/filters/text_base.rb +25 -0
  82. data/lib/hamlit/filters/tilt_base.rb +49 -0
  83. data/lib/hamlit/force_escapable.rb +29 -0
  84. data/lib/hamlit/helpers.rb +15 -0
  85. data/lib/hamlit/html.rb +14 -0
  86. data/lib/hamlit/identity.rb +13 -0
  87. data/lib/hamlit/object_ref.rb +30 -0
  88. data/lib/hamlit/parser.rb +49 -0
  89. data/lib/hamlit/parser/MIT-LICENSE +20 -0
  90. data/lib/hamlit/parser/README.md +30 -0
  91. data/lib/hamlit/parser/haml_buffer.rb +348 -0
  92. data/lib/hamlit/parser/haml_compiler.rb +553 -0
  93. data/lib/hamlit/parser/haml_error.rb +61 -0
  94. data/lib/hamlit/parser/haml_helpers.rb +727 -0
  95. data/lib/hamlit/parser/haml_options.rb +286 -0
  96. data/lib/hamlit/parser/haml_parser.rb +800 -0
  97. data/lib/hamlit/parser/haml_util.rb +288 -0
  98. data/lib/hamlit/parser/haml_xss_mods.rb +109 -0
  99. data/lib/hamlit/rails_helpers.rb +51 -0
  100. data/lib/hamlit/rails_template.rb +59 -0
  101. data/lib/hamlit/railtie.rb +10 -0
  102. data/lib/hamlit/ruby_expression.rb +32 -0
  103. data/lib/hamlit/string_splitter.rb +88 -0
  104. data/lib/hamlit/template.rb +28 -0
  105. data/lib/hamlit/utils.rb +18 -0
  106. data/lib/hamlit/version.rb +4 -0
  107. 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&#x000A;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