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
@@ -24,7 +24,7 @@ module Slim
24
24
 
25
25
  exps.each do |exp|
26
26
  if control?(exp)
27
- raise 'Explicit end statements are forbidden' if exp[2] =~ END_REGEX
27
+ raise(Temple::FilterError, 'Explicit end statements are forbidden') if exp[2] =~ END_REGEX
28
28
 
29
29
  # Two control code in a row. If this one is *not*
30
30
  # an else block, we should close the previous one.
@@ -4,29 +4,35 @@ module Slim
4
4
  class Engine < Temple::Engine
5
5
  # This overwrites some Temple default options or sets default options for Slim specific filters.
6
6
  # It is recommended to set the default settings only once in the code and avoid duplication. Only use
7
- # `set_default_options` when you have to override some default settings.
8
- set_default_options :pretty => false,
9
- :sort_attrs => true,
10
- :attr_wrapper => '"',
11
- :attr_delimiter => {'class' => ' '},
12
- :remove_empty_attrs => true,
13
- :generator => Temple::Generators::ArrayBuffer,
14
- :default_tag => 'div'
7
+ # `define_options` when you have to override some default settings.
8
+ define_options :pretty => false,
9
+ :sort_attrs => true,
10
+ :attr_wrapper => '"',
11
+ :attr_delimiter => {'class' => ' '},
12
+ :generator => Temple::Generators::ArrayBuffer,
13
+ :default_tag => 'div'
15
14
 
16
- use Slim::Parser, :file, :tabsize, :encoding, :shortcut, :default_tag
15
+ # TODO: Remove these options in 1.4.0
16
+ define_deprecated_options :remove_empty_attrs, :chain
17
+
18
+ use Slim::Parser, :file, :tabsize, :encoding, :shortcut, :default_tag, :escape_quoted_attrs
17
19
  use Slim::EmbeddedEngine, :enable_engines, :disable_engines, :pretty
18
20
  use Slim::Interpolation
19
21
  use Slim::EndInserter
20
- use Slim::Compiler, :disable_capture, :attr_delimiter, :attr_wrapper, :sort_attrs, :remove_empty_attrs, :default_tag
21
- html :AttributeMerger, :attr_delimiter
22
+ use Slim::ControlStructures, :disable_capture
23
+ use Slim::SplatAttributes, :attr_delimiter, :attr_wrapper, :sort_attrs, :default_tag
22
24
  html :AttributeSorter, :sort_attrs
23
- html :AttributeRemover, :remove_empty_attrs
25
+ html :AttributeMerger, :attr_delimiter
26
+ use Slim::BooleanAttributes, :attr_delimiter
24
27
  html :Pretty, :format, :attr_wrapper, :pretty, :indent
25
28
  filter :Escapable, :use_html_safe, :disable_escape
26
29
  filter :ControlFlow
27
30
  filter :MultiFlattener
28
- use(:Optimizer) { (options[:streaming] ? Temple::Filters::StaticMerger :
29
- Temple::Filters::DynamicInliner).new }
30
- use(:Generator) { options[:generator].new(options) }
31
+ use :Optimizer do
32
+ (options[:streaming] ? Temple::Filters::StaticMerger : Temple::Filters::DynamicInliner).new
33
+ end
34
+ use :Generator do
35
+ options[:generator].new(options.to_hash.reject {|k,v| !options[:generator].default_options.valid_keys.include?(k) })
36
+ end
31
37
  end
32
38
  end
@@ -31,11 +31,5 @@ module Slim
31
31
  def on_slim_attrs(*attrs)
32
32
  [:slim, :attrs, *attrs.map {|a| compile(a) }]
33
33
  end
34
-
35
- # Pass-through handler
36
- def on_slim_tag(name, attrs, content = nil)
37
- tag = [:slim, :tag, name, compile(attrs)]
38
- content ? (tag << compile(content)) : tag
39
- end
40
34
  end
41
35
  end
@@ -10,14 +10,9 @@ module Slim
10
10
  [:slim, :interpolate, String] |
11
11
  [:slim, :embedded, String, Expression] |
12
12
  [:slim, :text, Expression] |
13
- [:slim, :tag, String, SlimAttrs, 'Expression?']
13
+ [:slim, :attrvalue, Bool, String]
14
14
 
