curly-templates 1.0.1 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/curly/scanner.rb CHANGED
@@ -15,8 +15,9 @@ module Curly
15
15
  ESCAPED_CURLY_START = /\{\{\{/
16
16
 
17
17
  COMMENT_MARKER = /!/
18
- BLOCK_MARKER = /#/
18
+ CONDITIONAL_BLOCK_MARKER = /#/
19
19
  INVERSE_BLOCK_MARKER = /\^/
20
+ COLLECTION_BLOCK_MARKER = /\*/
20
21
  END_BLOCK_MARKER = /\//
21
22
 
22
23
 
@@ -27,7 +28,7 @@ module Curly
27
28
  # Example
28
29
  #
29
30
  # Curly::Scanner.scan("hello {{name}}!")
30
- # #=> [[:text, "hello "], [:reference, "name"], [:text, "!"]]
31
+ # #=> [[:text, "hello "], [:component, "name"], [:text, "!"]]
31
32
  #
32
33
  # Returns an Array of type/value pairs representing the tokens in the
33
34
  # template.
@@ -70,14 +71,16 @@ module Curly
70
71
  def scan_tag
71
72
  if @scanner.scan(COMMENT_MARKER)
72
73
  scan_comment
73
- elsif @scanner.scan(BLOCK_MARKER)
74
- scan_block_start
74
+ elsif @scanner.scan(CONDITIONAL_BLOCK_MARKER)
75
+ scan_conditional_block_start
75
76
  elsif @scanner.scan(INVERSE_BLOCK_MARKER)
76
77
  scan_inverse_block_start
78
+ elsif @scanner.scan(COLLECTION_BLOCK_MARKER)
79
+ scan_collection_block_start
77
80
  elsif @scanner.scan(END_BLOCK_MARKER)
78
81
  scan_block_end
79
82
  else
80
- scan_reference
83
+ scan_component
81
84
  end
82
85
  end
83
86
 
@@ -87,27 +90,37 @@ module Curly
87
90
  end
88
91
  end
89
92
 
90
- def scan_block_start
93
+ def scan_conditional_block_start
91
94
  if value = scan_until_end_of_curly
92
- [:block_start, value]
95
+ [:conditional_block_start, value]
96
+ end
97
+ end
98
+
99
+ def scan_collection_block_start
100
+ if value = scan_until_end_of_curly
101
+ [:collection_block_start, value]
93
102
  end
94
103
  end
95
104
 
96
105
  def scan_inverse_block_start
97
106
  if value = scan_until_end_of_curly
98
- [:inverse_block_start, value]
107
+ [:inverse_conditional_block_start, value]
99
108
  end
100
109
  end
101
110
 
102
111
  def scan_block_end
103
112
  if value = scan_until_end_of_curly
104
- [:block_end, value]
113
+ if value.end_with?("?")
114
+ [:conditional_block_end, value]
115
+ else
116
+ [:collection_block_end, value]
117
+ end
105
118
  end
106
119
  end
107
120
 
108
- def scan_reference
121
+ def scan_component
109
122
  if value = scan_until_end_of_curly
110
- [:reference, value]
123
+ [:component, value]
111
124
  end
112
125
  end
113
126
 
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe Curly::AttributeParser do
4
+ it "parses attributes" do
5
+ parse("width=10px height=20px").should == {
6
+ "width" => "10px",
7
+ "height" => "20px"
8
+ }
9
+ end
10
+
11
+ it "parses single quoted values" do
12
+ parse("title='hello world'").should == { "title" => "hello world" }
13
+ end
14
+
15
+ it "parses double quoted values" do
16
+ parse('title="hello world"').should == { "title" => "hello world" }
17
+ end
18
+
19
+ it "parses mixed quotes" do
20
+ parse(%[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
+ parse(" size=big ").should == { "size" => "big" }
30
+ end
31
+
32
+ it "parses empty attribute lists" do
33
+ parse(nil).should == {}
34
+ parse("").should == {}
35
+ parse(" ").should == {}
36
+ end
37
+
38
+ it "fails when an invalid attribute list is passed" do
39
+ expect { parse("foo") }.to raise_exception(Curly::AttributeError)
40
+ expect { parse("foo=") }.to raise_exception(Curly::AttributeError)
41
+ end
42
+
43
+ def parse(str)
44
+ described_class.parse(str)
45
+ end
46
+ end
@@ -0,0 +1,153 @@
1
+ require 'spec_helper'
2
+
3
+ describe Curly::Compiler do
4
+ include CompilationSupport
5
+
6
+ let(:presenter_class) do
7
+ Class.new(Curly::Presenter) do
8
+ presents :list
9
+
10
+ def title
11
+ @list.title
12
+ end
13
+
14
+ def items(status: nil)
15
+ if status
16
+ @list.items.select {|item| item.status == status }
17
+ else
18
+ @list.items
19
+ end
20
+ end
21
+
22
+ def companies
23
+ "Nike, Adidas"
24
+ end
25
+
26
+ def numbers
27
+ "one, two, three"
28
+ end
29
+ end
30
+ end
31
+
32
+ let(:simple_presenter_class) do
33
+ Class.new(Curly::Presenter) do
34
+ presents :company
35
+
36
+ def name
37
+ @company
38
+ end
39
+ end
40
+ end
41
+
42
+ let(:inner_presenter_class) do
43
+ Class.new(Curly::Presenter) do
44
+ presents :item, :item_counter
45
+ presents :list, default: nil
46
+
47
+ attr_reader :item_counter
48
+
49
+ def name
50
+ @item.name
51
+ end
52
+
53
+ def list_title
54
+ @list.title
55
+ end
56
+
57
+ def parts
58
+ @item.parts
59
+ end
60
+ end
61
+ end
62
+
63
+ let(:inner_inner_presenter_class) do
64
+ Class.new(Curly::Presenter) do
65
+ presents :part
66
+
67
+ def identifier
68
+ @part.identifier
69
+ end
70
+ end
71
+ end
72
+
73
+ let(:list) { double("list", title: "Inventory") }
74
+ let(:context) { double("context") }
75
+ let(:presenter) { presenter_class.new(context, list: list) }
76
+
77
+ before do
78
+ stub_const("ItemPresenter", inner_presenter_class)
79
+ stub_const("PartPresenter", inner_inner_presenter_class)
80
+ end
81
+
82
+ it "compiles collection blocks" do
83
+ item1 = double("item1", name: "foo")
84
+ item2 = double("item2", name: "bar")
85
+
86
+ list.stub(:items) { [item1, item2] }
87
+
88
+ template = "<ul>{{*items}}<li>{{name}}</li>{{/items}}</ul>"
89
+ expect(evaluate(template)).to eql "<ul><li>foo</li><li>bar</li></ul>"
90
+ end
91
+
92
+ it "allows attributes on collection blocks" do
93
+ item1 = double("item1", name: "foo", status: "active")
94
+ item2 = double("item2", name: "bar", status: "inactive")
95
+
96
+ list.stub(:items) { [item1, item2] }
97
+
98
+ template = "<ul>{{*items status=active}}<li>{{name}}</li>{{/items}}</ul>"
99
+ expect(evaluate(template)).to eql "<ul><li>foo</li></ul>"
100
+ end
101
+
102
+ it "fails if the component isn't available" do
103
+ template = "<ul>{{*doodads}}<li>{{name}}</li>{{/doodads}}</ul>"
104
+ expect { evaluate(template) }.to raise_exception(Curly::Error)
105
+ end
106
+
107
+ it "fails if the component doesn't support enumeration" do
108
+ template = "<ul>{{*numbers}}<li>{{name}}</li>{{/numbers}}</ul>"
109
+ expect { evaluate(template) }.to raise_exception(Curly::Error)
110
+ end
111
+
112
+ it "works even if the component method doesn't return an Array" do
113
+ stub_const("CompanyPresenter", simple_presenter_class)
114
+ template = "<ul>{{*companies}}<li>{{name}}</li>{{/companies}}</ul>"
115
+ expect(evaluate(template)).to eql "<ul><li>Nike, Adidas</li></ul>"
116
+ end
117
+
118
+ it "passes the index of the current item to the nested presenter" do
119
+ item1 = double("item1")
120
+ item2 = double("item2")
121
+
122
+ list.stub(:items) { [item1, item2] }
123
+
124
+ template = "<ul>{{*items}}<li>{{item_counter}}</li>{{/items}}</ul>"
125
+ expect(evaluate(template)).to eql "<ul><li>1</li><li>2</li></ul>"
126
+ end
127
+
128
+ it "restores the previous scope after exiting the collection block" do
129
+ part = double("part", identifier: "X")
130
+ item = double("item", name: "foo", parts: [part])
131
+ list.stub(:items) { [item] }
132
+
133
+ template = "{{*items}}{{*parts}}{{identifier}}{{/parts}}{{name}}{{/items}}{{title}}"
134
+ expect(evaluate(template)).to eql "XfooInventory"
135
+ end
136
+
137
+ it "passes the parent presenter's options to the nested presenter" do
138
+ list.stub(:items) { [double(name: "foo"), double(name: "bar")] }
139
+
140
+ template = "{{*items}}{{list_title}}: {{name}}. {{/items}}"
141
+ expect(evaluate(template, list: list)).to eql "Inventory: foo. Inventory: bar. "
142
+ end
143
+
144
+ it "compiles nested collection blocks" do
145
+ item1 = double("item1", name: "item1", parts: [double(identifier: "A"), double(identifier: "B")])
146
+ item2 = double("item2", name: "item2", parts: [double(identifier: "C"), double(identifier: "D")])
147
+
148
+ list.stub(:items) { [item1, item2] }
149
+
150
+ template = "{{title}}: {{*items}}{{name}} - {{*parts}}{{identifier}}{{/parts}}. {{/items}}"
151
+ expect(evaluate(template)).to eql "Inventory: item1 - AB. item2 - CD. "
152
+ end
153
+ end
@@ -1,6 +1,8 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Curly::Compiler do
4
+ include CompilationSupport
5
+
4
6
  let :presenter_class do
5
7
  Class.new do
6
8
  def foo
@@ -27,6 +29,10 @@ describe Curly::Compiler do
27
29
  nil
28
30
  end
29
31
 
32
+ def square?(width:, height:)
33
+ width.to_i == height.to_i
34
+ end
35
+
30
36
  def false?
31
37
  false
32
38
  end
@@ -35,16 +41,11 @@ describe Curly::Compiler do
35
41
  true
36
42
  end
37
43
 
38
- def parameterized(value)
39
- value
40
- end
41
-
42
- def self.method_available?(method)
43
- [:foo, :parameterized, :high_yield, :yield_value, :dirty,
44
- :false?, :true?, :hello?].include?(method)
44
+ def self.component_available?(method)
45
+ %w[foo high_yield yield_value dirty false? true? hello? square?].include?(method)
45
46
  end
46
47
 
47
- def self.available_methods
48
+ def self.available_components
48
49
  public_instance_methods
49
50
  end
50
51
 
@@ -63,14 +64,6 @@ describe Curly::Compiler do
63
64
  evaluate("{{foo}}").should == "FOO"
64
65
  end
65
66
 
66
- it "passes on an optional reference parameter to the presenter method" do
67
- evaluate("{{parameterized.foo.bar}}").should == "foo.bar"
68
- end
69
-
70
- it "passes an empty string to methods that take a parameter when none is provided" do
71
- evaluate("{{parameterized}}").should == ""
72
- end
73
-
74
67
  it "raises ArgumentError if the presenter class is nil" do
75
68
  expect do
76
69
  Curly::Compiler.compile("foo", nil)
@@ -78,15 +71,15 @@ describe Curly::Compiler do
78
71
  end
79
72
 
80
73
  it "makes sure only public methods are called on the presenter object" do
81
- expect { evaluate("{{bar}}") }.to raise_exception(Curly::InvalidReference)
74
+ expect { evaluate("{{bar}}") }.to raise_exception(Curly::InvalidComponent)
82
75
  end
83
76
 
84
- it "includes the invalid reference when failing to compile" do
77
+ it "includes the invalid component when failing to compile" do
85
78
  begin
86
79
  evaluate("{{bar}}")
87
80
  fail
88
- rescue Curly::InvalidReference => e
89
- e.reference.should == :bar
81
+ rescue Curly::InvalidComponent => e
82
+ e.component.should == "bar"
90
83
  end
91
84
  end
92
85
 
@@ -141,6 +134,10 @@ describe Curly::Compiler do
141
134
  evaluate("{{#hello.world?}}foo{{/hello.world?}}{{#hello.foo?}}bar{{/hello.foo?}}").should == "foo"
142
135
  end
143
136
 
137
+ it "passes attributes to blocks" do
138
+ evaluate("{{#square? width=2 height=2}}yeah!{{/square?}}").should == "yeah!"
139
+ end
140
+
144
141
  it "gives an error on mismatching blocks" do
145
142
  expect do
146
143
  evaluate("test{{#false?}}bar{{/true?}}")
@@ -174,7 +171,7 @@ describe Curly::Compiler do
174
171
  end
175
172
 
176
173
  it "returns false if an unavailable method is referenced" do
177
- presenter_class.stub(:available_methods) { [:foo] }
174
+ presenter_class.stub(:available_components) { [:foo] }
178
175
  validate("Hello, {{inspect}}").should == false
179
176
  end
180
177
 
@@ -190,17 +187,4 @@ describe Curly::Compiler do
190
187
  Curly.valid?(template, presenter_class)
191
188
  end
192
189
  end
193
-
194
- def evaluate(template, &block)
195
- code = Curly::Compiler.compile(template, presenter_class)
196
- context = double("context", presenter: presenter)
197
-
198
- context.instance_eval(<<-RUBY)
199
- def self.render
200
- #{code}
201
- end
202
- RUBY
203
-
204
- context.render(&block)
205
- end
206
190
  end
@@ -0,0 +1,160 @@
1
+ require 'spec_helper'
2
+
3
+ describe Curly::ComponentCompiler do
4
+ describe ".compile_conditional" do
5
+ let(:presenter_class) do
6
+ Class.new do
7
+ def monday?
8
+ true
9
+ end
10
+
11
+ def tuesday?
12
+ false
13
+ end
14
+
15
+ def day?(name)
16
+ name == "monday"
17
+ end
18
+
19
+ def season?(name:)
20
+ name == "summer"
21
+ end
22
+
23
+ def hello
24
+ "hello"
25
+ end
26
+
27
+ def self.component_available?(name)
28
+ true
29
+ end
30
+ end
31
+ end
32
+
33
+ it "compiles simple components" do
34
+ evaluate("monday?").should == true
35
+ evaluate("tuesday?").should == false
36
+ end
37
+
38
+ it "compiles components with an identifier" do
39
+ evaluate("day.monday?").should == true
40
+ evaluate("day.tuesday?").should == false
41
+ end
42
+
43
+ it "compiles components with attributes" do
44
+ evaluate("season? name=summer").should == true
45
+ evaluate("season? name=winter").should == false
46
+ end
47
+
48
+ it "fails if the component is missing a question mark" do
49
+ expect { evaluate("hello") }.to raise_exception(Curly::Error)
50
+ end
51
+
52
+ def evaluate(component, &block)
53
+ method, argument, attributes = Curly::ComponentParser.parse(component)
54
+ code = Curly::ComponentCompiler.compile_conditional(presenter_class, method, argument, attributes)
55
+ presenter = presenter_class.new
56
+ context = double("context", presenter: presenter)
57
+
58
+ context.instance_eval(<<-RUBY)
59
+ def self.render
60
+ #{code}
61
+ end
62
+ RUBY
63
+
64
+ context.render(&block)
65
+ end
66
+ end
67
+
68
+ describe ".compile_component" do
69
+ let(:presenter_class) do
70
+ Class.new do
71
+ def title
72
+ "Welcome!"
73
+ end
74
+
75
+ def i18n(key, fallback: nil)
76
+ case key
77
+ when "home.welcome" then "Welcome to our lovely place!"
78
+ else fallback
79
+ end
80
+ end
81
+
82
+ def summary(length = "long")
83
+ case length
84
+ when "long" then "This is a long summary"
85
+ when "short" then "This is a short summary"
86
+ end
87
+ end
88
+
89
+ def invalid(x, y)
90
+ end
91
+
92
+ def widget(size:, color: nil)
93
+ s = "Widget (#{size})"
94
+ s << " - #{color}" if color
95
+ s
96
+ end
97
+
98
+ def self.component_available?(name)
99
+ true
100
+ end
101
+ end
102
+ end
103
+
104
+ it "compiles components with identifiers" do
105
+ evaluate("i18n.home.welcome").should == "Welcome to our lovely place!"
106
+ end
107
+
108
+ it "compiles components with optional identifiers" do
109
+ evaluate("summary").should == "This is a long summary"
110
+ evaluate("summary.short").should == "This is a short summary"
111
+ end
112
+
113
+ it "compiles components with attributes" do
114
+ evaluate("widget size=100px").should == "Widget (100px)"
115
+ end
116
+
117
+ it "compiles components with optional attributes" do
118
+ evaluate("widget color=blue size=50px").should == "Widget (50px) - blue"
119
+ end
120
+
121
+ it "allows both identifier and attributes" do
122
+ evaluate("i18n.hello fallback=yolo").should == "yolo"
123
+ end
124
+
125
+ it "fails when an invalid attribute is used" do
126
+ expect { evaluate("i18n.foo extreme=true") }.to raise_exception(Curly::Error)
127
+ end
128
+
129
+ it "fails when a component is missing a required identifier" do
130
+ expect { evaluate("i18n") }.to raise_exception(Curly::Error)
131
+ end
132
+
133
+ it "fails when a component is missing a required attribute" do
134
+ expect { evaluate("widget") }.to raise_exception(Curly::Error)
135
+ end
136
+
137
+ it "fails when an identifier is specified for a component that doesn't support one" do
138
+ expect { evaluate("title.rugby") }.to raise_exception(Curly::Error)
139
+ end
140
+
141
+ it "fails when the method takes more than one argument" do
142
+ expect { evaluate("invalid") }.to raise_exception(Curly::Error)
143
+ end
144
+
145
+ def evaluate(component, &block)
146
+ method, argument, attributes = Curly::ComponentParser.parse(component)
147
+ code = Curly::ComponentCompiler.compile_component(presenter_class, method, argument, attributes)
148
+ presenter = presenter_class.new
149
+ context = double("context", presenter: presenter)
150
+
151
+ context.instance_eval(<<-RUBY)
152
+ def self.render
153
+ #{code}
154
+ end
155
+ RUBY
156
+
157
+ context.render(&block)
158
+ end
159
+ end
160
+ end