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.
data/benchmarks/run.rb ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.join(File.dirname(__FILE__), '..', 'lib') << File.join(File.dirname(__FILE__), 'src')
4
+
5
+ require 'slim'
6
+ require 'complex_view'
7
+
8
+ require 'benchmark'
9
+ require 'ostruct'
10
+ require 'erubis'
11
+ require 'erb'
12
+ require 'haml'
13
+
14
+ class SlimBenchmarks
15
+ def initialize(iterations)
16
+ @iterations = (iterations || 1000).to_i
17
+ @benches = []
18
+
19
+ tpl_erb = File.read(File.dirname(__FILE__) + '/src/complex.erb')
20
+ tpl_haml = File.read(File.dirname(__FILE__) + '/src/complex.haml')
21
+ tpl_slim = File.read(File.dirname(__FILE__) + '/src/complex.slim')
22
+
23
+ view = ComplexView.new
24
+ eview = OpenStruct.new(:header => view.header, :item => view.item).instance_eval{ binding }
25
+
26
+ erb = ERB.new(tpl_erb)
27
+ erubis = Erubis::Eruby.new(tpl_erb)
28
+ fast_erubis = Erubis::FastEruby.new(tpl_erb)
29
+ haml = Haml::Engine.new(tpl_haml)
30
+ haml_ugly = Haml::Engine.new(tpl_haml, :ugly => true)
31
+ slim = Slim::Engine.new(tpl_slim)
32
+
33
+ bench('erb') { ERB.new(tpl_erb).result(eview) }
34
+ bench('erubis') { Erubis::Eruby.new(tpl_erb).result(eview) }
35
+ bench('fast erubis') { Erubis::Eruby.new(tpl_erb).result(eview) }
36
+ bench('slim') { Slim::Engine.new(tpl_slim).render(view) }
37
+ bench('haml') { Haml::Engine.new(tpl_haml).render(view) }
38
+ bench('haml ugly') { Haml::Engine.new(tpl_haml, :ugly => true).render(view) }
39
+ bench('erb (cached)') { erb.result(eview) }
40
+ bench('erubis (cached)') { erubis.result(eview) }
41
+ bench('fast erubis (cached)') { fast_erubis.result(eview) }
42
+ bench('slim (cached)') { slim.render(view) }
43
+ bench('haml (cached)') { haml.render(view) }
44
+ bench('haml ugly (cached)') { haml_ugly.render(view) }
45
+ end
46
+
47
+ def run
48
+ puts "#{@iterations} Iterations"
49
+ Benchmark.bmbm do |x|
50
+ @benches.each do |name, block|
51
+ x.report name.to_s do
52
+ @iterations.to_i.times { block.call }
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def bench(name, &block)
59
+ @benches.push([name, block])
60
+ end
61
+ end
62
+
63
+ SlimBenchmarks.new(ARGV[0]).run
@@ -0,0 +1,23 @@
1
+ <!DOCTYPE HTML>
2
+
3
+ <html>
4
+ <head>
5
+ <title>Simple Benchmark</title>
6
+ </head>
7
+ <body>
8
+ <h1><%= header %></h1>
9
+ <% unless item.empty? %>
10
+ <ul>
11
+ <% for i in item %>
12
+ <% if i[:current] %>
13
+ <li><strong><%= i[:name] %></strong></li>
14
+ <% else %>
15
+ <li><a href="<%= i[:url] %>"><%= i[:name] %></a></li>
16
+ <% end %>
17
+ <% end %>
18
+ </ul>
19
+ <% else %>
20
+ <p>The list is empty.</p>
21
+ <% end %>
22
+ </body>
23
+ </html>
@@ -0,0 +1,18 @@
1
+ !!! html
2
+
3
+ %body
4
+ %head
5
+ %title Simple Benchmark
6
+ %body
7
+ %h1= header
8
+ - unless item.empty?
9
+ %ul
10
+ - for i in item
11
+ - if i[:current]
12
+ %li
13
+ %strong= i[:name]
14
+ - else
15
+ %li
16
+ %a{:href => i[:url]}= i[:name]
17
+ - else
18
+ %p The list is empty.
@@ -0,0 +1,18 @@
1
+ ! doctype html
2
+
3
+ body
4
+ head
5
+ title Simple Benchmark
6
+ body
7
+ h1 == header
8
+ - unless item.empty?
9
+ ul
10
+ - for i in item
11
+ - if i[:current]
12
+ li
13
+ strong == i[:name]
14
+ - else
15
+ li
16
+ a href="#{i[:url]}" == i[:name]
17
+ - else
18
+ p The list is empty.
@@ -0,0 +1,17 @@
1
+ require 'tilt'
2
+
3
+ class ComplexView
4
+ include Tilt::CompileSite
5
+
6
+ def header
7
+ "Colors"
8
+ end
9
+
10
+ def item
11
+ items = []
12
+ items << { :name => 'red', :current => true, :url => '#red' }
13
+ items << { :name => 'green', :current => false, :url => '#green' }
14
+ items << { :name => 'blue', :current => false, :url => '#blue' }
15
+ items
16
+ end
17
+ end
data/lib/slim.rb CHANGED
@@ -1,20 +1,24 @@
1
1
  # encoding: utf-8
