slim 0.6.1 → 0.7.0.beta.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,57 @@
1
+ module Slim
2
+ # In Slim you don't need to close any blocks:
3
+ #
4
+ # - if Slim.awesome?
5
+ # | But of course it is!
6
+ #
7
+ # However, the parser is not smart enough (and that's a good thing) to
8
+ # automatically insert end's where they are needed. Luckily, this filter
9
+ # does *exactly* that (and it does it well!)
10
+ class EndInserter < Filter
11
+ ELSE_REGEX = /^(else|elsif|when|end)\b/
12
+ END_REGEX = /^end\b/
13
+
14
+ def on_multi(*exps)
15
+ result = [:multi]
16
+ # This variable is true if the previous line was
17
+ # (1) a control code and (2) contained indented content.
18
+ prev_indent = false
19
+
20
+ exps.each do |exp|
21
+ if control?(exp)
22
+ if prev_indent
23
+ # Two control code in a row. If this one is *not*
24
+ # an else block, we should close the previous one.
25
+ append_end(result) if exp[2] !~ ELSE_REGEX
26
+ prev_indent = exp[2].match(END_REGEX).nil?
27
+ else
28
+ # Indent if the control code contains something.
29
+ prev_indent = !empty_exp?(exp[3])
30
+ end
31
+ elsif exp[0] != :newline && prev_indent
32
+ # This is *not* a control code, so we should close the previous one.
33
+ # Ignores newlines because they will be inserted after each line.
34
+ append_end(result)
35
+ prev_indent = false
36
+ end
37
+
38
+ result << compile(exp)
39
+ end
40
+
41
+ # The last line can be a control code too.
42
+ prev_indent ? append_end(result) : result
43
+ end
44
+
45
+ private
46
+
47
+ # Appends an end.
48
+ def append_end(result)
49
+ result << [:block, 'end']
50
+ end
51
+
52
+ # Checks if an expression is a Slim control code.
53
+ def control?(exp)
54
+ exp[0] == :slim && exp[1] == :control
55
+ end
56
+ end
57
+ end
data/lib/slim/engine.rb CHANGED
@@ -1,19 +1,21 @@
1
1
  module Slim
2
- class Engine
3
- include Compiler
2
+ class Engine < Temple::Engine
3
+ use Slim::Parser
4
+ use Slim::EndInserter
5
+ use Slim::Compiler, :use_html_safe
6
+ #use Slim::Debugger
7
+ use Temple::HTML::Fast, :format, :attr_wrapper => '"', :format => :html5
8
+ filter :MultiFlattener
9
+ filter :StaticMerger
10
+ filter :DynamicInliner
11
+ generator :ArrayBuffer
4
12
 
5
- attr_reader :compiled
6
- attr_reader :optimized
7
-
8
- # @param template The .slim template to convert
9
- # @return [Slim::Engine] instance of engine
10
- def initialize(template)
11
- @template = template
12
- compile
13
- end
14
-
15
- def render(scope = Object.new, locals = {})
16
- scope.instance_eval(optimized)
13
+ def self.new(*args)
14
+ if args.first.respond_to?(:each_line)
15
+ Template.new(Hash === args.last ? args.last : {}) { args.first }
16
+ else
17
+ super
18
+ end
17
19
  end
18
20
  end
19
21
  end
