curly-templates 2.0.1 → 2.1.0.beta1

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/Gemfile +2 -0
  4. data/README.md +85 -5
  5. data/curly-templates.gemspec +37 -8
  6. data/lib/curly.rb +1 -1
  7. data/lib/curly/{attribute_parser.rb → attribute_scanner.rb} +6 -4
  8. data/lib/curly/compiler.rb +81 -72
  9. data/lib/curly/component_compiler.rb +37 -31
  10. data/lib/curly/component_scanner.rb +19 -0
  11. data/lib/curly/incomplete_block_error.rb +0 -7
  12. data/lib/curly/incorrect_ending_error.rb +0 -21
  13. data/lib/curly/parser.rb +171 -0
  14. data/lib/curly/presenter.rb +1 -1
  15. data/lib/curly/scanner.rb +23 -9
  16. data/spec/attribute_scanner_spec.rb +46 -0
  17. data/spec/collection_blocks_spec.rb +88 -0
  18. data/spec/compiler/context_blocks_spec.rb +42 -0
  19. data/spec/component_compiler_spec.rb +26 -77
  20. data/spec/component_scanner_spec.rb +19 -0
  21. data/spec/{integration/components_spec.rb → components_spec.rb} +0 -0
  22. data/spec/{integration/conditional_blocks_spec.rb → conditional_blocks_spec.rb} +0 -0
  23. data/spec/dummy/.gitignore +1 -0
  24. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  25. data/spec/dummy/app/controllers/dashboards_controller.rb +13 -0
  26. data/spec/dummy/app/helpers/application_helper.rb +5 -0
  27. data/spec/dummy/app/presenters/dashboards/collection_presenter.rb +7 -0
  28. data/spec/dummy/app/presenters/dashboards/item_presenter.rb +7 -0
  29. data/spec/dummy/app/presenters/dashboards/new_presenter.rb +19 -0
  30. data/spec/dummy/app/presenters/dashboards/partials_presenter.rb +5 -0
  31. data/spec/dummy/app/presenters/dashboards/show_presenter.rb +12 -0
  32. data/spec/dummy/app/presenters/layouts/application_presenter.rb +9 -0
  33. data/spec/dummy/app/views/dashboards/_item.html.curly +1 -0
  34. data/spec/dummy/app/views/dashboards/collection.html.curly +5 -0
  35. data/spec/dummy/app/views/dashboards/new.html.curly +3 -0
  36. data/spec/dummy/app/views/dashboards/partials.html.curly +3 -0
  37. data/spec/dummy/app/views/dashboards/show.html.curly +3 -0
  38. data/spec/dummy/app/views/layouts/application.html.curly +8 -0
  39. data/spec/dummy/config.ru +4 -0
  40. data/spec/dummy/config/application.rb +12 -0
  41. data/spec/dummy/config/boot.rb +5 -0
  42. data/spec/dummy/config/environment.rb +5 -0
  43. data/spec/dummy/config/environments/test.rb +36 -0
  44. data/spec/dummy/config/routes.rb +6 -0
  45. data/spec/integration/application_layout_spec.rb +21 -0
  46. data/spec/integration/collection_blocks_spec.rb +17 -78
  47. data/spec/integration/context_blocks_spec.rb +21 -0
  48. data/spec/integration/partials_spec.rb +23 -0
  49. data/spec/parser_spec.rb +95 -0
  50. data/spec/scanner_spec.rb +24 -14
  51. data/spec/spec_helper.rb +4 -3
  52. metadata +49 -14
  53. data/lib/curly/component_parser.rb +0 -13
  54. data/spec/attribute_parser_spec.rb +0 -46
  55. data/spec/incorrect_ending_error_spec.rb +0 -13
@@ -1,46 +1,46 @@
1
1
  module Curly
2
2
  class ComponentCompiler
3
- attr_reader :presenter_class, :method
3
+ attr_reader :presenter_class, :component, :type
4
4
 
5
- def initialize(presenter_class, method)
6
- @presenter_class, @method = presenter_class, method
5
+ def self.compile(presenter_class, component, type: nil)
6
+ new(presenter_class, component, type: type).compile
7
7
  end
8
8
 
9
- def self.compile_component(presenter_class, method, argument, attributes)
10
- new(presenter_class, method).compile(argument, attributes)
9
+ def initialize(presenter_class, component, type: nil)
10
+ @presenter_class, @component, @type = presenter_class, component, type
11
11
  end
12
12
 
