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 +8 -1
- data/README.md +9 -5
- data/lib/slim/compiler.rb +20 -23
- data/lib/slim/embedded_engine.rb +86 -25
- data/lib/slim/end_inserter.rb +2 -2
- data/lib/slim/engine.rb +1 -2
- data/lib/slim/grammar.rb +1 -2
- data/lib/slim/interpolation.rb +8 -6
- data/lib/slim/parser.rb +295 -258
- data/lib/slim/sections.rb +1 -1
- data/lib/slim/version.rb +1 -1
- data/test/helper.rb +4 -0
- data/test/slim/test_code_evaluation.rb +31 -6
- data/test/slim/test_code_output.rb +22 -3
- data/test/slim/test_embedded_engines.rb +7 -2
- data/test/slim/test_html_structure.rb +79 -10
- data/test/slim/test_parser_errors.rb +22 -13
- data/test/slim/test_ruby_errors.rb +11 -0
- data/test_ruby_errors.rb +191 -0
- metadata +107 -111
- data/lib/slim/rails.rb +0 -3
data/CHANGES
CHANGED
@@ -1,6 +1,13 @@
|
|
1
|
-
|
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
|
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
|
-
>
|
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-
|
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
|
-
|
63
|
+
value = case code
|
64
|
+
when 'true'
|
79
65
|
escape = false
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
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
|
data/lib/slim/embedded_engine.rb
CHANGED
@@ -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 <
|
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)),
|
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 <
|
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 <
|
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 <
|
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 <
|
101
|
-
|
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
|
-
|
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 <
|
170
|
+
class ERBEngine < Filter
|
110
171
|
def on_slim_embedded(engine, body)
|
111
|
-
Temple::ERB::Parser.new.call(
|
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 <
|
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 <
|
186
|
+
class RubyEngine < Filter
|
126
187
|
def on_slim_embedded(engine, body)
|
127
|
-
[:code, "\n" +
|
188
|
+
[:code, "\n" + CollectText.new.call(body)]
|
128
189
|
end
|
129
190
|
end
|
130
191
|
|
data/lib/slim/end_inserter.rb
CHANGED
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
|
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]
|
data/lib/slim/interpolation.rb
CHANGED
@@ -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
|
-
|
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
|
13
|
+
def initialize(error, file, line, lineno, column)
|
14
14
|
@error = error
|
15
15
|
@file = file || '(__TEMPLATE__)'
|
16
|
-
@line = line.
|
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 =
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
112
|
-
|
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
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
126
|
+
def parse_line
|
127
|
+
if @line.strip.empty?
|
128
|
+
@stacks.last << [:newline]
|
129
|
+
return
|
130
|
+
end
|
119
131
|
|
120
|
-
|
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
|
-
|
126
|
-
|
127
|
-
syntax_error! 'Unexpected text indentation', line, lineno if offset < 0
|
134
|
+
# Remove the indentation
|
135
|
+
@line.lstrip!
|
128
136
|
|
129
|
-
|
130
|
-
|
131
|
-
|
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
|
-
|
134
|
-
|
135
|
-
|
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
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
#
|
144
|
-
#
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
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
|
-
|
167
|
-
|
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
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
-
|
213
|
-
stacks
|
214
|
-
stacks <<
|
215
|
-
|
216
|
-
|
217
|
-
#
|
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
|
-
|
220
|
-
|
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
|
-
|
229
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
247
|
+
until @lines.empty?
|
248
|
+
indent = get_indent(@lines.first)
|
249
|
+
break if indent <= @indents.last
|
263
250
|
|
264
|
-
|
265
|
-
QUOTED_VALUE_REGEX = /^("[^"]*"|'[^']*')/
|
266
|
-
ATTR_SHORTHAND = {
|
267
|
-
'#' => 'id',
|
268
|
-
'.' => 'class',
|
269
|
-
}.freeze
|
251
|
+
next_line
|
270
252
|
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
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
|
278
|
-
|
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
|
-
|
281
|
-
|
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
|
-
|
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,
|
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
|
-
|
308
|
-
line[0] = ?\s
|
335
|
+
@line.slice!(0)
|
309
336
|
end
|
310
337
|
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
line = $'
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
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
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
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
|
-
|
337
|
-
|
369
|
+
# Found something where an attribute should be
|
370
|
+
@line.lstrip!
|
371
|
+
syntax_error!('Expected attribute') unless @line.empty?
|
338
372
|
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
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(
|
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 =
|
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!
|
381
|
-
syntax_error!
|
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
|
-
|
390
|
-
|
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 &&
|
424
|
+
value = value[1..-2] if value =~ DELIMITER_REGEX &&
|
425
|
+
DELIMITERS[$&] == value[-1, 1]
|
395
426
|
|
396
|
-
return
|
427
|
+
return value
|
397
428
|
end
|
398
429
|
|
399
430
|
# Helper for raising exceptions
|
400
|
-
def syntax_error!(message,
|
401
|
-
|
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
|