hamlit 2.9.3

Sign up to get free protection for your applications and to get access to all the features.
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