curly-templates 2.0.1 → 2.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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