slim 0.9.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES CHANGED
@@ -1,6 +1,13 @@
1
- master
1
+ 1.0
2
2
 
3
3
  * Fixed html attribute issue in sections mode (#127)
4
+ * Obsolete directive syntax removed
5
+ * Syntax for trailing whitespace added (==' and =')
6
+ * Deprecated file 'slim/rails.rb' removed
7
+ * Parsing of #{interpolation} in markdown fixed
8
+ * Support for attributes which span multiple lines
9
+ * Dynamic attributes with value true/false are interpreted as boolean
10
+ * Support boolean attributes without value e.g. option(selected id="abc")
4
11
 
5
12
  0.9.3
6
13
 
data/README.md CHANGED
@@ -41,7 +41,7 @@ If you want to use the Slim template directly, you can use the Tilt interface:
41
41
 
42
42
  ## Syntax Highlighters
43
43
 
44
- Syntax highlight support for __Vim__ (very beta) and __Emacs__ are included in the `extra` folder. There is also a [Textmate bundle](https://github.com/fredwu/ruby-slim-textmate-bundle).
44
+ Syntax highlight support for __Emacs__ is included in the `extra` folder. There are also [Vim](https://github.com/bbommarito/vim-slim) and [Textmate](https://github.com/fredwu/ruby-slim-tmbundle) plugins.
45
45
 
46
46
  ## Template Converters
47
47
 
@@ -106,13 +106,17 @@ Here's a quick example to demonstrate what a Slim template looks like:
106
106
 
107
107
  > The equal sign tells Slim it's a Ruby call that produces output to add to the buffer (similar to Erb and Haml).
108
108
 
109
+ #### `='`
110
+
111
+ > Same as the single equal sign (`=`), except that it adds a trailing whitespace.
112
+
109
113
  #### `==`
110
114
 
111
- > Same as the single equal sign, but does not go through the `escape_html` method.
115
+ > Same as the single equal sign (`=`), but does not go through the `escape_html` method.
112
116
 
113
- #### `!`
117
+ #### `=='`
114
118
 
115
- > This is a directive. Most common example: `! doctype html # renders <!doctype html>`
119
+ > Same as the double equal sign (`==`), except that it adds a trailing whitespace.
116
120
 
117
121
  #### `/`
118
122
 
@@ -341,7 +345,7 @@ This project is released under the MIT license.
341
345
  ## Slim related projects
342
346
 
343
347
  * [Vim files](https://github.com/bbommarito/vim-slim)
344
- * [Textmate bundle](https://github.com/fredwu/ruby-slim-textmate-bundle)
348
+ * [Textmate bundle](https://github.com/fredwu/ruby-slim-tmbundle)
345
349
  * [Haml2Slim converter](https://github.com/fredwu/haml2slim)
346
350
  * [Rails 3 Generators](https://github.com/leogalmeida/slim-rails)
347
351
  * [Slim for Clojure](https://github.com/chaslemley/slim.clj)
data/lib/slim/compiler.rb CHANGED
@@ -2,8 +2,6 @@ module Slim
2
2
  # Compiles Slim expressions into Temple::HTML expressions.
3
3
  # @api private
4
4
  class Compiler < Filter
5
- set_default_options :bool_attrs => %w(selected)
6
-
7
5
  # Handle control expression `[:slim, :control, code, content]`
8
6
  #
9
7
  # @param [String] ruby code
@@ -56,35 +54,34 @@ module Slim
56
54
  end
57
55
  end
58
56
 
59
- # Handle directive expression `[:slim, :directive, type, args]`
60
- #
61
- # @param [String] type Directive type
62
- # @return [Array] Compiled temple expression
63
- def on_slim_directive(type, args)
64
- case type
65
- when 'doctype'
66
- [:html, :doctype, args]
67
- else
68
- raise "Invalid directive #{type}"
69
- end
70
- end
71
-
72
57
  # Handle attribute expression `[:slim, :attr, escape, code]`
73
58
  #
74
59
  # @param [Boolean] escape Escape html
75
60
  # @param [String] code Ruby code
76
61
  # @return [Array] Compiled temple expression
77
62
  def on_slim_attr(name, escape, code)
78
- if options[:bool_attrs].include?(name)
63
+ value = case code
64
+ when 'true'
79
65
  escape = false
80
- value = [:dynamic, "(#{code}) ? #{name.inspect} : nil"]
81
- elsif delimiter = options[:attr_delimiter][name]
82
- tmp = unique_name
83
- value = [:multi,
84
- [:code, "#{tmp} = #{code}"],
85
- [:dynamic, "#{tmp}.respond_to?(:join) ? #{tmp}.flatten.compact.join(#{delimiter.inspect}) : #{tmp}"]]
66
+ [:static, name]
67
+ when 'false', 'nil'
68
+ escape = false
69
+ [:multi]
86
70
  else
87
- value = [:dynamic, code]
71
+ tmp = unique_name
72
+ [:multi,
73
+ [:code, "#{tmp} = #{code}"],
74
+ [:case, tmp,
75
+ ['true', [:static, name]],
76
+ ['false, nil', [:static, '']],
77
+ [:else,
78
+ [:dynamic,
79
+ if delimiter = options[:attr_delimiter][name]
80
+ "#{tmp}.respond_to?(:join) ? #{tmp}.flatten.compact.join(#{delimiter.inspect}) : #{tmp}"
81
+ else
82
+ code
83
+ end
84
+ ]]]]
88
85
  end
89
86
  [:html, :attr, name, [:escape, escape, value]]
90
87
  end
@@ -1,4 +1,63 @@
1
1
  module Slim
2
+ # @api private
3
+ class CollectText < Filter
4
+ def call(exp)
5
+ @collected = ''
6
+ super(exp)
7
+ @collected
8
+ end
9
+
10
+ def on_slim_interpolate(text)
11
+ @collected << text
12
+ nil
13
+ end
14
+ end
15
+
16
+ # @api private
17
+ class CollectNewlines < Filter
18
+ def call(exp)
19
+ @collected = [:multi]
20
+ super(exp)
21
+ @collected
22
+ end
23
+
24
+ def on_newline
25
+ @collected << [:newline]
26
+ nil
27
+ end
28
+ end
29
+
30
+ # @api private
31
+ class ProtectOutput < Filter
32
+ def call(exp)
33
+ @protect = []
34
+ @collected = ''
35
+ super(exp)
36
+ @collected
37
+ end
38
+
39
+ def on_static(text)
40
+ @collected << text
41
+ nil
42
+ end
43
+
44
+ def on_slim_output(escape, text, content)
45
+ @collected << "pro#{@protect.size}tect"
46
+ @protect << [:slim, :output, escape, text, content]
47
+ nil
48
+ end
49
+
50
+ def unprotect(text)
51
+ block = [:multi]
52
+ while text =~ /pro(\d+)tect/
53
+ block << [:static, $`]
54
+ block << @protect[$1.to_i]
55
+ text = $'
56
+ end
57
+ block << [:static, text]
58
+ end
59
+ end
60
+
2
61
  # Temple filter which processes embedded engines
3
62
  # @api private
4
63
  class EmbeddedEngine < Filter
@@ -34,23 +93,17 @@ module Slim
34
93
  engine.new(Temple::ImmutableHash.new(local_options, filtered_options))
35
94
  end
36
95
 
37
- def collect_text(body)
38
- body[1..-1].inject('') do |text, exp|
39
- exp[0] == :slim && exp[1] == :interpolate ? (text << exp[2]) : text
40
- end
41
- end
42
-
43
- def collect_newlines(body)
44
- body[1..-1].inject([:multi]) do |multi, exp|
45
- exp[0] == :newline ? (multi << exp) : multi
46
- end
47
- end
48
-
49
96
  # Basic tilt engine
50
- class TiltEngine < EmbeddedEngine
97
+ class TiltEngine < Filter
51
98
  def on_slim_embedded(engine, body)
52
99
  engine = Tilt[engine] || raise("Tilt engine #{engine} is not available.")
53
- [:multi, render(engine, collect_text(body)), collect_newlines(body)]
100
+ [:multi, render(engine, collect_text(body)), CollectNewlines.new.call(body)]
101
+ end
102
+
103
+ protected
104
+
105
+ def collect_text(body)
106
+ CollectText.new.call(body)
54
107
  end
55
108
  end
56
109
 
@@ -64,7 +117,7 @@ module Slim
64
117
  end
65
118
 
66
119
  # Sass engine which supports :pretty option
67
- class SassEngine < StaticTiltEngine
120
+ class SassEngine < TiltEngine
68
121
  protected
69
122
 
70
123
  def render(engine, text)
@@ -75,7 +128,7 @@ module Slim
75
128
  end
76
129
 
77
130
  # Tilt-based engine which is fully dynamically evaluated during runtime (Slow and uncached)
78
- class DynamicTiltEngine < StaticTiltEngine
131
+ class DynamicTiltEngine < TiltEngine
79
132
  protected
80
133
 
81
134
  # Code to collect local variables
@@ -87,7 +140,7 @@ module Slim
87
140
  end
88
141
 
89
142
  # Tilt-based engine which is precompiled
90
- class PrecompiledTiltEngine < StaticTiltEngine
143
+ class PrecompiledTiltEngine < TiltEngine
91
144
  protected
92
145
 
93
146
  def render(engine, text)
@@ -97,24 +150,32 @@ module Slim
97
150
  end
98
151
 
99
152
  # Static template with interpolated ruby code
100
- class InterpolateTiltEngine < StaticTiltEngine
101
- protected
153
+ class InterpolateTiltEngine < TiltEngine
154
+ def initialize(opts = {})
155
+ super
156
+ @protect = ProtectOutput.new
157
+ end
158
+
159
+ def collect_text(body)
160
+ text = Interpolation.new.call(body)
161
+ @protect.call(text)
162
+ end
102
163
 
103
164
  def render(engine, text)
104
- [:slim, :interpolate, engine.new { text }.render]
165
+ @protect.unprotect(engine.new { text }.render)
105
166
  end
106
167
  end
107
168
 
108
169
  # ERB engine (uses the Temple ERB implementation)
109
- class ERBEngine < EmbeddedEngine
170
+ class ERBEngine < Filter
110
171
  def on_slim_embedded(engine, body)
111
- Temple::ERB::Parser.new.call(collect_text(body))
172
+ Temple::ERB::Parser.new.call(CollectText.new.call(body))
112
173
  end
113
174
  end
114
175
 
115
176
  # Tag wrapper engine
116
177
  # Generates a html tag and wraps another engine (specified via :engine option)
117
- class TagEngine < EmbeddedEngine
178
+ class TagEngine < Filter
118
179
  def on_slim_embedded(engine, body)
119
180
  content = options[:engine] ? options[:engine].new(options).on_slim_embedded(engine, body) : [:multi, body]
120
181
  [:html, :tag, options[:tag], [:html, :attrs, *options[:attributes].map {|k, v| [:html, :attr, k, [:static, v]] }], content]
@@ -122,9 +183,9 @@ module Slim
122
183
  end
123
184
 
124
185
  # Embeds ruby code
125
- class RubyEngine < EmbeddedEngine
186
+ class RubyEngine < Filter
126
187
  def on_slim_embedded(engine, body)
127
- [:code, "\n" + collect_text(body)]
188
+ [:code, "\n" + CollectText.new.call(body)]
128
189
  end
129
190
  end
130
191
 
@@ -10,8 +10,8 @@ module Slim
10
10
  #
11
11
  # @api private
12
12
  class EndInserter < Filter
13
- ELSE_REGEX = /^else|elsif|when\b/
14
- END_REGEX = /^end\b/
13
+ ELSE_REGEX = /\Aelse|elsif|when\b/
14
+ END_REGEX = /\Aend\b/
15
15
 
16
16
  # Handle multi expression `[:multi, *exps]`
17
17
  #
data/lib/slim/engine.rb CHANGED
@@ -33,7 +33,6 @@ module Slim
33
33
  # Symbol | :format | :html5 | HTML output format
34
34
  # String | :attr_wrapper | '"' | Character to wrap attributes in html (can be ' or ")
35
35
  # Hash | :attr_delimiter | {'class' => ' '} | Joining character used if multiple html attributes are supplied (e.g. id1_id2)
36
- # String list | :bool_attrs | %w(selected) | List of boolean attributes
37
36
  # Boolean | :pretty | false | Pretty html indenting (This is slower!)
38
37
  # Class | :generator | ArrayBuffer/RailsOutputBuffer | Temple code generator (default generator generates array buffer)
39
38
  #
@@ -58,7 +57,7 @@ module Slim
58
57
  use Slim::Interpolation
59
58
  use Slim::Sections, :sections, :dictionary, :dictionary_access
60
59
  use Slim::EndInserter
61
- use Slim::Compiler, :disable_capture, :attr_delimiter, :bool_attrs
60
+ use Slim::Compiler, :disable_capture, :attr_delimiter
62
61
  use Temple::HTML::Pretty, :format, :attr_wrapper, :attr_delimiter, :pretty
63
62
  filter :Escapable, :use_html_safe, :disable_escape
64
63
  filter :ControlFlow
data/lib/slim/grammar.rb CHANGED
@@ -9,8 +9,7 @@ module Slim
9
9
  [:slim, :condcomment, String, Expression] |
10
10
  [:slim, :output, Bool, String, Expression] |
11
11
  [:slim, :interpolate, String] |
12
- [:slim, :embedded, String, Expression] |
13
- [:slim, :directive, Value('doctype'), String]
12
+ [:slim, :embedded, String, Expression]
14
13
 
15
14
  HTMLAttr <<
16
15
  [:slim, :attr, String, Bool, String]
@@ -14,16 +14,18 @@ module Slim
14
14
  block = [:multi]
15
15
  until string.empty?
16
16
  case string
17
- when /^\\#\{/
17
+ when /\A\\#\{/
18
18
  # Escaped interpolation
19
- block << [:static, '#{']
19
+ # HACK: Use :slim :output because this is used by InterpolateTiltEngine
20
+ # to filter out protected strings (Issue #141).
21
+ block << [:slim, :output, false, '\'#{\'', [:multi]]
20
22
  string = $'
21
- when /^#\{/
23
+ when /\A#\{/
22
24
  # Interpolation
23
25
  string, code = parse_expression($')
24
- escape = code !~ /^\{.*\}$/
26
+ escape = code !~ /\A\{.*\}\Z/
25
27
  block << [:slim, :output, escape, escape ? code : code[1..-2], [:multi]]
26
- when /^([^#]+|#)/
28
+ when /\A([^#]+|#)/
27
29
  # Static text
28
30
  block << [:static, $&]
29
31
  string = $'
@@ -38,7 +40,7 @@ module Slim
38
40
  stack, code = [], ''
39
41
 
40
42
  until string.empty?
41
- if stack.empty? && string =~ /^\}/
43
+ if stack.empty? && string =~ /\A\}/
42
44
  # Stack is empty, this means we are finished
43
45
  # if the next character is a closing bracket
44
46
  string.slice!(0)
data/lib/slim/parser.rb CHANGED
@@ -10,20 +10,22 @@ module Slim
10
10
  class SyntaxError < StandardError
11
11
  attr_reader :error, :file, :line, :lineno, :column
12
12
 
13
- def initialize(error, file, line, lineno, column = 0)
13
+ def initialize(error, file, line, lineno, column)
14
14
  @error = error
15
15
  @file = file || '(__TEMPLATE__)'
16
- @line = line.strip
16
+ @line = line.to_s
17
17
  @lineno = lineno
18
18
  @column = column
19
19
  end
20
20
 
21
21
  def to_s
22
+ line = @line.strip
23
+ column = @column + line.size - @line.size
22
24
  %{#{error}
23
25
  #{file}, Line #{lineno}
24
26
  #{line}
25
27
  #{' ' * column}^
26
- }
28
+ }
27
29
  end
28
30
  end
29
31
 
@@ -35,7 +37,7 @@ module Slim
35
37
  # Compile string to Temple expression
36
38
  #
37
39
  # @param [String] str Slim code
38
- # @return [Array] Temple expression representing the code
40
+ # @return [Array] Temple expression representing the code]]
39
41
  def call(str)
40
42
  # Set string encoding if option is set
41
43
  if options[:encoding] && str.respond_to?(:encoding)
@@ -46,9 +48,39 @@ module Slim
46
48
  str.force_encoding(old_enc) unless str.valid_encoding?
47
49
  end
48
50
 
49
- lineno = 0
50
51
  result = [:multi]
52
+ reset(str.split($/), [result])
51
53
 
54
+ parse_line while next_line
55
+
56
+ reset
57
+ result
58
+ end
59
+
60
+ DELIMITERS = {
61
+ '(' => ')',
62
+ '[' => ']',
63
+ '{' => '}',
64
+ }.freeze
65
+ DELIMITER_REGEX = /\A[\(\[\{]/
66
+ CLOSE_DELIMITER_REGEX = /\A[\)\]\}]/
67
+
68
+ private
69
+
70
+ ATTR_NAME_REGEX = '\A\s*(\w[:\w-]*)'
71
+ QUOTED_VALUE_REGEX = /\A("[^"]*"|'[^']*')/
72
+ ATTR_SHORTCUT = {
73
+ '#' => 'id',
74
+ '.' => 'class',
75
+ }.freeze
76
+
77
+ if RUBY_VERSION > '1.9'
78
+ CLASS_ID_REGEX = /\A(#|\.)([\w\u00c0-\uFFFF][\w:\u00c0-\uFFFF-]*)/
79
+ else
80
+ CLASS_ID_REGEX = /\A(#|\.)(\w[\w:-]*)/
81
+ end
82
+
83
+ def reset(lines = nil, stacks = nil)
52
84
  # Since you can indent however you like in Slim, we need to keep a list
53
85
  # of how deeply indented you are. For instance, in a template like this:
54
86
  #
@@ -61,302 +93,295 @@ module Slim
61
93
  #
62
94
  # We uses this information to figure out how many steps we must "jump"
63
95
  # out when we see an de-indented line.
64
- indents = [0]
96
+ @indents = [0]
65
97
 
66
98
  # Whenever we want to output something, we'll *always* output it to the
67
99
  # last stack in this array. So when there's a line that expects
68
100
  # indentation, we simply push a new stack onto this array. When it
69
101
  # processes the next line, the content will then be outputted into that
70
102
  # stack.
71
- stacks = [result]
72
-
73
- # String buffer used for broken line (Lines ending with \)
74
- broken_line = nil
75
-
76
- # We have special treatment for text blocks:
77
- #
78
- # |
79
- # Hello
80
- # World!
81
- #
82
- block_indent, text_indent, in_comment = nil, nil, false
83
-
84
- str.each_line do |line|
85
- lineno += 1
103
+ @stacks = stacks
86
104
 
87
- # Remove the newline at the end
88
- line.chomp!
89
-
90
- # Handle broken lines
91
- if broken_line
92
- if broken_line[-1] == ?\\
93
- broken_line << "\n" << line
94
- next
95
- end
96
- broken_line = nil
97
- end
98
-
99
- if line.strip.empty?
100
- # This happens to be an empty line, so we'll just have to make sure
101
- # the generated code includes a newline (so the line numbers in the
102
- # stack trace for an exception matches the ones in the template).
103
- stacks.last << [:newline]
104
- next
105
- end
105
+ @lineno = 0
106
+ @lines = lines
107
+ @line = @orig_line = nil
108
+ end
106
109
 
107
- # Figure out the indentation. Kinda ugly/slow way to support tabs,
108
- # but remember that this is only done at parsing time.
109
- indent = line[/^[ \t]*/].gsub("\t", @tab).size
110
+ def next_line
111
+ if @lines.empty?
112
+ @orig_line = @line = nil
113
+ else
114
+ @orig_line = @lines.shift
115
+ @lineno += 1
116
+ @line = @orig_line.dup
117
+ end
118
+ end
110
119
 
111
- # Remove the indentation
112
- line.lstrip!
120
+ def get_indent(line)
121
+ # Figure out the indentation. Kinda ugly/slow way to support tabs,
122
+ # but remember that this is only done at parsing time.
123
+ line[/\A[ \t]*/].gsub("\t", @tab).size
124
+ end
113
125
 
114
- # Handle blocks with multiple lines
115
- if block_indent
116
- if indent > block_indent
117
- # This line happens to be indented deeper (or equal) than the block start character (|, ', /).
118
- # This means that it's a part of the block.
126
+ def parse_line
127
+ if @line.strip.empty?
128
+ @stacks.last << [:newline]
129
+ return
130
+ end
119
131
 
120
- unless in_comment
121
- # The indentation of first line of the text block determines the text base indentation.
122
- newline = text_indent ? "\n" : ''
123
- text_indent ||= indent
132
+ indent = get_indent(@line)
124
133
 
125
- # The text block lines must be at least indented as deep as the first line.
126
- offset = indent - text_indent
127
- syntax_error! 'Unexpected text indentation', line, lineno if offset < 0
134
+ # Remove the indentation
135
+ @line.lstrip!
128
136
 
129
- # Generate the additional spaces in front.
130
- stacks.last << [:slim, :interpolate, newline + (' ' * offset) + line]
131
- end
137
+ # If there's more stacks than indents, it means that the previous
138
+ # line is expecting this line to be indented.
139
+ expecting_indentation = @stacks.size > @indents.size
132
140
 
133
- stacks.last << [:newline]
134
- next
135
- end
141
+ if indent > @indents.last
142
+ # This line was actually indented, so we'll have to check if it was
143
+ # supposed to be indented or not.
144
+ syntax_error!('Unexpected indentation') unless expecting_indentation
136
145
 
137
- # It's guaranteed that we're now *not* in a block, because
138
- # the indent was less than the block start indent.
139
- block_indent = text_indent = nil
140
- in_comment = false
146
+ @indents << indent
147
+ else
148
+ # This line was *not* indented more than the line before,
149
+ # so we'll just forget about the stack that the previous line pushed.
150
+ @stacks.pop if expecting_indentation
151
+
152
+ # This line was deindented.
153
+ # Now we're have to go through the all the indents and figure out
154
+ # how many levels we've deindented.
155
+ while indent < @indents.last
156
+ @indents.pop
157
+ @stacks.pop
141
158
  end
142
159
 
143
- # If there's more stacks than indents, it means that the previous
144
- # line is expecting this line to be indented.
145
- expecting_indentation = stacks.size > indents.size
146
-
147
- if indent > indents.last
148
- # This line was actually indented, so we'll have to check if it was
149
- # supposed to be indented or not.
150
- syntax_error! 'Unexpected indentation', line, lineno unless expecting_indentation
151
-
152
- indents << indent
153
- else
154
- # This line was *not* indented more than the line before,
155
- # so we'll just forget about the stack that the previous line pushed.
156
- stacks.pop if expecting_indentation
157
-
158
- # This line was deindented.
159
- # Now we're have to go through the all the indents and figure out
160
- # how many levels we've deindented.
161
- while indent < indents.last
162
- indents.pop
163
- stacks.pop
164
- end
160
+ # This line's indentation happens lie "between" two other line's
161
+ # indentation:
162
+ #
163
+ # hello
164
+ # world
165
+ # this # <- This should not be possible!
166
+ syntax_error!('Malformed indentation') if indent != @indents.last
167
+ end
165
168
 
166
- # This line's indentation happens lie "between" two other line's
167
- # indentation:
168
- #
169
- # hello
170
- # world
171
- # this # <- This should not be possible!
172
- syntax_error! 'Malformed indentation', line, lineno if indents.last < indent
173
- end
169
+ parse_line_indicators
170
+ end
174
171
 
175
- case line[0]
176
- when ?/
177
- # Found a comment block.
178
- block = [:multi]
179
- stacks.last << if line =~ %r{^/!( ?)(.*)$}
180
- # HTML comment
181
- block_indent = indent
182
- text_indent = block_indent + ($1 ? 2 : 1)
183
- block << [:slim, :interpolate, $2] if $2
184
- [:html, :comment, block]
185
- elsif line =~ %r{^/\[\s*(.*?)\s*\]\s*$}
186
- # HTML conditional comment
187
- [:slim, :condcomment, $1, block]
188
- else
189
- # Slim comment
190
- block_indent = indent
191
- in_comment = true
192
- block
193
- end
194
- stacks << block
195
- when ?|, ?'
196
- # Found a text block.
197
- # We're now expecting the next line to be indented, so we'll need
198
- # to push a block to the stack.
199
- block = [:multi]
200
- block_indent = indent
201
- stacks.last << (line.slice!(0) == ?' ?
202
- [:multi, block, [:static, ' ']] : block)
203
- stacks << block
204
- unless line.strip.empty?
205
- block << [:slim, :interpolate, line.sub(/^( )/, '')]
206
- text_indent = block_indent + ($1 ? 2 : 1)
207
- end
208
- when ?-
209
- # Found a code block.
210
- # We expect the line to be broken or the next line to be indented.
172
+ def parse_line_indicators
173
+ case @line
174
+ when /\A\//
175
+ # Found a comment block.
176
+ if @line =~ %r{\A/!( ?)(.*)\Z}
177
+ # HTML comment
211
178
  block = [:multi]
212
- broken_line = line[1..-1].strip
213
- stacks.last << [:slim, :control, broken_line, block]
214
- stacks << block
215
- when ?=
216
- # Found an output block.
217
- # We expect the line to be broken or the next line to be indented.
179
+ @stacks.last << [:html, :comment, block]
180
+ @stacks << block
181
+ @stacks.last << [:slim, :interpolate, $2] if $2
182
+ parse_text_block($1 ? 2 : 1)
183
+ elsif @line =~ %r{\A/\[\s*(.*?)\s*\]\s*\Z}
184
+ # HTML conditional comment
218
185
  block = [:multi]
219
- escape = line[1] != ?=
220
- broken_line = escape ? line[1..-1].strip : line[2..-1].strip
221
- stacks.last << [:slim, :output, escape, broken_line, block]
222
- stacks << block
223
- when ?!
224
- # Found a directive (currently only used for doctypes)
225
- directive = line[1..-1].strip.split(/\s+/, 2)
226
- stacks.last << [:slim, :directive, directive[0].downcase, directive[1]]
186
+ @stacks.last << [:slim, :condcomment, $1, block]
187
+ @stacks << block
227
188
  else
228
- if line =~ /^(\w+):\s*$/
229
- # Embedded template detected. It is treated as block.
230
- block = [:multi]
231
- stacks.last << [:newline] << [:slim, :embedded, $1, block]
232
- stacks << block
233
- block_indent = indent
234
- next
235
- elsif line =~ /^doctype\s+/i
236
- stacks.last << [:slim, :directive, 'doctype', $'.strip]
237
- else
238
- # Found a HTML tag.
239
- tag, block, broken_line, text_indent = parse_tag(line, lineno)
240
- stacks.last << tag
241
- stacks << block if block
242
- if text_indent
243
- block_indent = indent
244
- text_indent += indent
245
- end
246
- end
189
+ # Slim comment
190
+ parse_comment_block
247
191
  end
248
- stacks.last << [:newline]
192
+ when /\A[\|']/
193
+ # Found a text block.
194
+ trailing_ws = @line.slice!(0) == ?'
195
+ if @line.strip.empty?
196
+ parse_text_block
197
+ else
198
+ @stacks.last << [:slim, :interpolate, @line.sub(/\A( )/, '')]
199
+ parse_text_block($1 ? 2 : 1)
200
+ end
201
+ @stacks.last << [:static, ' '] if trailing_ws
202
+ when /\A-/
203
+ # Found a code block.
204
+ # We expect the line to be broken or the next line to be indented.
205
+ block = [:multi]
206
+ @line.slice!(0)
207
+ @stacks.last << [:slim, :control, parse_broken_line, block]
208
+ @stacks << block
209
+ when /\A=/
210
+ # Found an output block.
211
+ # We expect the line to be broken or the next line to be indented.
212
+ @line =~ /\A=(=?)('?)/
213
+ @line = $'
214
+ block = [:multi]
215
+ @stacks.last << [:slim, :output, $1.empty?, parse_broken_line, block]
216
+ @stacks.last << [:static, ' '] unless $2.empty?
217
+ @stacks << block
218
+ when /\A(\w+):\s*\Z/
219
+ # Embedded template detected. It is treated as block.
220
+ block = [:multi]
221
+ @stacks.last << [:newline] << [:slim, :embedded, $1, block]
222
+ @stacks << block
223
+ parse_text_block
224
+ return # Don't append newline
225
+ when /\Adoctype\s+/i
226
+ # Found doctype declaration
227
+ @stacks.last << [:html, :doctype, $'.strip]
228
+ when /\A([#\.]|\w[:\w-]*)/
229
+ # Found a HTML tag.
230
+ parse_tag($&)
231
+ else
232
+ syntax_error! 'Unknown line indicator'
249
233
  end
234
+ @stacks.last << [:newline]
235
+ end
250
236
 
251
- result
237
+ def parse_comment_block
238
+ until @lines.empty? || get_indent(@lines.first) <= @indents.last
239
+ next_line
240
+ @stacks.last << [:newline]
241
+ end
252
242
  end
253
243
 
254
- DELIMITERS = {
255
- '(' => ')',
256
- '[' => ']',
257
- '{' => '}',
258
- }.freeze
259
- DELIMITER_REGEX = /^[\(\[\{]/
260
- CLOSE_DELIMITER_REGEX = /^[\)\]\}]/
244
+ def parse_text_block(offset = nil)
245
+ text_indent = offset ? @indents.last + offset : nil
261
246
 
262
- private
247
+ until @lines.empty?
248
+ indent = get_indent(@lines.first)
249
+ break if indent <= @indents.last
263
250
 
264
- ATTR_REGEX = /^\s+(\w[:\w-]*)=/
265
- QUOTED_VALUE_REGEX = /^("[^"]*"|'[^']*')/
266
- ATTR_SHORTHAND = {
267
- '#' => 'id',
268
- '.' => 'class',
269
- }.freeze
251
+ next_line
270
252
 
271
- if RUBY_VERSION > '1.9'
272
- CLASS_ID_REGEX = /^(#|\.)([\w\u00c0-\uFFFF][\w:\u00c0-\uFFFF-]*)/
273
- else
274
- CLASS_ID_REGEX = /^(#|\.)(\w[\w:-]*)/
253
+ # The indentation of first line of the text block
254
+ # determines the text base indentation.
255
+ newline = text_indent ? "\n" : ''
256
+ text_indent ||= indent
257
+
258
+ # The text block lines must be at least indented
259
+ # as deep as the first line.
260
+ if indent < text_indent
261
+ @line.lstrip!
262
+ syntax_error!('Unexpected text indentation')
263
+ end
264
+
265
+ @line.slice!(0, text_indent)
266
+
267
+ # Generate the additional spaces in front.
268
+ @stacks.last << [:newline] << [:slim, :interpolate, newline + @line]
269
+ end
275
270
  end
276
271
 
277
- def parse_tag(line, lineno)
278
- orig_line = line
272
+ def parse_broken_line
273
+ broken_line = @line.strip
274
+ while broken_line[-1] == ?\\
275
+ next_line || syntax_error!('Unexpected end of file')
276
+ broken_line << "\n" << @line.strip
277
+ end
278
+ broken_line
279
+ end
279
280
 
280
- case line
281
- when /^[#\.]/
281
+ def parse_tag(tag)
282
+ size = @line.size
283
+
284
+ if tag == '#' || tag == '.'
282
285
  tag = 'div'
283
- when /^\w[:\w-]*/
284
- tag = $&
285
- line = $'
286
286
  else
287
- syntax_error! 'Unknown line indicator', orig_line, lineno
287
+ @line.slice!(0, tag.size)
288
288
  end
289
289
 
290
+ tag = [:html, :tag, tag, parse_attributes]
291
+ @stacks.last << tag
292
+
293
+ case @line
294
+ when /\A\s*=(=?)/
295
+ # Handle output code
296
+ block = [:multi]
297
+ @line = $'
298
+ content = [:slim, :output, $1 != '=', parse_broken_line, block]
299
+ tag << content
300
+ @stacks << block
301
+ when /\A\s*\//
302
+ # Closed tag. Do nothing
303
+ when /\A\s*\Z/
304
+ # Empty content
305
+ content = [:multi]
306
+ tag << content
307
+ @stacks << content
308
+ else
309
+ # Text content
310
+ content = [:multi, [:slim, :interpolate, @line.sub(/\A( )/, '')]]
311
+ tag << content
312
+ @stacks << content
313
+ parse_text_block(size - @line.size + ($1 ? 1 : 0))
314
+ end
315
+ end
316
+
317
+ def parse_attributes
290
318
  # Now we'll have to find all the attributes. We'll store these in an
291
319
  # nested array: [[name, value], [name2, value2]]. The value is a piece
292
320
  # of Ruby code.
293
321
  attributes = [:html, :attrs]
294
322
 
295
323
  # Find any literal class/id attributes
296
- while line =~ CLASS_ID_REGEX
324
+ while @line =~ CLASS_ID_REGEX
297
325
  # The class/id attribute is :static instead of :slim :text,
298
326
  # because we don't want text interpolation in .class or #id shortcut
299
- attributes << [:html, :attr, ATTR_SHORTHAND[$1], [:static, $2]]
300
- line = $'
327
+ attributes << [:html, :attr, ATTR_SHORTCUT[$1], [:static, $2]]
328
+ @line = $'
301
329
  end
302
330
 
303
331
  # Check to see if there is a delimiter right after the tag name
304
- delimiter = ''
305
- if line =~ DELIMITER_REGEX
332
+ delimiter = nil
333
+ if @line =~ DELIMITER_REGEX
306
334
  delimiter = DELIMITERS[$&]
307
- # Replace the delimiter with a space so we can continue parsing as normal.
308
- line[0] = ?\s
335
+ @line.slice!(0)
309
336
  end
310
337
 
311
- # Parse attributes
312
- while line =~ ATTR_REGEX
313
- name = $1
314
- line = $'
315
- if line =~ QUOTED_VALUE_REGEX
316
- # Value is quoted (static)
317
- line = $'
318
- attributes << [:html, :attr, name, [:slim, :interpolate, $1[1..-2]]]
319
- else
320
- # Value is ruby code
321
- escape = line[0] != ?=
322
- line, code = parse_ruby_attribute(orig_line, escape ? line : line[1..-1], lineno, delimiter)
323
- attributes << [:slim, :attr, name, escape, code]
338
+ orig_line = @orig_line
339
+ lineno = @lineno
340
+ while true
341
+ # Parse attributes
342
+ attr_regex = delimiter ? /#{ATTR_NAME_REGEX}(=|\s|(?=#{Regexp.escape delimiter}))/ : /#{ATTR_NAME_REGEX}=/
343
+ while @line =~ attr_regex
344
+ @line = $'
345
+ name = $1
346
+ if delimiter && $2 != '='
347
+ attributes << [:slim, :attr, name, false, 'true']
348
+ elsif @line =~ QUOTED_VALUE_REGEX
349
+ # Value is quoted (static)
350
+ @line = $'
351
+ attributes << [:html, :attr, name, [:slim, :interpolate, $1[1..-2]]]
352
+ else
353
+ # Value is ruby code
354
+ escape = @line[0] != ?=
355
+ @line.slice!(0) unless escape
356
+ attributes << [:slim, :attr, name, escape, parse_ruby_attribute(delimiter)]
357
+ end
324
358
  end
325
- end
326
359
 
327
- # Find ending delimiter
328
- unless delimiter.empty?
329
- if line =~ /^\s*#{Regexp.escape delimiter}/
330
- line = $'
331
- else
332
- syntax_error! "Expected closing delimiter #{delimiter}", orig_line, lineno, orig_line.size - line.size
360
+ # No ending delimiter, attribute end
361
+ break unless delimiter
362
+
363
+ # Find ending delimiter
364
+ if @line =~ /\A\s*#{Regexp.escape delimiter}/
365
+ @line = $'
366
+ break
333
367
  end
334
- end
335
368
 
336
- content = [:multi]
337
- tag = [:html, :tag, tag, attributes, content]
369
+ # Found something where an attribute should be
370
+ @line.lstrip!
371
+ syntax_error!('Expected attribute') unless @line.empty?
338
372
 
339
- if line =~ /^\s*=(=?)/
340
- # Handle output code
341
- block = [:multi]
342
- broken_line = $'.strip
343
- content << [:slim, :output, $1 != '=', broken_line, block]
344
- [tag, block, broken_line, nil]
345
- elsif line =~ /^\s*\//
346
- # Closed tag
347
- tag.pop
348
- [tag, block, nil, nil]
349
- elsif line =~ /^\s*$/
350
- # Empty line
351
- [tag, content, nil, nil]
352
- else
353
- # Handle text content
354
- content << [:slim, :interpolate, line.sub(/^( )/, '')]
355
- [tag, content, nil, orig_line.size - line.size + ($1 ? 1 : 0)]
373
+ # Attributes span multiple lines
374
+ @stacks.last << [:newline]
375
+ next_line || syntax_error!("Expected closing delimiter #{delimiter}",
376
+ :orig_line => orig_line,
377
+ :lineno => lineno,
378
+ :column => orig_line.size)
356
379
  end
380
+
381
+ return attributes
357
382
  end
358
383
 
359
- def parse_ruby_attribute(orig_line, line, lineno, delimiter)
384
+ def parse_ruby_attribute(delimiter)
360
385
  # Delimiter stack
361
386
  stack = []
362
387
 
@@ -364,41 +389,53 @@ module Slim
364
389
  value = ''
365
390
 
366
391
  # Attribute ends with space or attribute delimiter
367
- end_regex = /^[\s#{Regexp.escape delimiter}]/
392
+ end_regex = /\A[\s#{Regexp.escape delimiter.to_s}]/
368
393
 
369
- until line.empty?
370
- if stack.empty? && line =~ end_regex
394
+ until @line.empty?
395
+ if stack.empty? && @line =~ end_regex
371
396
  # Stack is empty, this means we left the attribute value
372
397
  # if next character is space or attribute delimiter
373
398
  break
374
- elsif line =~ DELIMITER_REGEX
399
+ elsif @line =~ DELIMITER_REGEX
375
400
  # Delimiter found, push it on the stack
376
401
  stack << DELIMITERS[$&]
377
- value << line.slice!(0)
378
- elsif line =~ CLOSE_DELIMITER_REGEX
402
+ value << @line.slice!(0)
403
+ elsif @line =~ CLOSE_DELIMITER_REGEX
379
404
  # Closing delimiter found, pop it from the stack if everything is ok
380
- syntax_error! "Unexpected closing #{$&}", orig_line, lineno if stack.empty?
381
- syntax_error! "Expected closing #{stack.last}", orig_line, lineno if stack.last != $&
382
- value << line.slice!(0)
405
+ syntax_error!("Unexpected closing #{$&}") if stack.empty?
406
+ syntax_error!("Expected closing #{stack.last}") if stack.last != $&
407
+ value << @line.slice!(0)
383
408
  stack.pop
384
409
  else
385
- value << line.slice!(0)
410
+ value << @line.slice!(0)
386
411
  end
387
412
  end
388
413
 
389
- syntax_error! "Expected closing attribute delimiter #{stack.last}", orig_line, lineno unless stack.empty?
390
- syntax_error! 'Invalid empty attribute', orig_line, lineno if value.empty?
414
+ unless stack.empty?
415
+ syntax_error!("Expected closing attribute delimiter #{stack.last}")
416
+ end
417
+
418
+ if value.empty?
419
+ syntax_error!('Invalid empty attribute')
420
+ end
391
421
 
392
422
  # Remove attribute wrapper which doesn't belong to the ruby code
393
423
  # e.g id=[hash[:a] + hash[:b]]
394
- value = value[1..-2] if value =~ DELIMITER_REGEX && DELIMITERS[$&] == value[-1, 1]
424
+ value = value[1..-2] if value =~ DELIMITER_REGEX &&
425
+ DELIMITERS[$&] == value[-1, 1]
395
426
 
396
- return line, value
427
+ return value
397
428
  end
398
429
 
399
430
  # Helper for raising exceptions
400
- def syntax_error!(message, *args)
401
- raise SyntaxError.new(message, options[:file], *args)
431
+ def syntax_error!(message, args = {})
432
+ args[:orig_line] ||= @orig_line
433
+ args[:line] ||= @line
434
+ args[:lineno] ||= @lineno
435
+ args[:column] ||= args[:orig_line] && args[:line] ?
436
+ args[:orig_line].size - args[:line].size : 0
437
+ raise SyntaxError.new(message, options[:file],
438
+ args[:orig_line], args[:lineno], args[:column])
402
439
  end
403
440
  end
404
441
  end