slim 1.3.0 → 1.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/.travis.yml +9 -5
  2. data/CHANGES +23 -0
  3. data/Gemfile +13 -2
  4. data/README.md +346 -105
  5. data/Rakefile +21 -9
  6. data/lib/slim.rb +3 -3
  7. data/lib/slim/boolean_attributes.rb +67 -0
  8. data/lib/slim/command.rb +23 -23
  9. data/lib/slim/control_structures.rb +57 -0
  10. data/lib/slim/embedded_engine.rb +80 -43
  11. data/lib/slim/end_inserter.rb +1 -1
  12. data/lib/slim/engine.rb +21 -15
  13. data/lib/slim/filter.rb +0 -6
  14. data/lib/slim/grammar.rb +2 -7
  15. data/lib/slim/interpolation.rb +1 -1
  16. data/lib/slim/logic_less/filter.rb +8 -8
  17. data/lib/slim/logic_less/wrapper.rb +1 -1
  18. data/lib/slim/parser.rb +51 -52
  19. data/lib/slim/splat_attributes.rb +112 -0
  20. data/lib/slim/translator.rb +13 -12
  21. data/lib/slim/version.rb +1 -1
  22. data/slim.gemspec +1 -1
  23. data/test/{slim → core}/helper.rb +3 -7
  24. data/test/{slim → core}/test_code_blocks.rb +0 -0
  25. data/test/{slim → core}/test_code_escaping.rb +4 -4
  26. data/test/{slim → core}/test_code_evaluation.rb +3 -112
  27. data/test/{slim → core}/test_code_output.rb +0 -0
  28. data/test/{slim → core}/test_code_structure.rb +0 -0
  29. data/test/{slim → core}/test_embedded_engines.rb +8 -3
  30. data/test/{slim → core}/test_encoding.rb +0 -0
  31. data/test/core/test_html_attributes.rb +218 -0
  32. data/test/{slim → core}/test_html_escaping.rb +17 -0
  33. data/test/{slim → core}/test_html_structure.rb +13 -98
  34. data/test/{slim → core}/test_parser_errors.rb +24 -15
  35. data/test/{slim → core}/test_pretty.rb +0 -0
  36. data/test/{slim → core}/test_ruby_errors.rb +7 -0
  37. data/test/{slim → core}/test_slim_template.rb +0 -0
  38. data/test/{slim → core}/test_text_interpolation.rb +2 -2
  39. data/test/core/test_thread_options.rb +18 -0
  40. data/test/{slim/logic_less → logic_less}/test_logic_less.rb +21 -0
  41. data/test/{slim/logic_less → logic_less}/test_wrapper.rb +3 -3
  42. data/test/rails/app/controllers/slim_controller.rb +4 -2
  43. data/test/rails/app/views/parents/_form.html.slim +1 -0
  44. data/test/rails/app/views/parents/edit.html.slim +2 -1
  45. data/test/rails/app/views/parents/new.html.slim +2 -1
  46. data/test/rails/app/views/slim/thread_options.html.slim +1 -0
  47. data/test/rails/test/test_slim.rb +6 -4
  48. data/test/{slim/translator → translator}/test_translator.rb +0 -0
  49. metadata +44 -82
  50. data/lib/slim/compiler.rb +0 -194
  51. data/test/rails/app/views/slim/nil.html.slim +0 -1
  52. data/test/slim/test_chain_manipulation.rb +0 -42
data/Rakefile CHANGED
@@ -11,24 +11,26 @@ task :bench, :iterations, :slow do
11
11
  ruby('benchmarks/run-benchmarks.rb')
12
12
  end
13
13
 
14
- task 'test' => %w(test:core test:logic_less test:translator)
14
+ task 'test' => %w(test:core_and_plugins)
15
15
 
16
16
  namespace 'test' do
17
+ task 'core_and_plugins' => %w(core logic_less translator)
18
+
17
19
  Rake::TestTask.new('core') do |t|
18
- t.libs << 'lib' << 'test/slim'
19
- t.test_files = FileList['test/slim/test_*.rb']
20
+ t.libs << 'lib' << 'test/core'
21
+ t.test_files = FileList['test/core/test_*.rb']
20
22
  t.verbose = true
21
23
  end