13
- def self.compile_conditional(presenter_class, method, argument, attributes)
14
- if argument && argument.end_with?("?")
15
- method += "?"
16
- argument = argument[0..-2]
17
- end
18
-
19
- unless method.end_with?("?")
20
- raise Curly::Error, "conditional components must end with `?`"
21
- end
22
-
23
- new(presenter_class, method).compile(argument, attributes)
24
- end
25
-
26
- def compile(argument, attributes = {})
13
+ def compile
27
14
  unless presenter_class.component_available?(method)
28
15
  raise Curly::InvalidComponent.new(method)
29
16
  end
30
17
 
31
- validate_attributes(attributes)
18
+ validate_block_argument!
19
+ validate_attributes!
32
20
 
33
21
  code = "presenter.#{method}("
34
22
 
35
- append_positional_argument(code, argument)
36
- append_keyword_arguments(code, argument, attributes)
23
+ append_positional_argument(code)
24
+ append_keyword_arguments(code)
37
25
 
38
26
  code << ")"
39
27
  end
40
28
 
41
29
  private
42
30
 
43
- def append_positional_argument(code, argument)
31
+ def method
32
+ component.name
33
+ end
34
+
35
+ def argument
36
+ component.identifier
37
+ end
38
+
39
+ def attributes
40
+ component.attributes
41
+ end
42
+
43
+ def append_positional_argument(code)
44
44
  if required_identifier?
45
45
  if argument.nil?
46
46
  raise Curly::Error, "`#{method}` requires an identifier"
@@ -56,9 +56,7 @@ module Curly
56
56
  end
57
57
  end
58
58
 
59
- def append_keyword_arguments(code, argument, attributes)
60
- keyword_argument_string = build_keyword_argument_string(attributes)
61
-
59
+ def append_keyword_arguments(code)
62
60
  unless keyword_argument_string.empty?
63
61
  code << ", " unless argument.nil?
64
62
  code << keyword_argument_string
@@ -78,19 +76,27 @@ module Curly
78
76
  param_types.include?(:opt)
79
77
  end
80
78
 
81
- def build_keyword_argument_string(kwargs)
82
- kwargs.map {|name, value| "#{name}: #{value.inspect}" }.join(", ")
79
+ def keyword_argument_string
80
+ @keyword_argument_string ||= attributes.map {|name, value|
81
+ "#{name}: #{value.inspect}"
82
+ }.join(", ")
83
+ end
84
+
85
+ def validate_block_argument!
86
+ if type == :context && !param_types.include?(:block)
87
+ raise Curly::Error, "`#{method}` cannot be used as a context block"
88
+ end
83
89
  end
84
90
 
85
- def validate_attributes(kwargs)
86
- kwargs.keys.each do |key|
91
+ def validate_attributes!
92
+ attributes.keys.each do |key|
87
93
  unless attribute_names.include?(key)
88
94
  raise Curly::Error, "`#{method}` does not allow attribute `#{key}`"
89
95
  end
90
96
  end
91
97
 
92
98
  required_attribute_names.each do |key|
93
- unless kwargs.key?(key)
99
+ unless attributes.key?(key)
94
100
  raise Curly::Error, "`#{method}` is missing the required attribute `#{key}`"
95
101
  end
96
102
  end
@@ -0,0 +1,19 @@
1
+ require 'curly/attribute_scanner'
2
+
3
+ module Curly
4
+ class ComponentScanner
5
+ def self.scan(component)
6
+ first, rest = component.split(/\s+/, 2)
7
+ name, identifier = first.split(".", 2)
8
+
9
+ if identifier && identifier.end_with?("?")
10
+ name += "?"
11
+ identifier = identifier[0..-2]
12
+ end
13
+
14
+ attributes = AttributeScanner.scan(rest)
15
+
16
+ [name, identifier, attributes]
17
+ end
18
+ end
19
+ end
@@ -1,11 +1,4 @@
1
1
  module Curly
2
2
  class IncompleteBlockError < Error
3
- def initialize(component)
4
- @component = component
5
- end
6
-
7
- def message
8
- "error compiling `{{##{@component}}}`: conditional block must be terminated with `{{/#{@component}}}}`"
9
- end
10
3
  end
11
4
  end
@@ -1,25 +1,4 @@
1
1
  module Curly
2
2
  class IncorrectEndingError < Error
3
- def initialize(actual_block, expected_block)
4
- @actual_block, @expected_block = actual_block, expected_block
5
- end
6
-
7
- def message
8
- "compilation error: expected `{{/#{expected_block}}}`, got `{{/#{actual_block}}}`"
9
- end
10
-
11
- private
12
-
13
- def actual_block
14
- present_block(@actual_block)
15
- end
16
-
17
- def expected_block
18
- present_block(@expected_block)
19
- end
20
-
21
- def present_block(block)
22
- block.compact.join(".")
23
- end
24
3
  end
25
4
  end