@@ -0,0 +1,44 @@
1
+ module Slim
2
+ class Filter
3
+ include Temple::Utils
4
+
5
+ DEFAULT_OPTIONS = {}
6
+
7
+ def initialize(options = {})
8
+ @options = DEFAULT_OPTIONS.merge(options)
9
+ end
10
+
11
+ def compile(exp)
12
+ if exp[0] == :slim
13
+ _, type, *args = exp
14
+ else
15
+ type, *args = exp
16
+ end
17
+
18
+ if respond_to?("on_#{type}")
19
+ send("on_#{type}", *args)
20
+ else
21
+ exp
22
+ end
23
+ end
24
+
25
+ def on_control(code, content)
26
+ [:slim, :control, code, compile(content)]
27
+ end
28
+
29
+ def on_tag(name, attrs, content)
30
+ [:slim, :tag, name, attrs, compile(content)]
31
+ end
32
+
33
+ def on_multi(*exps)
34
+ [:multi, *exps.map { |exp| compile(exp) }]
35
+ end
36
+ end
37
+
38
+ class Debugger < Filter
39
+ def compile(exp)
40
+ puts exp.inspect
41
+ exp
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,37 @@
1
+ module Slim
2
+ module Helpers
3
+ def list_of(enum, &block)
4
+ enum.map do |i|
5
+ "<li>#{yield(i)}</li>"
6
+ end.join("\n")
7
+ end
8
+
9
+ def escape_html_safe(html)
10
+ html.html_safe? ? html : escape_html(html)
11
+ end
12
+
13
+ if defined?(EscapeUtils)
14
+ def escape_html(html)
15
+ EscapeUtils.escape_html(html.to_s)
16
+ end
17
+ elsif RUBY_VERSION > '1.9'
18
+ ESCAPE_HTML = {
19
+ '&' => '&amp;',
20
+ '"' => '&quot;',
21
+ '<' => '&lt;',
22
+ '>' => '&gt;',
23
+ '/' => '&#47;',
24
+ }
25
+
26
+ def escape_html(html)
27
+ html.to_s.gsub(/[&\"<>\/]/, ESCAPE_HTML)
28
+ end
29
+ else
30
+ def escape_html(html)
31
+ html.to_s.gsub(/&/n, '&amp;').gsub(/\"/n, '&quot;').gsub(/>/n, '&gt;').gsub(/</n, '&lt;').gsub(/\//, '&#47;')
32
+ end
33
+ end
34
+
35
+ module_function :escape_html, :escape_html_safe
36
+ end
37
+ end
@@ -0,0 +1,355 @@
1
+ module Slim
2
+ class Parser
3
+ class SyntaxError < StandardError
4
+ def initialize(message, line, lineno, column = 0)
5
+ @message = message
6
+ @line = line.strip
7
+ @lineno = lineno
8
+ @column = column
9
+ end
10
+
11
+ def to_s
12
+ %{#{@message}
13
+ Line #{@lineno}
14
+ #{@line}
15
+ #{' ' * @column}^
16
+ }
17
+ end
18
+ end
19
+
20
+ attr_reader :options
21
+
22
+ def initialize(options = {})
23
+ @options = options
24
+ @tab = ' ' * (options[:tabsize] || 4)
25
+ end
26
+
27
+ def compile(str)
28
+ lineno = 0
29
+ result = [:multi]
30
+
31
+ # Since you can indent however you like in Slim, we need to keep a list
32
+ # of how deeply indented you are. For instance, in a template like this:
33
+ #
34
+ # ! doctype # 0 spaces
35
+ # html # 0 spaces
36
+ # head # 1 space
37
+ # title # 4 spaces
38
+ #
39
+ # indents will then contain [0, 1, 4] (when it's processing the last line.)
40
+ #
41
+ # We uses this information to figure out how many steps we must "jump"
42
+ # out when we see an de-indented line.
43
+ indents = [0]
44
+
45
+ # Whenever we want to output something, we'll *always* output it to the
46
+ # last stack in this array. So when there's a line that expects
47
+ # indentation, we simply push a new stack onto this array. When it
48
+ # processes the next line, the content will then be outputted into that
49
+ # stack.
50
+ stacks = [result]
51
+
52
+ # String buffer used for broken line (Lines ending with \)
53
+ broken_line = nil
54
+
55
+ # We have special treatment for text blocks:
56
+ #
57
+ # |
58
+ # Hello
59
+ # World!
60
+ #
61
+ text_indent, text_base_indent = nil, nil
62
+
63
+ str.each_line do |line|
64
+ lineno += 1
65
+
66
+ # Remove the newline at the ned
67
+ line.chop!
68
+
69
+ # Handle broken lines
70
+ if broken_line
71
+ if broken_line[-1] == ?\\
72
+ broken_line << "\n#{line}"
73
+ next
74
+ end
75
+ broken_line = nil
76
+ end
77
+
78
+ # Figure out the indentation. Kinda ugly/slow way to support tabs,
79
+ # but remember that this is only done at parsing time.
80
+ indent = line[/^[ \t]*/].gsub("\t", @tab).size
81
+
82
+ # Remove the indentation
83
+ line.lstrip!
84
+
85
+ if line.strip.empty? || line[0] == ?/
86
+ # This happens to be an empty line or a comment, so we'll just have to make sure
87
+ # the generated code includes a newline (so the line numbers in the
88
+ # stack trace for an exception matches the ones in the template).
89
+ stacks.last << [:newline]
90
+ next
91
+ end
92
+
93
+ # Handle text blocks with multiple lines
94
+ if text_indent
95
+ if indent > text_indent
96
+ # This line happens to be indented deeper (or equal) than the block start character (|, ', `).
97
+ # This means that it's a part of the text block.
98
+
99
+ # The indentation of first line of the text block determines the text base indentation.
100
+ text_base_indent ||= indent
101
+
102
+ # The text block lines must be at least indented as deep as the first line.
103
+ offset = indent - text_base_indent
104
+ syntax_error! 'Unexpected text indentation', line, lineno if offset < 0
105
+
106
+ # Generate the additional spaces in front.
107
+ i = ' ' * offset
108
+ stacks.last << [:slim, :text, i + line]
109
+ stacks.last << [:newline]
110
+
111
+ # Mark this line as it's been indented as the text block start character.
112
+ indent = text_indent
113
+
114
+ next
115
+ end
116
+
117
+ # It's guaranteed that we're now *not* in a text block, because
118
+ # the indent will always be set to the text block start indent.
119
+ text_indent = text_base_indent = nil
120
+ end
121
+
122
+ # If there's more stacks than indents, it means that the previous
123
+ # line is expecting this line to be indented.
124
+ expecting_indentation = stacks.size > indents.size
125
+
126
+ if indent > indents.last
127
+ # This line was actually indented, so we'll have to check if it was
128
+ # supposed to be indented or not.
129
+ syntax_error! 'Unexpected indentation', line, lineno unless expecting_indentation
130
+
131
+ indents << indent
132
+ else
133
+ # This line was *not* indented more than the line before,
134
+ # so we'll just forget about the stack that the previous line pushed.
135
+ stacks.pop if expecting_indentation
136
+
137
+ # This line was deindented.
138
+ # Now we're have to go through the all the indents and figure out
139
+ # how many levels we've deindented.
140
+ while indent < indents.last
141
+ indents.pop
142
+ stacks.pop
143
+ end
144
+
145
+ # This line's indentation happens lie "between" two other line's
146
+ # indentation:
147
+ #
148
+ # hello
149
+ # world
150
+ # this # <- This should not be possible!
151
+ syntax_error! 'Malformed indentation', line, lineno if indents.last < indent
152
+ end
153
+
154
+ case line[0]
155
+ when ?|, ?', ?`
156
+ # Found a piece of text.
157
+
158
+ # We're now expecting the next line to be indented, so we'll need
159
+ # to push a block to the stack.
160
+ block = [:multi]
161
+ stacks.last << block
162
+ stacks << block
163
+ text_indent = indent
164
+
165
+ line.slice!(0)
166
+ if !line.strip.empty?
167
+ block << [:slim, :text, line.sub(/^( )/, '')]
168
+ text_base_indent = text_indent + ($1 ? 2 : 1)
169
+ end
170
+ when ?-, ?=
171
+ # Found a potential code block.
172
+
173
+ # First of all we need to push a exp into the stack. Anything
174
+ # indented deeper will be pushed into this exp. We'll include the
175
+ # same exp in the current-stack, which makes sure that it'll be
176
+ # included in the generated code.
177
+ block = [:multi]
178
+ if line[1] == ?=
179
+ broken_line = line[2..-1].strip
180
+ stacks.last << [:slim, :output, false, broken_line, block]
181
+ elsif line[0] == ?=
182
+ broken_line = line[1..-1].strip
183
+ stacks.last << [:slim, :output, true, broken_line, block]
184
+ else
185
+ broken_line = line[1..-1].strip
186
+ stacks.last << [:slim, :control, broken_line, block]
187
+ end
188
+ stacks << block
189
+ when ?!
190
+ # Found a directive (currently only used for doctypes)
191
+ stacks.last << [:slim, :directive, line[1..-1].strip]
192
+ else
193
+ if line =~ /^(\w+):\s*$/
194
+ # Embedded template detected. It is treated like a text block.
195
+ block = [:slim, :embedded, $1]
196
+ stacks.last << block
197
+ stacks << block
198
+ text_indent = indent
199
+ else
200
+ # Found a HTML tag.
201
+ exp, content, broken_line = parse_tag(line, lineno)
202
+ stacks.last << exp
203
+ stacks << content if content
204
+ end
205
+ end
206
+ end
207
+
208
+ result
209
+ end
210
+
211
+ private
212
+
213
+ ATTR_REGEX = /^ ([\w-]+)=/
214
+ QUOTED_VALUE_REGEX = /^("[^"]+"|'[^']+')/
215
+ ATTR_SHORTHAND = {
216
+ '#' => 'id',
217
+ '.' => 'class',
218
+ }
219
+ DELIMITERS = {
220
+ '(' => ')',
221
+ '[' => ']',
222
+ '{' => '}',
223
+ }
224
+ DELIMITER_REGEX = /^([\(\[\{])/
225
+ CLOSE_DELIMITER_REGEX = /^([\)\]\}])/
226
+ if RUBY_VERSION > '1.9'
227
+ CLASS_ID_REGEX = /^(#|\.)([\w\u00c0-\uFFFF][\w:\u00c0-\uFFFF-]*)/
228
+ else
229
+ CLASS_ID_REGEX = /^(#|\.)([\w][\w:-]*)/
230
+ end
231
+
232
+ def parse_tag(line, lineno)
233
+ orig_line = line
234
+
235
+ if line =~ /^(#|\.)/
236
+ tag = 'div'
237
+ elsif line =~ /^[\w:]+/
238
+ tag = $&
239
+ line = $'
240
+ else
241
+ syntax_error! 'Unknown line indicator', orig_line, lineno
242
+ end
243
+
244
+ # Now we'll have to find all the attributes. We'll store these in an
245
+ # nested array: [[name, value], [name2, value2]]. The value is a piece
246
+ # of Ruby code.
247
+ attributes = []
248
+
249
+ # Find any literal class/id attributes
250
+ while line =~ CLASS_ID_REGEX
251
+ attributes << [ATTR_SHORTHAND[$1], $2]
252
+ line = $'
253
+ end
254
+
255
+ # Check to see if there is a delimiter right after the tag name
256
+ delimiter = ''
257
+ if line =~ DELIMITER_REGEX
258
+ delimiter = DELIMITERS[$1]
259
+ # Replace the delimiter with a space so we can continue parsing as normal.
260
+ line[0] = ?\s
261
+ end
262
+
263
+ # Parse attributes
264
+ while line =~ ATTR_REGEX
265
+ key = $1
266
+ line = $'
267
+ if line =~ QUOTED_VALUE_REGEX
268
+ # Value is quote (static)
269
+ line = $'
270
+ value = $1[1..-2]
271
+ else
272
+ # Value is ruby code
273
+ line, value = parse_ruby_attribute(orig_line, line, lineno, delimiter)
274
+ end
275
+ attributes << [key, value]
276
+ end
277
+
278
+ # Find ending delimiter
279
+ if !delimiter.empty?
280
+ if line[0, 1] == delimiter
281
+ line.slice!(0)
282
+ else
283
+ syntax_error! "Expected closing attribute delimiter #{delimiter}", orig_line, lineno, orig_line.size - line.size
284
+ end
285
+ end
286
+
287
+ content = [:multi]
288
+ broken_line = nil
289
+
290
+ if line.strip.empty?
291
+ # If the line was empty there might be some indented content in the
292
+ # lines beneath it. We'll handle this by making this method return
293
+ # the block-variable. #compile will then push this onto the
294
+ # stacks-array.
295
+ block = content
296
+ elsif line =~ /^\s*=(=?)/
297
+ # Output
298
+ block = [:multi]
299
+ broken_line = $'.strip
300
+ content << [:slim, :output, $1 != '=', broken_line, block]
301
+ else
302
+ # Text content
303
+ line.sub!(/^ /, '')
304
+ content << [:slim, :text, line]
305
+ end
306
+
307
+ return [:slim, :tag, tag, attributes, content], block, broken_line
308
+ end
309
+
310
+ def parse_ruby_attribute(orig_line, line, lineno, delimiter)
311
+ # Delimiter stack
312
+ stack = []
313
+
314
+ # Attribute value buffer
315
+ value = ''
316
+
317
+ # Attribute ends with space or attribute delimiter
318
+ end_regex = /^[\s#{Regexp.escape delimiter}]/
319
+
320
+ until line.empty?
321
+ if stack.empty? && line =~ end_regex
322
+ # Stack is empty, this means we left the attribute value
323
+ # if next character is space or attribute delimiter
324
+ break
325
+ elsif line =~ DELIMITER_REGEX
326
+ # Delimiter found, push it on the stack
327
+ stack << DELIMITERS[$1]
328
+ value << line.slice!(0)
329
+ elsif line =~ CLOSE_DELIMITER_REGEX
330
+ # Closing delimiter found, pop it from the stack if everything is ok
331
+ syntax_error! "Unexpected closing #{$1}", orig_line, lineno if stack.empty?
332
+ syntax_error! "Expected closing #{stack.last}", orig_line, lineno if stack.last != $1
333
+ value << line.slice!(0)
334
+ stack.pop
335
+ else
336
+ value << line.slice!(0)
337
+ end
338
+ end
339
+
340
+ syntax_error! "Expected closing attribute delimiter #{stack.last}", orig_line, lineno if !stack.empty?
341
+ syntax_error! 'Invalid empty attribute', orig_line, lineno if value.empty?
342
+
343
+ # Remove attribute wrapper which doesn't belong to the ruby code
344
+ # e.g id=[hash[:a] + hash[:b]]
345
+ value = value[1..-2] if value =~ DELIMITER_REGEX && DELIMITERS[value[0, 1]] == value[-1, 1]
346
+
347
+ [line, '#{%s}' % value]
348
+ end
349
+
350
+ # A little helper for raising exceptions.
351
+ def syntax_error!(*args)
352
+ raise SyntaxError.new(*args)
353
+ end
354
+ end
355
+ end