22
24
 
23
25
  Rake::TestTask.new('logic_less') do |t|
24
- t.libs << 'lib' << 'test/slim'
25
- t.test_files = FileList['test/slim/logic_less/test_*.rb']
26
+ t.libs << 'lib' << 'test/core'
27
+ t.test_files = FileList['test/logic_less/test_*.rb']
26
28
  t.verbose = true
27
29
  end
28
30
 
29
31
  Rake::TestTask.new('translator') do |t|
30
- t.libs << 'lib' << 'test/slim'
31
- t.test_files = FileList['test/slim/translator/test_*.rb']
32
+ t.libs << 'lib' << 'test/core'
33
+ t.test_files = FileList['test/translator/test_*.rb']
32
34
  t.verbose = true
33
35
  end
34
36
 
@@ -38,8 +40,18 @@ namespace 'test' do
38
40
  t.verbose = true
39
41
  end
40
42
 
41
- task 'ci' do |t|
42
- Rake::Task[ENV['TASK']].execute
43
+ begin
44
+ require 'sinatra'
45
+ spec = Gem::Specification.find_by_name('sinatra')
46
+ Rake::TestTask.new('sinatra') do |t|
47
+ # Run Slim integration test in Sinatra
48
+ t.test_files = FileList["#{spec.gem_dir}/test/slim_test.rb"]
49
+ t.verbose = true
50
+ end
51
+ rescue LoadError
52
+ task :sinatra do
53
+ abort 'Sinatra is not available'
54
+ end
43
55
  end
44
56
  end
45
57
 
@@ -1,12 +1,12 @@
1
- # encoding: utf-8
2
-
3
1
  require 'temple'
4
2
  require 'slim/parser'
5
3
  require 'slim/filter'
6
4
  require 'slim/end_inserter'
7
5
  require 'slim/embedded_engine'
8
6
  require 'slim/interpolation'
9
- require 'slim/compiler'
7
+ require 'slim/control_structures'
8
+ require 'slim/splat_attributes'
9
+ require 'slim/boolean_attributes'
10
10
  require 'slim/engine'
11
11
  require 'slim/template'
12
12
  require 'slim/version'
@@ -0,0 +1,67 @@
1
+ module Slim
2
+ # @api private
3
+ class BooleanAttributes < Filter
4
+ define_options :attr_delimiter
5
+
6
+ # Handle attributes expression `[:html, :attrs, *attrs]`
7
+ #
8
+ # @param [Array] attrs Array of temple expressions
9
+ # @return [Array] Compiled temple expression
10
+ def on_html_attrs(*attrs)
11
+ [:multi, *attrs.map {|a| compile(a) }]
12
+ end
13
+
14
+ # Handle attribute expression `[:slim, :attr, escape, code]`
15
+ #
16
+ # @param [Boolean] escape Escape html
17
+ # @param [String] code Ruby code
18
+ # @return [Array] Compiled temple expression
19
+ def on_html_attr(name, value)
20
+ unless value[0] == :slim && value[1] == :attrvalue
21
+ @attr = name
22
+ return super
23
+ end
24
+
25
+ escape, code = value[2], value[3]
26
+ case code
27
+ when 'true'
28
+ [:html, :attr, name, [:static, name]]
29
+ when 'false', 'nil'
30
+ [:multi]
31
+ else
32
+ tmp = unique_name
33
+ conds = [:case, tmp,
34
+ ['true', [:html, :attr, name, [:static, name]]],
35
+ ['false, nil', [:multi]]]
36
+ if delimiter = options[:attr_delimiter][name]
37
+ conds << ['Array',
38
+ [:multi,
39
+ [:code, "#{tmp} = #{tmp}.flatten.compact.join(#{delimiter.inspect})"],
40
+ [:if, "!#{tmp}.empty?",
41
+ [:html, :attr, name, [:escape, escape, [:dynamic, tmp]]]]]]
42
+ end
43
+ conds << [:else, [:html, :attr, name, [:escape, escape, [:dynamic, tmp]]]]
44
+ [:multi, [:code, "#{tmp} = (#{code})"], conds]
45
+ end
46
+ end
47
+
48
+ # Handle attribute expression `[:slim, :attrvalue, escape, code]`
49
+ #
50
+ # @param [Boolean] escape Escape html
51
+ # @param [String] code Ruby code
52
+ # @return [Array] Compiled temple expression
53
+ def on_slim_attrvalue(escape, code)
54
+ tmp = unique_name
55
+ [:multi,
56
+ [:code, "#{tmp} = #{code}"],
57
+ [:escape, escape,
58
+ [:dynamic,
59
+ if delimiter = options[:attr_delimiter][@attr]
60
+ "Array === #{tmp} ? #{tmp}.flatten.compact.join(#{delimiter.inspect}) : #{tmp}"
61
+ else
62
+ tmp
63
+ end
64
+ ]]]
65
+ end
66
+ end
67
+ end
@@ -3,6 +3,10 @@ require 'slim/translator'
3
3
  require 'optparse'