@@ -0,0 +1,171 @@
1
+ require 'curly/incomplete_block_error'
2
+ require 'curly/incorrect_ending_error'
3
+
4
+ class Curly::Parser
5
+ class Component
6
+ attr_reader :name, :identifier, :attributes
7
+
8
+ def initialize(name, identifier = nil, attributes = {})
9
+ @name, @identifier, @attributes = name, identifier, attributes
10
+ end
11
+
12
+ def to_s
13
+ [name, identifier].compact.join(".")
14
+ end
15
+
16
+ def ==(other)
17
+ other.name == name &&
18
+ other.identifier == identifier &&
19
+ other.attributes == attributes
20
+ end
21
+
22
+ def type
23
+ :component
24
+ end
25
+ end
26
+
27
+ class Text
28
+ attr_reader :value
29
+
30
+ def initialize(value)
31
+ @value = value
32
+ end
33
+
34
+ def type
35
+ :text
36
+ end
37
+ end
38
+
39
+ class Comment
40
+ attr_reader :value
41
+
42
+ def initialize(value)
43
+ @value = value
44
+ end
45
+
46
+ def type
47
+ :comment
48
+ end
49
+ end
50
+
51
+ class Root
52
+ attr_reader :nodes
53
+
54
+ def initialize
55
+ @nodes = []
56
+ end
57
+
58
+ def <<(node)
59
+ @nodes << node
60
+ end
61
+
62
+ def to_s
63
+ "<root>"
64
+ end
65
+
66
+ def closed_by?(component)
67
+ false
68
+ end
69
+ end
70
+
71
+ class Block
72
+ attr_reader :type, :component, :nodes
73
+
74
+ def initialize(type, component, nodes = [])
75
+ @type, @component, @nodes = type, component, nodes
76
+ end
77
+
78
+ def closed_by?(component)
79
+ self.component.name == component.name &&
80
+ self.component.identifier == component.identifier
81
+ end
82
+
83
+ def to_s
84
+ component.to_s
85
+ end
86
+
87
+ def <<(node)
88
+ @nodes << node
89
+ end
90
+
91
+ def ==(other)
92
+ other.type == type &&
93
+ other.component == component &&
94
+ other.nodes == nodes
95
+ end
96
+ end
97
+
98
+ def self.parse(tokens)
99
+ new(tokens).parse
100
+ end
101
+
102
+ def initialize(tokens)
103
+ @tokens = tokens
104
+ @root = Root.new
105
+ @stack = [@root]
106
+ end
107
+
108
+ def parse
109
+ @tokens.each do |token, *args|
110
+ send("parse_#{token}", *args)
111
+ end
112
+
113
+ unless @stack.size == 1
114
+ raise Curly::IncompleteBlockError,
115
+ "block `#{@stack.last}` is not closed"
116
+ end
117
+
118
+ @root.nodes
119
+ end
120
+
121
+ private
122
+
123
+ def parse_text(value)
124
+ tree << Text.new(value)
125
+ end
126
+
127
+ def parse_component(*args)
128
+ tree << Component.new(*args)
129
+ end
130
+
131
+ def parse_conditional_block_start(*args)
132
+ parse_block(:conditional, *args)
133
+ end
134
+
135
+ def parse_inverse_conditional_block_start(*args)
136
+ parse_block(:inverse_conditional, *args)
137
+ end
138
+
139
+ def parse_collection_block_start(*args)
140
+ parse_block(:collection, *args)
141
+ end
142
+
143
+ def parse_context_block_start(*args)
144
+ parse_block(:context, *args)
145
+ end
146
+
147
+ def parse_block(type, *args)
148
+ component = Component.new(*args)
149
+ block = Block.new(type, component)
150
+ tree << block
151
+ @stack.push(block)
152
+ end
153
+
154
+ def parse_block_end(*args)
155
+ component = Component.new(*args)
156
+ block = @stack.pop
157
+
158
+ unless block.closed_by?(component)
159
+ raise Curly::IncorrectEndingError,
160
+ "block `#{block}` cannot be closed by `#{component}`"
161
+ end
162
+ end
163
+
164
+ def parse_comment(comment)
165
+ tree << Comment.new(comment)
166
+ end
167
+
168
+ def tree
169
+ @stack.last
170
+ end
171
+ end
@@ -86,7 +86,7 @@ module Curly
86
86
  #
87
87
  # - is a String,
88
88
  # - responds to #cache_key itself, or
89
- # - is an Array of a Hash whose items themselves fit either of these
89
+ # - is an Array or a Hash whose items themselves fit either of these
90
90
  # criteria.
91
91
  #
92
92
  # Returns the cache key Object or nil if no caching should be performed.