2
2
 
3
- $:.unshift File.dirname(__FILE__)
3
+ require 'temple'
4
+ require 'tilt'
4
5
 
5
- require 'bundler/setup'
6
- require 'escape_utils'
6
+ require 'slim/helpers'
7
+ require 'slim/parser'
8
+ require 'slim/filter'
9
+ require 'slim/end_inserter'
10
+ require 'slim/embedded_engine'
7
11
  require 'slim/compiler'
8
12
  require 'slim/engine'
13
+ require 'slim/template'
9
14
 
10
- module Slim
11
- class << self
12
- def version
13
- '0.6.1'
14
- end
15
+ begin
16
+ require 'escape_utils'
17
+ rescue LoadError
18
+ end
15
19
 
16
- def escape_html(html)
17
- EscapeUtils.escape_html(html.to_s)
18
- end
20
+ module Slim
21
+ def self.version
22
+ Slim::VERSION
19
23
  end
20
24
  end
data/lib/slim/compiler.rb CHANGED
@@ -1,161 +1,95 @@
1
- # encoding: utf-8
1
+ module Slim
2
+ # Compiles Slim expressions into Temple::HTML expressions.
3
+ class Compiler < Filter
4
+ def on_text(string)
5
+ if string.include?('#{')
6
+ [:dynamic, escape_interpolation(string)]
7
+ else
8
+ [:static, string]
9
+ end
10
+ end
2
11
 
3
- require 'slim/optimizer'
12
+ def on_control(code, content)
13
+ [:multi,
14
+ [:block, code],
15
+ compile(content)]
16
+ end
4
17
 