4
4
 
5
5
  module Slim
6
+ Engine.set_default_options :logic_less => false,
7
+ :pretty => false,
8
+ :tr => false
9
+
6
10
  # Slim commandline interface
7
11
  # @api private
8
12
  class Command
@@ -29,32 +33,38 @@ module Slim
29
33
 
30
34
  # Configure OptionParser
31
35
  def set_opts(opts)
32
- opts.on('-s', '--stdin', :NONE, 'Read input from standard input instead of an input file') do
36
+ opts.on('-s', '--stdin', 'Read input from standard input instead of an input file') do
33
37
  @options[:input] = $stdin
34
38
  end
35
39
 
36
- opts.on('--trace', :NONE, 'Show a full traceback on error') do
40
+ opts.on('--trace', 'Show a full traceback on error') do
37
41
  @options[:trace] = true
38
42
  end
39
43
 
40
- opts.on('-c', '--compile', :NONE, 'Compile only but do not run') do
44
+ opts.on('-c', '--compile', 'Compile only but do not run') do
45
+ @options[:compile] = true
46
+ end
47
+
48
+ opts.on('-r', '--rails', 'Generate rails compatible code (Implies --compile)') do
49
+ Engine.set_default_options :disable_capture => true, :generator => Temple::Generators::RailsOutputBuffer
41
50
  @options[:compile] = true
42
51
  end
43
52
 
44
- opts.on('-r', '--rails', :NONE, 'Generate rails compatible code (combine with -c)') do
45
- @options[:rails] = true
53
+ opts.on('-t', '--translator', 'Enable translator plugin') do
54
+ Engine.set_default_options :tr => true
46
55
  end
47
56
 
48
- opts.on('-t', '--translator', :NONE, 'Enable translator plugin') do
49
- @options[:translator] = true
57
+ opts.on('-l', '--logic-less', 'Enable logic less plugin') do
58
+ Engine.set_default_options :logic_less => true
50
59
  end
51
60
 
52
- opts.on('-l', '--logic-less', :NONE, 'Enable logic-less plugin') do
53
- @options[:logic_less] = true
61
+ opts.on('-p', '--pretty', 'Produce pretty html') do
62
+ Engine.set_default_options :pretty => true
54
63
  end
55
64
 
56
- opts.on('-p', '--pretty', :NONE, 'Produce pretty html') do
57
- @options[:pretty] = true
65
+ opts.on('-o', '--option [NAME=CODE]', String, 'Set slim option') do |str|
66
+ parts = str.split('=', 2)
67
+ Engine.default_options[parts.first.to_sym] = eval(parts.last)
58
68
  end
59
69
 
60
70
  opts.on_tail('-h', '--help', 'Show this message') do
@@ -88,19 +98,9 @@ module Slim
88
98
  end
89
99
 
90
100
  if @options[:compile]
91
- @options[:output].puts(Slim::Engine.new(:file => @options[:file],
92
- :pretty => @options[:pretty],
93
- :logic_less => @options[:logic_less],
94
- :disable_capture => @options[:rails],
95
- :tr => @options[:translator],
96
- :generator => @options[:rails] ?
97
- Temple::Generators::RailsOutputBuffer :
98
- Temple::Generators::ArrayBuffer).call(@options[:input].read))
101
+ @options[:output].puts(Slim::Engine.new(:file => @options[:file]).call(@options[:input].read))
99
102
  else
