slim 0.9.4 → 1.0.0

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.
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