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