100
- @options[:output].puts(Slim::Template.new(@options[:file],
101
- :pretty => @options[:pretty],
102
- :tr => @options[:translator],
103
- :logic_less => @options[:logic_less]) { @options[:input].read }.render)
103
+ @options[:output].puts(Slim::Template.new(@options[:file]) { @options[:input].read }.render)
104
104
  end
105
105
  end
106
106
  end
@@ -0,0 +1,57 @@
1
+ module Slim
2
+ # @api private
3
+ class ControlStructures < Filter
4
+ define_options :disable_capture
5
+
6
+ # Handle control expression `[:slim, :control, code, content]`
7
+ #
8
+ # @param [String] code Ruby code
9
+ # @param [Array] content Temple expression
10
+ # @return [Array] Compiled temple expression
11
+ def on_slim_control(code, content)
12
+ [:multi,
13
+ [:code, code],
14
+ compile(content)]
15
+ end
16
+
17
+ # Handle output expression `[:slim, :output, escape, code, content]`
18
+ #
19
+ # @param [Boolean] escape Escape html
20
+ # @param [String] code Ruby code
21
+ # @param [Array] content Temple expression
22
+ # @return [Array] Compiled temple expression
23
+ def on_slim_output(escape, code, content)
24
+ if empty_exp?(content)
25
+ [:multi, [:escape, escape, [:dynamic, code]], content]
26
+ else
27
+ tmp = unique_name
28
+
29
+ [:multi,
30
+ # Capture the result of the code in a variable. We can't do
31
+ # `[:dynamic, code]` because it's probably not a complete
32
+ # expression (which is a requirement for Temple).
33
+ [:block, "#{tmp} = #{code}",
34
+
35
+ # Capture the content of a block in a separate buffer. This means
36
+ # that `yield` will not output the content to the current buffer,
37
+ # but rather return the output.
38
+ #
39
+ # The capturing can be disabled with the option :disable_capture.
40
+ # Output code in the block writes directly to the output buffer then.
41
+ # Rails handles this by replacing the output buffer for helpers.
42
+ options[:disable_capture] ? compile(content) : [:capture, unique_name, compile(content)]],
43
+
44
+ # Output the content.
45
+ [:escape, escape, [:dynamic, tmp]]]
46
+ end
47
+ end
48
+
49
+ # Handle text expression `[:slim, :text, content]`
50
+ #
51
+ # @param [Array] content Temple expression
52
+ # @return [Array] Compiled temple expression
53
+ def on_slim_text(content)
54
+ compile(content)
55
+ end
56
+ end
57
+ end
@@ -63,53 +63,78 @@ module Slim
63
63
  class EmbeddedEngine < Filter
64
64
  @engines = {}
65
65
 
66
- class << self
67
- attr_reader :engines
68
-
69
- # Register embedded engine
70
- #
71
- # @param [String] name Name of the engine
72
- # @param [Class] klass Engine class
73
- # @param option_filter List of options to pass to engine.
74
- # Last argument can be default option hash.
75
- def register(name, klass, *option_filter)
76
- local_options = Hash === option_filter.last ? option_filter.pop : nil
77
- @engines[name.to_s] = [klass, option_filter, local_options]
66
+ # Register embedded engine
67
+ #
68
+ # @param [String] name Name of the engine
69
+ # @param [Class] klass Engine class
70
+ # @param option_filter List of options to pass to engine.
71
+ # Last argument can be default option hash.
72
+ def self.register(name, klass, *option_filter)
73
+ name = name.to_sym
74
+ local_options = option_filter.last.respond_to?(:to_hash) ? option_filter.pop.to_hash : {}
75
+ define_options(name, *option_filter)
76
+ klass.define_options(name)
77
+ @engines[name.to_sym] = proc do |options|
78
+ klass.new({}.update(options).delete_if {|k,v| !option_filter.include?(k) && k != name }.update(local_options))
78
79
  end
79
80
  end
80
81
 
82
+ def self.create(name, options)
83
+ constructor = @engines[name] || raise(Temple::FilterError, "Embedded engine #{name} not found")
84
+ constructor.call(options)
85
+ end
86
+
87
+ define_options :enable_engines, :disable_engines
88
+
89
+ def initialize(opts = {})
90
+ super
91
+ @engines = {}
92
+ @enabled = normalize_engine_list(options[:enable_engines])
93
+ @disabled = normalize_engine_list(options[:disable_engines])
94
+ end
95
+
81
96
  def on_slim_embedded(name, body)
