curly-templates 1.0.1 → 2.0.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.
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