slim 1.3.0 → 1.3.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.
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