slim 0.6.1 → 0.7.0.beta.2

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