15
- SlimAttrs <<
16
- [:slim, :attrs, 'SlimAttr*']
17
-
18
- SlimAttr <<
19
- HTMLAttr |
20
- [:slim, :attr, String, Bool, String] |
15
+ HTMLAttr <<
21
16
  [:slim, :splat, String]
22
17
  end
23
18
  end
@@ -45,7 +45,7 @@ module Slim
45
45
  i += 1
46
46
  end
47
47
 
48
- raise "Text interpolation: Expected closing }" if count != 0
48
+ raise(Temple::FilterError, "Text interpolation: Expected closing }") if count != 0
49
49
 
50
50
  return string[i..-1], string[0, i-1]
51
51
  end
@@ -1,16 +1,16 @@
1
1
  module Slim
2
- # Handle logic-less mode
2
+ # Handle logic less mode
3
3
  # This filter can be activated with the option "logic_less"
4
4
  # @api private
5
5
  class LogicLess < Filter
6
- set_default_options :logic_less => true,
7
- :dictionary => 'self',
8
- :dictionary_access => :wrapped # :symbol, :string, :wrapped
6
+ define_options :logic_less => true,
7
+ :dictionary => 'self',
8
+ :dictionary_access => :wrapped # :symbol, :string, :wrapped
9
9
 
10
10
  def initialize(opts = {})
11
11
  super
12
12
  unless [:string, :symbol, :wrapped].include?(options[:dictionary_access])
13
- raise "Invalid dictionary access #{options[:dictionary_access].inspect}"
13
+ raise ArgumentError, "Invalid dictionary access #{options[:dictionary_access].inspect}"
14
14
  end
15
15
  end
16
16
 
@@ -37,7 +37,7 @@ module Slim
37
37
  end
38
38
 
39
39
  def on_slim_output(escape, name, content)
40
- raise 'Output statements with content are forbidden in logic less mode' if !empty_exp?(content)
40
+ raise(Temple::FilterError, 'Output statements with content are forbidden in logic less mode') if !empty_exp?(content)
41
41
  [:slim, :output, escape, access(name), content]
42
42
  end
43
43
 
@@ -50,11 +50,11 @@ module Slim
50
50
  end
51
51
 
52
52
  def on_dynamic(code)
53
- raise 'Embedded code is forbidden in logic less mode'
53
+ raise Temple::FilterError, 'Embedded code is forbidden in logic less mode'
54
54
  end
55
55
 
56
56
  def on_code(code)
57
- raise 'Embedded code is forbidden in logic less mode'
57
+ raise Temple::FilterError, 'Embedded code is forbidden in logic less mode'
58
58
  end
59
59
 
60
60
  protected
@@ -1,6 +1,6 @@
1
1
  module Slim
2
2
  class LogicLess
3
- # For logic-less mode, objects can be encased in the Wrapper class.
3
+ # For logic less mode, objects can be encased in the Wrapper class.
4
4
  # @api private
5
5
  class Wrapper
6
6
  attr_reader :value, :parent
@@ -1,15 +1,16 @@
1
1
  module Slim
2
2
  # Parses Slim code and transforms it to a Temple expression
3
3
  # @api private
4
- class Parser
5
- include Temple::Mixins::Options
6
-
7
- set_default_options :tabsize => 4,
8
- :encoding => 'utf-8',
9
- :shortcut => {
10
- '#' => 'id',
11
- '.' => 'class'
12
- }
4
+ class Parser < Temple::Parser
5
+ define_options :file,
6
+ :default_tag,
7
+ :escape_quoted_attrs => false,
8
+ :tabsize => 4,
9
+ :encoding => 'utf-8',
10
+ :shortcut => {
11
+ '#' => 'id',
12
+ '.' => 'class'
13
+ }
13
14
 
14
15
  class SyntaxError < StandardError
15
16
  attr_reader :error, :file, :line, :lineno, :column
@@ -26,22 +27,22 @@ module Slim
26
27
  line = @line.strip
27
28
  column = @column + line.size - @line.size
28
29
  %{#{error}
29
- #{file}, Line #{lineno}
30
+ #{file}, Line #{lineno}, Column #{@column}
30
31
  #{line}
31
32
  #{' ' * column}^
32
33
  }
33
34
  end
34
35
  end
35
36
 
36
- def initialize(options = {})
37
+ def initialize(opts = {})
37
38
  super