5
- module Slim
6
- module Compiler
7
- include Optimizer
8
-
9
- AUTOCLOSED = %w{meta img link br hr input area param col base}
10
- CONTROL_WORDS = %w{if unless do}
11
- ELSE_CONTROL_WORDS = %w{else elsif}
12
-
13
- REGEX_LINE_PARSER = /^(\s*)(!?`?\|?-?=?\/?\w*)\(?((\S*[#.]\S+)?(?:\s*(?:\w|-)*="[^=]+")*)?\)?(.*)/
14
-
15
- REGEX_LINE_CONTAINS_OUTPUT_CODE = /^\s*=(.*)/
16
- REGEX_LINE_CONTAINS_METHOD_DETECTED = /^((?:(?!#{CONTROL_WORDS * '\b|'}\b).)*)/
17
- REGEX_METHOD_HAS_NO_PARENTHESES = /^\w+\s+\S+/
18
- REGEX_CODE_BLOCK_DETECTED = / do ?.*$/
19
- REGEX_CODE_CONTROL_WORD_DETECTED = /(?:\s|(\())(#{CONTROL_WORDS * '|'})\b\s?(.*)$/
20
- REGEX_CODE_ELSE_CONTROL_WORD_DETECTED = /^#{ELSE_CONTROL_WORDS * '\b|'}\b/
21
- REGEX_FIND_HTML_ATTR_ID = /#([^.\s]+)/
22
- REGEX_FIND_HTML_ATTR_CLASSES = /\.([^#\s]+)/
23
-
24
- def compile
25
- @_buffer = ['_buf = [];']
26
- in_text = false
27
- enders = []
28
- text_indent = last_indent = -1
29
-
30
- @template.each_line do |line|
31
- line.chomp!
32
- line.rstrip!
33
-
34
- if line.empty?
35
- @_buffer << '_buf << "<br/>";' if in_text
36
- next
37
- end
18
+ def on_embedded(engine, *body)
19
+ EmbeddedEngine[engine].compile(body)
20
+ end
38
21
 
39
- line =~ REGEX_LINE_PARSER
22
+ # why is escaping not handled by temple?
23
+ def on_output(escape, code, content)
24
+ if empty_exp?(content)
25
+ [:dynamic, escape ? escape_code(code) : code]
26
+ else
27
+ on_output_block(escape, code, content)
28
+ end
29
+ end
40
30
 
41
- indent = $1.to_s.length
31
+ def on_output_block(escape, code, content)
32
+ tmp1, tmp2 = tmp_var, tmp_var
42
33
 
43
- if in_text && indent > text_indent
44
- spaces = indent - text_indent
45
- @_buffer << "_buf << \"#{(' '*(spaces - 1)) + line.lstrip}\";"
46
- next
47
- end
34
+ [:multi,
35
+ # Capture the result of the code in a variable. We can't do
36
+ # `[:dynamic, code]` because it's probably not a complete
37
+ # expression (which is a requirement for Temple).
38
+ [:block, "#{tmp1} = #{code}"],
48
39
 
49
- marker = $2
50
- attrs = $3
51
- shortcut_attrs = $4
52
- string = $5
53
-
54
- # Remove the first space, but allow people to pad if they want.
55
- string.slice!(0) if string =~ /^\s/
56
-
57
- line_type = case marker
58
- when '`', '|' then :text
59
- when '-' then :control_code
60
- when '=' then :output_code
61
- when '!' then :declaration
62
- when '/' then next # simply ignore any ruby code comments
63
- else :markup
64
- end
65
-
66
- unless attrs.empty?
67
- normalize_attributes!(attrs) if shortcut_attrs
68
- attrs.gsub!('"', '\"')
69
- attrs = " #{attrs}" unless attrs =~ /^\s/
70
- end
40
+ # Capture the content of a block in a separate buffer. This means
41
+ # that `yield` will not output the content to the current buffer,
42
+ # but rather return the output.
43
+ [:capture, tmp2,
44
+ compile(content)],
71
45
 
72
- unless indent > last_indent
73
- begin
74
- break if enders.empty?
75
- continue_closing = true
76
- ender, ender_indent = enders.pop
77
-
78
- unless ender_indent < indent || ender_indent == indent && string =~ REGEX_CODE_ELSE_CONTROL_WORD_DETECTED
79
- @_buffer << ender
80
- else
81
- enders << [ender, ender_indent]
82
- continue_closing = false
83
- end
84
- end while continue_closing == true
85
- end
46
+ # Make sure that `yield` returns the output.
47
+ [:block, tmp2],
86
48
 
87
- last_indent = indent
88
-
89
- case line_type
90
- when :markup
91
- if AUTOCLOSED.include?(marker)
92
- @_buffer << "_buf << \"<#{marker}#{attrs}/>\";"
93
- else
94
- # prepends "div" to the shortcut form of attrs if no marker is given
95
- marker = 'div' if shortcut_attrs && marker.empty?
96
-
97
- enders << ["_buf << \"</#{marker}>\";", indent]
98
- @_buffer << "_buf << \"<#{marker}#{attrs}>\";"
99
- end
100
-
101
- if string =~ REGEX_LINE_CONTAINS_OUTPUT_CODE
102
- @_buffer << "_buf << #{parse_string($1.strip)};"
103
- else
104
- @_buffer << "_buf << \"#{string}\";" unless string.empty?
105
- end
106
- when :text
107
- in_text = true
108
- text_indent = indent
109
- @_buffer << "_buf << \"#{string}\";" unless string.empty?
110
- when :control_code
111
- enders << ['end;', indent] unless enders.detect{|e| e[0] == 'end;' && e[1] == indent}
112
- @_buffer << "#{string};"
113
- when :output_code
114
- enders << ['end;', indent] if string =~ REGEX_CODE_BLOCK_DETECTED
115
- @_buffer << "_buf << #{parse_string(string)};"
116
- when :declaration
117
- @_buffer << "_buf << \"<!#{string}>\";"
118
- else
119
- raise NotImplementedError.new("Don't know how to parse line: #{line}")
120
- end
121
- end # template iterator
49
+ # Close the block.
50
+ [:block, "end"],
122
51
 
123
- enders.reverse_each do |t|
124
- @_buffer << t[0]
52
+ # Output the content.
53
+ on_output(escape, tmp1, [:multi])]
54
+ end
55
+
56
+ def on_directive(type)
57
+ case type
58
+ when /^doctype/
59
+ [:html, :doctype, $'.strip]
60
+ else
125
61
  end
62
+ end
126
63
 
127
- @_buffer << "_buf.join;"
64
+ def on_tag(name, attrs, content)
65
+ attrs = attrs.inject([:html, :attrs]) do |m, (key, value)|
66
+ if value.include?('#{')
67
+ value = [:dynamic, escape_interpolation(value)]
68
+ else
69
+ value = [:static, value]
70
+ end
71
+ m << [:html, :basicattr, [:static, key], value]
72
+ end
128
73
 
129
- @compiled = @_buffer.join
130
- @optimized = optimize!
74
+ [:html, :tag, name, attrs, compile(content)]
131
75
  end
132
76
 
133
77
  private
134
78
 
135
- def parse_string(string)
136
- if string =~ REGEX_LINE_CONTAINS_OUTPUT_CODE
137
- $1.strip
138
- else
139
- parenthesesify_method!(string) if string =~ REGEX_METHOD_HAS_NO_PARENTHESES
140
- wraps_with_slim_escape!(string) unless string =~ REGEX_CODE_BLOCK_DETECTED
141
- string.strip
79
+ def escape_interpolation(string)
80
+ string.gsub!(/(.?)\#\{(.*?)\}/) do
81
+ $1 == '\\' ? $& : "#{$1}#\{#{escape_code($2)}}"
142
82
  end
83
+ '"%s"' % string
143
84
  end
144
85
 
145
- # adds a pair of parentheses to the method
146
- def parenthesesify_method!(string)
147
- string.sub!(' ', '(') && string.sub!(REGEX_CODE_CONTROL_WORD_DETECTED, '\1) \2 \3') || string << ')'
148
- end
149
-
150
- # escapes the string
151
- def wraps_with_slim_escape!(string)
152
- string.sub!(REGEX_LINE_CONTAINS_METHOD_DETECTED, 'Slim.escape_html(\1)')
86
+ def escape_code(param)
87
+ "Slim::Helpers.escape_html#{@options[:use_html_safe] ? '_safe' : ''}((#{param}))"
153
88
  end
154
89
 
155
- # converts 'p#hello.world.mate' to 'p id="hello" class="world mate"'
156
- def normalize_attributes!(string)
157
- string.sub!(REGEX_FIND_HTML_ATTR_ID, ' id="\1"')
158
- string.sub!(REGEX_FIND_HTML_ATTR_CLASSES, ' class="\1"') && string.gsub!('.', ' ')
90
+ def tmp_var
91
+ @tmp_var ||= 0
92
+ "_slimtmp#{@tmp_var += 1}"
159
93
  end
160
94
  end
161
95
  end
@@ -0,0 +1,108 @@
1
+ module Slim
2
+ class EmbeddedEngine
3
+ @engines = {}
4
+
5
+ attr_reader :options
6
+
7
+ def initialize(options)
8
+ @options = options
9
+ end
10
+
11
+ def self.[](name)
12
+ engine = @engines[name.to_s]
13
+ raise "Invalid embedded engine #{name}" if !engine
14
+ engine.dup
15
+ end
16
+
17
+ def self.register(name, klass, options = {})
18
+ options.merge!(:name => name.to_s)
19
+ @engines[name.to_s] = klass.new(options)
20
+ end
21
+
22
+ def collect_text(body)
23
+ body.inject('') do |text, exp|
24
+ if exp[0] == :slim && exp[1] == :text
25
+ text << exp[2]
26
+ elsif exp[0] == :newline
27
+ text << "\n"
28
+ end
29
+ text
30
+ end
31
+ end
32
+
33
+ class TiltEngine < EmbeddedEngine
34
+ # Code to collect local variables
35
+ COLLECT_LOCALS = %q{eval('{' + local_variables.select {|v| v[0] != ?_ }.map {|v| ":#{v}=>#{v}" }.join(',') + '}')}
36
+
37
+ def compile(body)
38
+ text = collect_text(body)
39
+ engine = Tilt[options[:name]]
40
+ if options[:precompiled]
41
+ # Wrap precompiled code in proc, local variables from out the proc are accessible
42
+ # WARNING: This is a bit of a hack. Tilt::Engine#precompiled is protected
43
+ precompiled = engine.new { text }.send(:precompiled, {}).first
44
+ [:dynamic, "proc { #{precompiled} }.call"]
45
+ elsif options[:dynamic]
46
+ # Fully dynamic evaluation of the template during runtime (Slow and uncached)
47
+ [:dynamic, "#{engine.name}.new { #{text.inspect} }.render(self, #{COLLECT_LOCALS})"]
48
+ elsif options[:interpolate]
49
+ # Static template with interpolated ruby code
50
+ [:dynamic, '"%s"' % engine.new { text }.render]
51
+ else
52
+ # Static template
53
+ [:static, engine.new { text }.render]
54
+ end
55
+ end
56
+ end
57
+
58
+ class TagEngine < EmbeddedEngine
59
+ def compile_text(body)
60
+ body.inject([:multi]) do |block, exp|
61
+ block << (exp[0] == :slim && exp[1] == :text ? [:static, exp[2]] : exp)
62
+ end
63
+ end
64
+
65
+ def compile(body)
66
+ attrs = [:html, :attrs]
67
+ options[:attributes].each do |key, value|
68
+ attrs << [:html, :basicattr, [:static, key.to_s], [:static, value.to_s]]
69
+ end
70
+ [:html, :tag, options[:tag], attrs, compile_text(body)]
71
+ end
72
+ end
73
+
74
+ class RubyEngine < EmbeddedEngine
75
+ def compile(body)
76
+ [:block, collect_text(body)]
77
+ end
78
+ end
79
+
80
+ # These engines are executed at compile time, embedded ruby is interpolated
81
+ register :markdown, TiltEngine, :interpolate => true
82
+ register :textile, TiltEngine, :interpolate => true
83
+ register :rdoc, TiltEngine, :interpolate => true
84
+
85
+ # These engines are executed at compile time
86
+ register :sass, TiltEngine
87
+ register :less, TiltEngine
88
+ register :coffee, TiltEngine
89
+
90
+ # These engines are precompiled, code is embedded
91
+ register :erb, TiltEngine, :precompiled => true
92
+ register :haml, TiltEngine, :precompiled => true
93
+ register :nokogiri, TiltEngine, :precompiled => true
94
+ register :builder, TiltEngine, :precompiled => true
95
+
96
+ # These engines are completely executed at runtime (Usage not recommended, no caching!)
97
+ register :liquid, TiltEngine, :dynamic => true
98
+ register :radius, TiltEngine, :dynamic => true
99
+ register :markaby, TiltEngine, :dynamic => true
100
+
101
+ # Embedded javascript/css
102
+ register :javascript, TagEngine, :tag => 'script', :attributes => { :type => 'text/javascript' }
103
+ register :css, TagEngine, :tag => 'style', :attributes => { :type => 'text/css' }
104
+
105
+ # Embedded ruby code
106
+ register :ruby, RubyEngine
107
+ end
108
+ end