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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/Gemfile +2 -0
- data/README.md +85 -5
- data/curly-templates.gemspec +37 -8
- data/lib/curly.rb +1 -1
- data/lib/curly/{attribute_parser.rb → attribute_scanner.rb} +6 -4
- data/lib/curly/compiler.rb +81 -72
- data/lib/curly/component_compiler.rb +37 -31
- data/lib/curly/component_scanner.rb +19 -0
- data/lib/curly/incomplete_block_error.rb +0 -7
- data/lib/curly/incorrect_ending_error.rb +0 -21
- data/lib/curly/parser.rb +171 -0
- data/lib/curly/presenter.rb +1 -1
- data/lib/curly/scanner.rb +23 -9
- data/spec/attribute_scanner_spec.rb +46 -0
- data/spec/collection_blocks_spec.rb +88 -0
- data/spec/compiler/context_blocks_spec.rb +42 -0
- data/spec/component_compiler_spec.rb +26 -77
- data/spec/component_scanner_spec.rb +19 -0
- data/spec/{integration/components_spec.rb → components_spec.rb} +0 -0
- data/spec/{integration/conditional_blocks_spec.rb → conditional_blocks_spec.rb} +0 -0
- data/spec/dummy/.gitignore +1 -0
- data/spec/dummy/app/controllers/application_controller.rb +2 -0
- data/spec/dummy/app/controllers/dashboards_controller.rb +13 -0
- data/spec/dummy/app/helpers/application_helper.rb +5 -0
- data/spec/dummy/app/presenters/dashboards/collection_presenter.rb +7 -0
- data/spec/dummy/app/presenters/dashboards/item_presenter.rb +7 -0
- data/spec/dummy/app/presenters/dashboards/new_presenter.rb +19 -0
- data/spec/dummy/app/presenters/dashboards/partials_presenter.rb +5 -0
- data/spec/dummy/app/presenters/dashboards/show_presenter.rb +12 -0
- data/spec/dummy/app/presenters/layouts/application_presenter.rb +9 -0
- data/spec/dummy/app/views/dashboards/_item.html.curly +1 -0
- data/spec/dummy/app/views/dashboards/collection.html.curly +5 -0
- data/spec/dummy/app/views/dashboards/new.html.curly +3 -0
- data/spec/dummy/app/views/dashboards/partials.html.curly +3 -0
- data/spec/dummy/app/views/dashboards/show.html.curly +3 -0
- data/spec/dummy/app/views/layouts/application.html.curly +8 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +12 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/test.rb +36 -0
- data/spec/dummy/config/routes.rb +6 -0
- data/spec/integration/application_layout_spec.rb +21 -0
- data/spec/integration/collection_blocks_spec.rb +17 -78
- data/spec/integration/context_blocks_spec.rb +21 -0
- data/spec/integration/partials_spec.rb +23 -0
- data/spec/parser_spec.rb +95 -0
- data/spec/scanner_spec.rb +24 -14
- data/spec/spec_helper.rb +4 -3
- metadata +49 -14
- data/lib/curly/component_parser.rb +0 -13
- data/spec/attribute_parser_spec.rb +0 -46
- 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, :
|
3
|
+
attr_reader :presenter_class, :component, :type
|
4
4
|
|
5
|
-
def
|
6
|
-
|
5
|
+
def self.compile(presenter_class, component, type: nil)
|
6
|
+
new(presenter_class, component, type: type).compile
|
7
7
|
end
|
8
8
|
|
9
|
-
def
|
10
|
-
|
9
|
+
def initialize(presenter_class, component, type: nil)
|
10
|
+
@presenter_class, @component, @type = presenter_class, component, type
|
11
11
|
end
|
12
12
|
|
13
|
-
def
|
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
|
-
|
18
|
+
validate_block_argument!
|
19
|
+
validate_attributes!
|
32
20
|
|
33
21
|
code = "presenter.#{method}("
|
34
22
|
|
35
|
-
append_positional_argument(code
|
36
|
-
append_keyword_arguments(code
|
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
|
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
|
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
|
82
|
-
|
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
|
86
|
-
|
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
|
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,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
|
data/lib/curly/parser.rb
ADDED
@@ -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
|
data/lib/curly/presenter.rb
CHANGED
@@ -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
|
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.
|
data/lib/curly/scanner.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
114
|
-
|
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
|
-
|
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
|