38
- @tab = ' ' * @options[:tabsize]
39
+ @tab = ' ' * options[:tabsize]
39
40
  @shortcut = {}
40
- @options[:shortcut].each do |k,v|
41
+ options[:shortcut].each do |k,v|
41
42
  @shortcut[k] = if v =~ /\A([^\s]+)\s+([^\s]+)\Z/
42
43
  [$1, $2]
43
44
  else
44
- [@options[:default_tag], v]
45
+ [options[:default_tag], v]
45
46
  end
46
47
  end
47
48
  shortcut = "[#{Regexp.escape @shortcut.keys.join}]"
@@ -72,7 +73,7 @@ module Slim
72
73
  result
73
74
  end
74
75
 
75
- private
76
+ protected
76
77
 
77
78
  DELIMITERS = {
78
79
  '(' => ')',
@@ -82,8 +83,8 @@ module Slim
82
83
 
83
84
  DELIMITER_REGEX = /\A[#{Regexp.escape DELIMITERS.keys.join}]/
84
85
  ATTR_NAME = '\A\s*(\w[:\w-]*)'
85
- QUOTED_ATTR_REGEX = /#{ATTR_NAME}=("|')/
86
- CODE_ATTR_REGEX = /#{ATTR_NAME}=/
86
+ QUOTED_ATTR_REGEX = /#{ATTR_NAME}=(=?)("|')/
87
+ CODE_ATTR_REGEX = /#{ATTR_NAME}=(=?)/
87
88
 
88
89
  def reset(lines = nil, stacks = nil)
89
90
  # Since you can indent however you like in Slim, we need to keep a list
@@ -176,25 +177,28 @@ module Slim
176
177
 
177
178
  def parse_line_indicators
178
179
  case @line
180
+ when /\A\/!( ?)/
181
+ # HTML comment
182
+ @stacks.last << [:html, :comment, [:slim, :text, parse_text_block($', @indents.last + $1.size + 2)]]
183
+ when /\A\/\[\s*(.*?)\s*\]\s*\Z/
184
+ # HTML conditional comment
185
+ block = [:multi]
186
+ @stacks.last << [:html, :condcomment, $1, block]
187
+ @stacks << block
179
188
  when /\A\//
180
- # Found a comment block.
181
- if @line =~ %r{\A/!( ?)(.*)\Z}
182
- # HTML comment
183
- @stacks.last << [:html, :comment, [:slim, :text, parse_text_block($2, @indents.last + $1.size + 2)]]
184
- elsif @line =~ %r{\A/\[\s*(.*?)\s*\]\s*\Z}
185
- # HTML conditional comment
186
- block = [:multi]
187
- @stacks.last << [:html, :condcomment, $1, block]
188
- @stacks << block
189
- else
190
- # Slim comment
191
- parse_comment_block
192
- end
193
- when /\A([\|'])( ?)(.*)\Z/
189
+ # Slim comment
190
+ parse_comment_block
191
+ when /\A([\|'])( ?)/
194
192
  # Found a text block.
195
193
  trailing_ws = $1 == "'"
196
- @stacks.last << [:slim, :text, parse_text_block($3, @indents.last + $2.size + 1)]
194
+ @stacks.last << [:slim, :text, parse_text_block($', @indents.last + $2.size + 1)]
197
195
  @stacks.last << [:static, ' '] if trailing_ws
196
+ when /\A</
197
+ # Inline html
198
+ # @stacks.last << parse_text_block(@line, @indents.last + 1)
199
+ block = [:multi]
200
+ @stacks.last << [:multi, [:slim, :interpolate, @line], block]
201
+ @stacks << block
198
202
  when /\A-/
199
203
  # Found a code block.
200
204
  # We expect the line to be broken or the next line to be indented.
@@ -289,7 +293,7 @@ module Slim
289
293
  end
290
294
 
291
295
  def parse_tag(tag)
292
- tag = [:slim, :tag, @shortcut[tag] ? @shortcut[tag][0] : tag, parse_attributes]
296
+ tag = [:html, :tag, @shortcut[tag] ? @shortcut[tag][0] : tag, parse_attributes]
293
297
  @stacks.last << tag
294
298
 
295
299
  case @line
@@ -325,7 +329,7 @@ module Slim
325
329
  end
326
330
 
327
331
  def parse_attributes
328
- attributes = [:slim, :attrs]
332
+ attributes = [:html, :attrs]
329
333
 
330
334
  # Find any shortcut attributes
331
335
  while @line =~ @shortcut_regex
@@ -356,20 +360,21 @@ module Slim
356
360
  when QUOTED_ATTR_REGEX
357
361
  # Value is quoted (static)
358
362
  @line = $'
359
- attributes << [:html, :attr, $1, [:slim, :interpolate, parse_quoted_attribute($2)]]
363
+ attributes << [:html, :attr, $1,
364
+ [:escape, options[:escape_quoted_attrs] && $2.empty?,
365
+ [:slim, :interpolate, parse_quoted_attribute($3)]]]
360
366
  when CODE_ATTR_REGEX
361
367
  # Value is ruby code
362
368
  @line = $'
363
- escape = @line[0] != ?=
364
- @line.slice!(0) unless escape
365
369
  name = $1
370
+ escape = $2.empty?
366
371
  value = parse_ruby_code(delimiter)
367
372
  # Remove attribute wrapper which doesn't belong to the ruby code
368
373
  # e.g id=[hash[:a] + hash[:b]]
369
374
  value = value[1..-2] if value =~ DELIMITER_REGEX &&
370
375
  DELIMITERS[$&] == value[-1, 1]
371
376
  syntax_error!('Invalid empty attribute') if value.empty?
372
- attributes << [:slim, :attr, name, escape, value]
377
+ attributes << [:html, :attr, name, [:slim, :attrvalue, escape, value]]
373
378
  else
374
379
  break unless delimiter
375
380
 
@@ -377,7 +382,7 @@ module Slim
377
382
  when boolean_attr_regex
378
383
  # Boolean attribute
379
384
  @line = $'
380
- attributes << [:slim, :attr, $1, false, 'true']
385
+ attributes << [:html, :attr, $1, [:slim, :attrvalue, false, 'true']]
381
386
  when end_regex
382
387
  # Find ending delimiter
383
388
  @line = $'
@@ -389,11 +394,8 @@ module Slim
389
394
 
390
395
  # Attributes span multiple lines
391
396
  @stacks.last << [:newline]
392
- orig_line, lineno = @orig_line, @lineno
393
- next_line || syntax_error!("Expected closing delimiter #{delimiter}",
394
- :orig_line => orig_line,
395
- :lineno => lineno,
396
- :column => orig_line.size)
397
+ syntax_error!("Expected closing delimiter #{delimiter}") if @lines.empty?
398
+ next_line
397
399
  end
398
400
  end
399
401
  end
@@ -442,19 +444,16 @@ module Slim
442
444
  end
443
445
 
444
446
  syntax_error!("Expected closing brace }") if count != 0
447
+ syntax_error!("Expected closing quote #{quote}") if @line[0] != quote[0]
445
448
  @line.slice!(0)
449
+
446
450
  value
447
451
  end
448
452
 
449
453
  # Helper for raising exceptions
450
- def syntax_error!(message, args = {})
451
- args[:orig_line] ||= @orig_line
452
- args[:line] ||= @line
453
- args[:lineno] ||= @lineno
454
- args[:column] ||= args[:orig_line] && args[:line] ?
455
- args[:orig_line].size - args[:line].size : 0
456
- raise SyntaxError.new(message, options[:file],
457
- args[:orig_line], args[:lineno], args[:column])
454
+ def syntax_error!(message)
455
+ raise SyntaxError.new(message, options[:file], @orig_line, @lineno,
456
+ @orig_line && @line ? @orig_line.size - @line.size : 0)
458
457
  end
459
458
  end
460
459
  end
@@ -0,0 +1,112 @@
1
+ module Slim
2
+ # @api private
3
+ class SplatAttributes < Filter
4
+ define_options :attr_delimiter, :attr_wrapper, :sort_attrs, :default_tag
5
+
6
+ def call(exp)
7
+ @attr_delimiter, @splat_used = unique_name, false
8
+ exp = compile(exp)
9
+ if @splat_used
10
+ [:multi, [:code, "#{@attr_delimiter} = #{@options[:attr_delimiter].inspect}"], exp]
11
+ else
12
+ exp
13
+ end
14
+ end
15
+
16
+ # Handle tag expression `[:html, :tag, name, attrs, content]`
17
+ #
18
+ # @param [String] name Tag name
19
+ # @param [Array] attrs Temple expression
20
+ # @param [Array] content Temple expression
21
+ # @return [Array] Compiled temple expression
22
+ def on_html_tag(name, attrs, content = nil)
23
+ return super if name != '*'
24
+ hash, merger, formatter = splat_attributes(attrs[2..-1])
25
+ tmp = unique_name
26
+ tag = [:multi,
27
+ merger,
28
+ [:code, "#{tmp} = #{hash}.delete('tag').to_s"],
29
+ [:if, "#{tmp}.empty?",
30
+ [:code, "#{tmp} = #{@options[:default_tag].inspect}"]],
31
+ [:static, '<'],
32
+ [:dynamic, "#{tmp}"],
33
+ formatter]
34
+ tag << if content
35
+ [:multi,
36
+ [:static, '>'],
37
+ compile(content),
38
+ [:static, '</'],
39
+ [:dynamic, "#{tmp}"],
40
+ [:static, '>']]
41
+ else
42
+ [:static, '/>']
43
+ end
44
+ end
45
+
46
+ # Handle attributes expression `[:html, :attrs, *attrs]`
47
+ #
48
+ # @param [Array] attrs Array of temple expressions
49
+ # @return [Array] Compiled temple expression
50
+ def on_html_attrs(*attrs)
51
+ return super if attrs.all? {|attr| attr[1] != :splat}
52
+ hash, merger, formatter = splat_attributes(attrs)
53
+ [:multi, merger, formatter]
54
+ end
55
+
56
+ protected
57
+
58
+ def splat_attributes(attrs)
59
+ @splat_used = true
60
+
61
+ hash, name, value, tmp = unique_name, unique_name, unique_name, unique_name
62
+
63
+ merger = [:multi, [:code, "#{hash} = {}"]]
64
+ attrs.each do |attr|
65
+ merger << if attr[0] == :html && attr[1] == :attr
66
+ [:multi,
67
+ [:capture, tmp, compile(attr[3])],
68
+ [:code, "(#{hash}[#{attr[2].inspect}] ||= []) << #{tmp}"]]
69
+ elsif attr[0] == :slim
70
+ if attr[1] == :attr
71
+ [:code, "(#{hash}[#{attr[2].inspect}] ||= []) << (#{attr[4]})"]
72
+ elsif attr[1] == :splat
73
+ [:code, "(#{attr[2]}).each {|#{name},#{value}| (#{hash}[#{name}.to_s] ||= []) << (#{value}) }"]
74
+ else
75
+ attr
76
+ end
77
+ else
78
+ attr
79
+ end
80
+ end
81
+
82
+ merger << [:block, "#{hash}.each do |#{name},#{value}|",
83
+ [:multi,
84
+ [:code, "#{value}.flatten!"],
85
+ [:if, "#{value}.size > 1 && !#{@attr_delimiter}[#{name}]",
86
+ [:code, %{raise("Multiple #\{#{name}\} attributes specified")}]],
87
+ [:code, "#{value}.compact!"],
88
+ [:case, "#{value}.size",
89
+ ['0',
90
+ [:code, "#{hash}[#{name}] = nil"]],
91
+ ['1',
92
+ [:case, "#{value}.first",
93
+ ['true', [:code, "#{hash}[#{name}] = #{name}"]],
94
+ ['false, nil', [:code, "#{hash}[#{name}] = nil"]],
95
+ [:else, [:code, "#{hash}[#{name}] = #{value}.first"]]]],
96
+ [:else,
97
+ [:code, "#{hash}[#{name}] = #{value}.join(#{@attr_delimiter}[#{name}].to_s)"]]]]]
98
+
99
+ attr = [:if, value,
100
+ [:multi,
101
+ [:static, ' '],
102
+ [:dynamic, name],
103
+ [:static, "=#{options[:attr_wrapper]}"],
104
+ [:escape, true, [:dynamic, value]],
105
+ [:static, options[:attr_wrapper]]]]
106
+ enumerator = options[:sort_attrs] ? "#{hash}.sort_by {|#{name},#{value}| #{name} }" : hash
107
+ formatter = [:block, "#{enumerator}.each do |#{name},#{value}|", attr]
108
+
109
+ return hash, merger, formatter
110
+ end
111
+ end
112
+ end