@@ -1,4 +1,5 @@
1
1
  require 'strscan'
2
+ require 'curly/component_scanner'
2
3
  require 'curly/syntax_error'
3
4
 
4
5
  module Curly
@@ -15,6 +16,7 @@ module Curly
15
16
  ESCAPED_CURLY_START = /\{\{\{/
16
17
 
17
18
  COMMENT_MARKER = /!/
19
+ CONTEXT_BLOCK_MARKER = /@/
18
20
  CONDITIONAL_BLOCK_MARKER = /#/
19
21
  INVERSE_BLOCK_MARKER = /\^/
20
22
  COLLECTION_BLOCK_MARKER = /\*/
@@ -73,6 +75,8 @@ module Curly
73
75
  scan_comment
74
76
  elsif @scanner.scan(CONDITIONAL_BLOCK_MARKER)
75
77
  scan_conditional_block_start
78
+ elsif @scanner.scan(CONTEXT_BLOCK_MARKER)
79
+ scan_context_block_start
76
80
  elsif @scanner.scan(INVERSE_BLOCK_MARKER)
77
81
  scan_inverse_block_start
78
82
  elsif @scanner.scan(COLLECTION_BLOCK_MARKER)
@@ -92,35 +96,45 @@ module Curly
92
96
 
93
97
  def scan_conditional_block_start
94
98
  if value = scan_until_end_of_curly
95
- [:conditional_block_start, value]
99
+ name, identifier, attributes = ComponentScanner.scan(value)
100
+
101
+ [:conditional_block_start, name, identifier, attributes]
102
+ end
103
+ end
104
+
105
+ def scan_context_block_start
106
+ if value = scan_until_end_of_curly
107
+ name, identifier, attributes = ComponentScanner.scan(value)
108
+
109
+ [:context_block_start, name, identifier, attributes]
96
110
  end
97
111
  end
98
112
 
99
113
  def scan_collection_block_start
100
114
  if value = scan_until_end_of_curly
101
- [:collection_block_start, value]
115
+ name, identifier, attributes = ComponentScanner.scan(value)
116
+ [:collection_block_start, name, identifier, attributes]
102
117
  end
103
118
  end
104
119
 
105
120
  def scan_inverse_block_start
106
121
  if value = scan_until_end_of_curly
107
- [:inverse_conditional_block_start, value]
122
+ name, identifier, attributes = ComponentScanner.scan(value)
123
+ [:inverse_conditional_block_start, name, identifier, attributes]
108
124
  end
109
125
  end
110
126
 
111
127
  def scan_block_end
112
128
  if value = scan_until_end_of_curly
113
- if value.end_with?("?")
114
- [:conditional_block_end, value]
115
- else
116
- [:collection_block_end, value]
117
- end
129
+ name, identifier, attributes = ComponentScanner.scan(value)
130
+ [:block_end, name, identifier]
118
131
  end
119
132
  end
120
133
 
121
134
  def scan_component
122
135
  if value = scan_until_end_of_curly
123
- [:component, value]
136
+ name, identifier, attributes = ComponentScanner.scan(value)
137
+ [:component, name, identifier, attributes]
124
138
  end
125
139
  end
126
140
 
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe Curly::AttributeScanner do
4
+ it "scans attributes" do
5
+ scan("width=10px height=20px").should == {
6
+ "width" => "10px",
7
+ "height" => "20px"
8
+ }
9
+ end
10
+
11
+ it "scans single quoted values" do
12
+ scan("title='hello world'").should == { "title" => "hello world" }
13
+ end
14
+
15
+ it "scans double quoted values" do
16
+ scan('title="hello world"').should == { "title" => "hello world" }
17
+ end
18
+
19
+ it "scans mixed quotes" do
20
+ scan(%[x=y q="foo's bar" v='bim " bum' t="foo ' bar"]).should == {
21
+ "x" => "y",
22
+ "q" => "foo's bar",
23
+ "t" => "foo ' bar",
24
+ "v" => 'bim " bum'
25
+ }
26
+ end
27
+
28
+ it "deals with weird whitespace" do
29
+ scan(" size=big ").should == { "size" => "big" }
30
+ end
31
+
32
+ it "scans empty attribute lists" do
33
+ scan(nil).should == {}
34
+ scan("").should == {}
35
+ scan(" ").should == {}
36
+ end
37
+
38
+ it "fails when an invalid attribute list is passed" do
39
+ expect { scan("foo") }.to raise_exception(Curly::AttributeError)
40
+ expect { scan("foo=") }.to raise_exception(Curly::AttributeError)
41
+ end
42
+
43
+ def scan(str)
44
+ described_class.scan(str)
45
+ end
46
+ end