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