82
- name = name.to_s
83
- raise "Embedded engine #{name} is disabled" if (options[:enable_engines] && !options[:enable_engines].include?(name)) ||
84
- (options[:disable_engines] && options[:disable_engines].include?(name))
85
- engine, option_filter, local_options = self.class.engines[name] || raise("Embedded engine #{name} not found")
86
- filtered_options = Hash[*option_filter.select {|k| options.include?(k) }.map {|k| [k, options[k]] }.flatten]
87
- engine.new(Temple::ImmutableHash.new(local_options, filtered_options)).on_slim_embedded(name, body)
97
+ name = name.to_sym
98
+ raise(Temple::FilterError, "Embedded engine #{name} is disabled") unless enabled?(name)
99
+ @engines[name] ||= self.class.create(name, options)
100
+ @engines[name].on_slim_embedded(name, body)
101
+ end
102
+
103
+ def enabled?(name)
104
+ (!@enabled || @enabled.include?(name)) &&
105
+ (!@disabled || !@disabled.include?(name))
88
106
  end
89
107
 
90
108
  protected
91
109
 
92
- def collect_text(body)
93
- @text_collector ||= TextCollector.new
94
- @text_collector.call(body)
110
+ def normalize_engine_list(list)
111
+ raise(ArgumentError, "Option :enable_engines/:disable_engines must be String or Symbol list") unless !list || Array === list
112
+ list ? list.map {|s| s.to_sym } : list
95
113
  end
96
114
 
97
- # Basic tilt engine
98
- class TiltEngine < EmbeddedEngine
99
- def on_slim_embedded(engine, body)
100
- tilt_engine = Tilt[engine] || raise("Tilt engine #{engine} is not available.")
101
- tilt_options = options[engine.to_sym] || {}
102
- [:multi, tilt_render(tilt_engine, tilt_options, collect_text(body)), collect_newlines(body)]
103
- end
104
-
115
+ class Engine < Filter
105
116
  protected
106
117
 
118
+ def collect_text(body)
119
+ @text_collector ||= TextCollector.new
120
+ @text_collector.call(body)
121
+ end
122
+
107
123
  def collect_newlines(body)
108
124
  @newline_collector ||= NewlineCollector.new
109
125
  @newline_collector.call(body)
110
126
  end
111
127
  end
112
128
 
129
+ # Basic tilt engine
130
+ class TiltEngine < Engine
131
+ def on_slim_embedded(engine, body)
132
+ tilt_engine = Tilt[engine] || raise(Temple::FilterError, "Tilt engine #{engine} is not available.")
133
+ tilt_options = options[engine.to_sym] || {}
134
+ [:multi, tilt_render(tilt_engine, tilt_options, collect_text(body)), collect_newlines(body)]
135
+ end
136
+ end
137
+
113
138
  # Tilt-based static template (evaluated at compile-time)
114
139
  class StaticTiltEngine < TiltEngine
115
140
  protected
@@ -121,11 +146,14 @@ module Slim
121
146
 
122
147
  # Sass engine which supports :pretty option
123
148
  class SassEngine < TiltEngine
149
+ define_options :pretty
150
+
124
151
  protected
125
152
 
126
153
  def tilt_render(tilt_engine, tilt_options, text)
127
154
  text = tilt_engine.new(tilt_options.merge(
128
- :style => (options[:pretty] ? :expanded : :compressed), :cache => false)) { text }.render
155
+ :style => options[:pretty] ? :expanded : :compressed,
156
+ :cache => false)) { text }.render
129
157
  text.chomp!
130
158
  [:static, options[:pretty] ? "\n#{text}\n" : text]
131
159
  end
@@ -136,7 +164,7 @@ module Slim
136
164
  protected
137
165
 
138
166
  def tilt_render(tilt_engine, tilt_options, text)
139
- # WARNING: This is a bit of a hack. Tilt::Engine#precompiled is protected
167
+ # HACK: Tilt::Engine#precompiled is protected
140
168
  [:dynamic, tilt_engine.new(tilt_options) { text }.send(:precompiled, {}).first]
141
169
  end
142
170
  end
@@ -163,7 +191,7 @@ module Slim
163
191
  end
164
192
 
165
193
  # ERB engine (uses the Temple ERB implementation)
166
- class ERBEngine < EmbeddedEngine
194
+ class ERBEngine < Engine
167
195
  def on_slim_embedded(engine, body)
168
196
  [:multi, [:newline], erb_parser.call(collect_text(body))]
169
197
  end
@@ -177,15 +205,24 @@ module Slim
177
205
 
178
206
  # Tag wrapper engine
179
207
  # Generates a html tag and wraps another engine (specified via :engine option)
180
- class TagEngine < EmbeddedEngine
208
+ class TagEngine < Engine
209
+ disable_option_validator!
210
+
181
211
  def on_slim_embedded(engine, body)
182
- content = options[:engine] ? options[:engine].new(options).on_slim_embedded(engine, body) : body
183
- [:html, :tag, options[:tag], [:html, :attrs, *options[:attributes].map {|k, v| [:html, :attr, k, [:static, v]] }], content]
212
+ if options[:engine]
213
+ opts = {}.update(options)
214
+ opts.delete(:engine)
215
+ opts.delete(:tag)
216
+ opts.delete(:attributes)
217
+ @engine ||= options[:engine].new(opts)
218
+ body = @engine.on_slim_embedded(engine, body)
219
+ end
220
+ [:html, :tag, options[:tag], [:html, :attrs, *options[:attributes].map {|k, v| [:html, :attr, k, [:static, v]] }], body]
184
221
  end
185
222
  end
186
223
 
187
224
  # Embeds ruby code
188
- class RubyEngine < EmbeddedEngine
225
+ class RubyEngine < Engine
189
226
  def on_slim_embedded(engine, body)
190
227
  [:multi, [:newline], [:code, collect_text(body)]]
191
228
  end
@@ -200,11 +237,11 @@ module Slim
200
237
  register :mediawiki, InterpolateTiltEngine
201
238
 
202
239
  # These engines are executed at compile time
203
- register :coffee, TagEngine, :tag => :script, :attributes => { :type => 'text/javascript' }, :engine => StaticTiltEngine
204
- register :less, TagEngine, :tag => :style, :attributes => { :type => 'text/css' }, :engine => StaticTiltEngine
205
- register :styl, TagEngine, :tag => :style, :attributes => { :type => 'text/css' }, :engine => StaticTiltEngine
206
- register :sass, TagEngine, :pretty, :tag => :style, :attributes => { :type => 'text/css' }, :engine => SassEngine
207
- register :scss, TagEngine, :pretty, :tag => :style, :attributes => { :type => 'text/css' }, :engine => SassEngine
240
+ register :coffee, TagEngine, :tag => :script, :attributes => { :type => 'text/javascript' }, :engine => StaticTiltEngine
241
+ register :less, TagEngine, :tag => :style, :attributes => { :type => 'text/css' }, :engine => StaticTiltEngine
242
+ register :styl, TagEngine, :tag => :style, :attributes => { :type => 'text/css' }, :engine => StaticTiltEngine
243
+ register :sass, TagEngine, :pretty, :tag => :style, :attributes => { :type => 'text/css' }, :engine => SassEngine
244
+ register :scss, TagEngine, :pretty, :tag => :style, :attributes => { :type => 'text/css' }, :engine => SassEngine
208
245
 
209
246
  # These engines are precompiled, code is embedded
210
247
  register :erb, ERBEngine
@@ -212,8 +249,8 @@ module Slim
212
249
  register :builder, PrecompiledTiltEngine
213
250
 
214
251
  # Embedded javascript/css
215
- register :javascript, TagEngine, :tag => :script, :attributes => { :type => 'text/javascript' }
216
- register :css, TagEngine, :tag => :style, :attributes => { :type => 'text/css' }
252
+ register :javascript, TagEngine, :tag => :script, :attributes => { :type => 'text/javascript' }
253
+ register :css, TagEngine, :tag => :style, :attributes => { :type => 'text/css' }
217
254
 
218
255
  # Embedded ruby code
219
256
  register :ruby, RubyEngine