lilu 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,268 @@
1
+ require 'rubygems'
2
+ require 'hpricot'
3
+ require 'active_support'
4
+
5
+ class Hpricot::Elem
6
+ attr_accessor :cache_search
7
+ alias :_search :search
8
+ def search(expr,&block)
9
+ if @_inner_html
10
+ self.inner_html= @_inner_html
11
+ @_inner_html = nil
12
+ end
13
+ if cache_search
14
+ @_search ||= {}
15
+ @_search[expr] || @_search[expr] = _search(expr,&block)
16
+ else
17
+ _search(expr,&block)
18
+ end
19
+ end
20
+
21
+ def _inner_html=(html)
22
+ @_inner_html = html
23
+ end
24
+
25
+ alias :_output :output
26
+ def output(out, opts={})
27
+ if @_inner_html
28
+ if empty? and ElementContent[@stag.name] == :EMPTY
29
+ @stag.output(out, opts.merge(:style => :empty))
30
+ else
31
+ @stag.output(out, opts)
32
+ out << @_inner_html
33
+ if @etag
34
+ @etag.output(out, opts)
35
+ elsif !opts[:preserve]
36
+ ETag.new(@stag.name).output(out,opts)
37
+ end
38
+ end
39
+ else
40
+ _output(out,opts)
41
+ end
42
+ end
43
+
44
+ end
45
+ module Lilu
46
+
47
+ module Version ; MAJOR, MINOR, TINY = 0, 1, 0 ; end
48
+
49
+ class Action
50
+ attr_accessor :element
51
+ attr_reader :renderer
52
+ def initialize(element,renderer)
53
+ @element, @renderer = element, renderer
54
+ renderer.action = self
55
+ end
56
+ end
57
+
58
+
59
+ class Populate < Action
60
+ def for(method,data,&block)
61
+ return element.collect {|e| self.element = e ; renderer.instance_eval { action.for(method,data,&block) } } if element.is_a?(Hpricot::Elements)
62
+
63
+ element.cache_search = true
64
+ update_action = Update.new(element,renderer)
65
+ parent = element.parent
66
+ element_html = element.to_html
67
+ data.send(method) do |*objects|
68
+ update_action.element = element
69
+ update_action.with(block.call(*objects))
70
+
71
+ parent.insert_after(Hpricot.make(element.to_html),element)
72
+ element = Hpricot.make(element_html)
73
+ end
74
+ renderer.action = self
75
+
76
+ Hpricot::Elements[element].remove
77
+ end
78
+ end
79
+
80
+ class Remove < Action
81
+ def initialize(*args)
82
+ super(*args)
83
+ return element.remove if element.is_a?(Hpricot::Elements)
84
+ Hpricot::Elements[element].remove
85
+ end
86
+ end
87
+
88
+ class Replace < Action
89
+ def with(new_element=nil,&block)
90
+ return element.collect {|e| self.element = e ; renderer.instance_eval { action.with(new_element) } } if element.is_a?(Hpricot::Elements)
91
+ case new_element
92
+ when String
93
+ element.swap new_element
94
+ when Hpricot::Elem
95
+ Hpricot::Elements[new_element].remove
96
+ element.parent.insert_after(new_element,element)
97
+ Hpricot::Elements[element].remove
98
+ when Proc
99
+ with(new_element.call.to_s)
100
+ when nil
101
+ with(block.call) if block_given?
102
+ else
103
+ element.swap new_element.to_s
104
+ end
105
+ end
106
+ end
107
+
108
+ class Update < Action
109
+
110
+ def with(arg=nil,&block)
111
+ return element.collect {|e| self.element = e ; renderer.instance_eval { action.with(arg,&block) } } if element.is_a?(Hpricot::Elements)
112
+ case arg
113
+ when Hash
114
+ arg.each_pair do |path,value|
115
+ value = value.call if value.is_a?(Proc)
116
+ case path
117
+ when String
118
+ elem = element.at(path)
119
+ raise ElementNotFound.new("Element #{elem} not found") unless elem
120
+
121
+ saved_element = element
122
+ self.element = elem
123
+ res = with(value,&block)
124
+ self.element = saved_element
125
+ res
126
+ when Replacing
127
+ Replace.new(path.element,renderer).with value.to_s
128
+ when renderer
129
+ element._inner_html = value.to_s
130
+ else
131
+ element[path] = value.to_s
132
+ end
133
+ end
134
+ when Proc
135
+ with arg.call
136
+ when nil
137
+ with renderer.instance_eval(&block) if block_given?
138
+ else
139
+ element._inner_html = arg.to_s
140
+ end
141
+ end
142
+
143
+
144
+ end
145
+
146
+ class Use < Action
147
+ def initialize(*args)
148
+ super(*args)
149
+ raise ArgumentError.new("Use action can not accept :all parameter") if element.is_a?(Hpricot::Elements)
150
+ renderer.doc = element
151
+ end
152
+ end
153
+
154
+ # Experimental stuff
155
+ class Replacing
156
+ attr_reader :element
157
+ def initialize(renderer,element)
158
+ @renderer = renderer
159
+ case element
160
+ when String
161
+ @element = renderer.element_at(element)
162
+ when Hpricot::Elem, Hpricot::Elements
163
+ @element = element
164
+ end
165
+ end
166
+ end
167
+ #
168
+
169
+ class ElementNotFound < Exception
170
+ def initialize(element)
171
+ super("Element #{element} was not found")
172
+ end
173
+ end
174
+
175
+ class Renderer
176
+ attr_accessor :action, :doc
177
+ attr_reader :instructions, :html_source
178
+
179
+ def element
180
+ action.element
181
+ end
182
+
183
+ def initialize(instructions,html_source,local_assignments={})
184
+ @instructions = instructions
185
+ @html_source = html_source
186
+ @doc = Hpricot(@html_source)
187
+ @view = local_assignments["___view"] if local_assignments.is_a?(Hash)
188
+ inject_local_assignments(local_assignments)
189
+ end
190
+
191
+
192
+ def apply
193
+ eval(@instructions) do |*name|
194
+ name = name.first
195
+ name = 'layout' if name.nil?
196
+ instance_variable_get("@content_for_#{name}")
197
+ end
198
+ @doc.to_html
199
+ end
200
+
201
+ %w[update populate remove use replace].each {|method_name| module_eval <<-EOL
202
+ def #{method_name}(*path)
203
+ elem = find_elements(*path)
204
+ path.pop if path.first == :all
205
+ raise ElementNotFound.new(path) unless elem
206
+ Lilu::#{method_name.camelize}.new(elem,self)
207
+ end
208
+ EOL
209
+ }
210
+
211
+
212
+ def mapping(opts={})
213
+ opts
214
+ end
215
+
216
+ # Helper for partials
217
+ def partial(name,opts={})
218
+ render({:partial => name}.merge(opts))
219
+ end
220
+
221
+ # Helper for lambda
222
+ alias_method :L, :lambda
223
+
224
+ def element_at(path)
225
+ doc.at(path)
226
+ end
227
+
228
+ def method_missing(sym,*args)
229
+ return @view.send(sym,*args) if @view and @view.respond_to?(sym)
230
+ return instance_variable_get("@#{sym}") if args.empty? and instance_variables.member? "@#{sym}"
231
+ return @controller.send(sym, *args) if @controller and @controller.respond_to?(sym)
232
+ raise NoMethodError.new(sym.to_s)
233
+ end
234
+
235
+ # Helper for replacing
236
+ def replacing(element)
237
+ Replacing.new(self,element)
238
+ end
239
+
240
+ protected
241
+
242
+ def find_elements(*path)
243
+ path_first, path_second = path[0], path[1]
244
+ case path_first
245
+ when Hpricot::Elem, Hpricot::Elements
246
+ path_first
247
+ when :all
248
+ raise InvalidArgument.new("if :all is specified, second argument with path should be specified as well") unless path_second
249
+ doc.search(path_second)
250
+ else
251
+ doc.at(path_first)
252
+ end
253
+ end
254
+
255
+ def inject_local_assignments(local_assignments)
256
+ case local_assignments
257
+ when Hash
258
+ local_assignments.each_pair {|ivar,val| instance_variable_set(ivar.to_s.starts_with?('@') ? ivar : "@#{ivar}", val) }
259
+ when Binding
260
+ eval("instance_variables",local_assignments).each {|ivar| instance_variable_set(ivar, eval("instance_variable_get('#{ivar}')",local_assignments)) }
261
+ else
262
+ local_assignments.instance_variables.each {|ivar| instance_variable_set(ivar.to_s.starts_with?('@') ? ivar : "@#{ivar}", local_assignments.instance_variable_get(ivar)) }
263
+ end
264
+ end
265
+
266
+ end
267
+
268
+ end
@@ -0,0 +1,22 @@
1
+ module Lilu
2
+ module Camping
3
+ def self.for(app,path)
4
+ app.module_eval do
5
+ include Lilu::Camping
6
+ @@templates = path
7
+ end
8
+ end
9
+
10
+ def render(m,layout=true)
11
+ @content_for_layout = render_lilu(m)
12
+ render_lilu("layout") if layout
13
+ end
14
+
15
+ protected
16
+
17
+ def render_lilu(m)
18
+ Lilu::Renderer.new(IO.read("#{@@templates}/templates/#{m}.lilu"),IO.read("#{@@templates}/templates/#{m}.html"),binding).apply
19
+ end
20
+ end
21
+ end
22
+
@@ -0,0 +1,16 @@
1
+ require File.dirname(__FILE__) + '/lilu'
2
+ module Lilu
3
+ class View
4
+ def initialize(view)
5
+ @view = view
6
+ end
7
+ def render(template, local_assigns = {})
8
+ lilu_file_path = local_assigns[:lilu_file_path]
9
+ local_assigns.delete :lilu_file_path
10
+ @view.instance_eval do
11
+ local_assigns.merge!("content_for_layout" => @content_for_layout,"controller" => @controller,"___view" => self)
12
+ Lilu::Renderer.new(template,IO.read(lilu_file_path.gsub(/#{File.extname(lilu_file_path)}$/,'.html')),@assigns.merge(local_assigns)).apply
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,223 @@
1
+ require File.dirname(__FILE__) + '/../lib/lilu'
2
+ require 'ostruct'
3
+ describe "Newly created ", Lilu::Renderer do
4
+
5
+ before(:each) do
6
+ @instructions = "remove('html')"
7
+ @html_source = "<html></html>"
8
+ @renderer = Lilu::Renderer.new(@instructions,@html_source)
9
+ end
10
+
11
+ it "should load instructions" do
12
+ @renderer.instructions.should == @instructions
13
+ end
14
+
15
+ it "should load html source" do
16
+ @renderer.html_source.should == @html_source
17
+ end
18
+ end
19
+
20
+ describe "Newly created ", Lilu::Renderer, " with local assignments" do
21
+
22
+ before(:all) do
23
+ @test_variable = "Lilu"
24
+ end
25
+
26
+ { "binding" => lambda {|s| s.instance_eval "binding" },
27
+ "Hash with variable names without @ prefix" => { "test_variable" => "Lilu" },
28
+ "Hash with variable names with @ prefix" => { "@test_variable" => "Lilu" },
29
+ "Object with instance variables" => Object.new.instance_eval { @test_variable = "Lilu" ; self}
30
+ }.each_pair do |name, val|
31
+ it "should load instance variable from #{name}" do
32
+ @instructions = "remove('html')"
33
+ @html_source = "<html></html>"
34
+ val = val.call(self) if val.is_a?(Proc) # hack to support binding test
35
+ @renderer = Lilu::Renderer.new(@instructions,@html_source,val)
36
+ @renderer.instance_variable_get(:@test_variable).should == @test_variable
37
+ end
38
+ end
39
+ end
40
+
41
+ describe Lilu::Renderer do
42
+
43
+ %w[update populate remove use replace].each do |verb|
44
+ it "should raise an exception if element is not found when using #{verb} verb" do
45
+ @instructions = %{#{verb}("#some-missing-data") }
46
+ @html_source = %{<div id="some-data">Lola</div>}
47
+ @renderer = Lilu::Renderer.new(@instructions,@html_source)
48
+ lambda { @renderer.apply }.should raise_error(Lilu::ElementNotFound)
49
+ end
50
+ end
51
+
52
+ it "should remove element on remove(path) construct" do
53
+ @instructions = %{remove("#some-data") }
54
+ @html_source = %{<div id="some-data">Lola</div>}
55
+ @renderer = Lilu::Renderer.new(@instructions,@html_source)
56
+ Hpricot(@renderer.apply).at("#some-lilu-data").should be_nil
57
+ end
58
+
59
+ it "should remove all elements on remove(:all,path) construct" do
60
+ @instructions = %{remove(:all,".not-for-public") }
61
+ @html_source = %{<div class="not-for-public">Lola</div><br /><div class="not-for-public">1</div>}
62
+ @renderer = Lilu::Renderer.new(@instructions,@html_source)
63
+ Hpricot(@renderer.apply).to_s.should == "<br />"
64
+ end
65
+
66
+
67
+ it "should update element details on update(path).with(String) construct" do
68
+ @instructions = %{update("#some-data").with("Lilu")}
69
+ @html_source = %{<div id="some-data">Lola</div>}
70
+ @renderer = Lilu::Renderer.new(@instructions,@html_source)
71
+ Hpricot(@renderer.apply).at("#some-data").inner_html.should == "Lilu"
72
+ end
73
+
74
+ it "should update element details on update(path).with Hash construct" do
75
+ @instructions = %{update("#some-data").with :id => "some-lilu-data", "a" => { :href => "/", self => "is here" } }
76
+ @html_source = %{<div id="some-data">Lola <a href="#">is there</a></div>}
77
+ @renderer = Lilu::Renderer.new(@instructions,@html_source)
78
+ result = Hpricot(@renderer.apply)
79
+ result.at("#some-lilu-data").inner_html.should == "Lola <a href=\"/\">is here</a>"
80
+ end
81
+
82
+ it "should populate element details on populate(path).for(:each,@blogs) { block } construct" do
83
+ @blogs = [OpenStruct.new(:url => "http://railsware.com", :blog_id => 1, :name => "Railsware"),OpenStruct.new(:url => "http://railsware.com/", :blog_id => 2, :name => "Railsware!")]
84
+ @instructions = %{populate("#blog-example").for(:each,@blogs) {|blog| mapping 'a' => {:href => blog.url, self => blog.name}, :id => blog.blog_id } }
85
+ @html_source = %{<ul id="blogs"><li id="blog-example"><a href="#">My Blog</a></li></ul>}
86
+ @renderer = Lilu::Renderer.new(@instructions,@html_source,self)
87
+ result = Hpricot(@renderer.apply)
88
+ result.at("#blogs/#2/a")[:href].should == "http://railsware.com/"
89
+ result.at("#blogs/#1/a").inner_html.should == "Railsware"
90
+ result.at("#blogs/#blog-example").should be_nil
91
+ end
92
+
93
+ it "should populate element details on populate(:all,path).for(:each,@blogs) { block } construct" do
94
+ @blogs = [OpenStruct.new(:url => "http://railsware.com", :blog_id => 1, :name => "Railsware")]
95
+ @instructions = %{populate(:all,".blog-example").for(:each,@blogs) {|blog| mapping 'a' => {:href => blog.url, self => blog.name}, :id => blog.blog_id } }
96
+ @html_source = %{<ul id="blogs1"><li id="blog-example" class="blog-example"><a href="#">My Blog</a></li></ul><ul id="blogs2"><li id="blog-example" class="blog-example"><a href="#">My Blog</a></li></ul>}
97
+ @renderer = Lilu::Renderer.new(@instructions,@html_source,self)
98
+ result = Hpricot(@renderer.apply)
99
+ result.at("#blogs1/#1/a")[:href].should == "http://railsware.com"
100
+ result.at("#blogs1/#1/a").inner_html.should == "Railsware"
101
+ result.at("#blogs1/#blog-example").should be_nil
102
+ result.at("#blogs2/#1/a")[:href].should == "http://railsware.com"
103
+ result.at("#blogs2/#1/a").inner_html.should == "Railsware"
104
+ result.at("#blogs2/#blog-example").should be_nil
105
+
106
+ end
107
+
108
+ it "should update all elements, matched by path on update(:all, path).with Hash construct" do
109
+ @instructions = %{update(:all, "a").with :href => "#", self => 'Stay here'}
110
+ @html_source = %{<a href="http://java.net">C'mon, Java!</a><a href="http://www.php.net">Go away!</a>}
111
+ @renderer = Lilu::Renderer.new(@instructions,@html_source)
112
+ result = Hpricot(@renderer.apply)
113
+ (result/"a").each { |elem| elem.inner_html.should == 'Stay here'; elem[:href].should == '#' }
114
+ end
115
+
116
+ it "should update all elements, matched by path on update(:all, path).with Hash construct, taking in account each element content" do
117
+ @instructions = 'update(:all, "a").with :href => L{"#{element[:href]}/download"}, self => L{"Download #{element.inner_html}"}'
118
+ links = {"http://java.net" => "C'mon, Java!", "http://www.php.net" => "Go away!"}
119
+ @html_source = ""
120
+ links.each_pair {|url,name| @html_source << %{<a href="#{url}">#{name}</a>} }
121
+ @renderer = Lilu::Renderer.new(@instructions,@html_source)
122
+ result = Hpricot(@renderer.apply)
123
+ links.each_pair do |url,name|
124
+ result.at("//a[@href='#{url}/download']").inner_html.should == "Download #{name}"
125
+ end
126
+ end
127
+
128
+ it "should update all elements, matched by path on update(:all, path).with Block construct, taking in account each element content" do
129
+ @instructions = 'update(:all, "a").with { mapping :href => "#{element[:href]}/download", self => "Download #{element.inner_html }" }'
130
+ links = {"http://java.net" => "C'mon, Java!", "http://www.php.net" => "Go away!"}
131
+ @html_source = ""
132
+ links.each_pair {|url,name| @html_source << %{<a href="#{url}">#{name}</a>} }
133
+ @renderer = Lilu::Renderer.new(@instructions,@html_source)
134
+ result = Hpricot(@renderer.apply)
135
+ links.each_pair do |url,name|
136
+ result.at("//a[@href='#{url}/download']").inner_html.should == "Download #{name}"
137
+ end
138
+ end
139
+
140
+ it "should update all elements, matched by path on update(:all, path).with Lambda construct, taking in account each element content" do
141
+ @instructions = 'update(:all, "a").with L{ mapping :href => "#{element[:href]}/download", self => "Download #{element.inner_html }" }'
142
+ links = {"http://java.net" => "C'mon, Java!", "http://www.php.net" => "Go away!"}
143
+ @html_source = ""
144
+ links.each_pair {|url,name| @html_source << %{<a href="#{url}">#{name}</a>} }
145
+ @renderer = Lilu::Renderer.new(@instructions,@html_source)
146
+ result = Hpricot(@renderer.apply)
147
+ links.each_pair do |url,name|
148
+ result.at("//a[@href='#{url}/download']").inner_html.should == "Download #{name}"
149
+ end
150
+ end
151
+
152
+ it "should remove everything outside of specified element on use(path)" do
153
+ @instructions = "use('#main')"
154
+ @html_source = '<html><body><div id="main">Blablabla</div></body></html>'
155
+ @renderer = Lilu::Renderer.new(@instructions,@html_source)
156
+ result = Hpricot(@renderer.apply)
157
+ result.to_s.should == '<div id="main">Blablabla</div>'
158
+ end
159
+
160
+ it "should raise ArgumentError if use is called with :all argument" do
161
+ @instructions = "use(:all,'#main')"
162
+ @html_source = '<html><body><div id="main">Blablabla</div></body></html>'
163
+ @renderer = Lilu::Renderer.new(@instructions,@html_source)
164
+ lambda { Hpricot(@renderer.apply) }.should raise_error(ArgumentError)
165
+ end
166
+
167
+ it "should replace element with another using replace().with String construct" do
168
+ @instructions = "replace('#main').with 'Hello!'"
169
+ @html_source = '<html><body><div id="main">Blablabla</div></body></html>'
170
+ @renderer = Lilu::Renderer.new(@instructions,@html_source)
171
+ result = Hpricot(@renderer.apply)
172
+ result.to_s.should == '<html><body>Hello!</body></html>'
173
+ end
174
+
175
+ it "should replace element with another using replace().with Block construct" do
176
+ @instructions = "replace('#main').with { 'Hello!' }"
177
+ @html_source = '<html><body><div id="main">Blablabla</div></body></html>'
178
+ @renderer = Lilu::Renderer.new(@instructions,@html_source)
179
+ result = Hpricot(@renderer.apply)
180
+ result.to_s.should == '<html><body>Hello!</body></html>'
181
+ end
182
+
183
+ it "should replace element with another using replace().with Lambda construct" do
184
+ @instructions = "replace('#main').with L{ 'Hello!' }"
185
+ @html_source = '<html><body><div id="main">Blablabla</div></body></html>'
186
+ @renderer = Lilu::Renderer.new(@instructions,@html_source)
187
+ result = Hpricot(@renderer.apply)
188
+ result.to_s.should == '<html><body>Hello!</body></html>'
189
+ end
190
+
191
+ it "should replace all elements with another using replace(:all,).with String construct" do
192
+ @instructions = "replace(:all,'.main').with 'Hello!'"
193
+ @html_source = '<html><body><div class="main">Blablabla</div><div class="main">Blublublu</div></body></html>'
194
+ @renderer = Lilu::Renderer.new(@instructions,@html_source)
195
+ result = Hpricot(@renderer.apply)
196
+ result.to_s.should == '<html><body>Hello!Hello!</body></html>'
197
+ end
198
+
199
+ it "should replace all elements with another using replace(:all,).with String construct" do
200
+ @instructions = "replace(:all,'.main').with 'Hello!'"
201
+ @html_source = '<html><body><div class="main">Blablabla</div><div class="main">Blublublu</div></body></html>'
202
+ @renderer = Lilu::Renderer.new(@instructions,@html_source)
203
+ result = Hpricot(@renderer.apply)
204
+ result.to_s.should == '<html><body>Hello!Hello!</body></html>'
205
+ end
206
+
207
+ it "should replace element with another using replace().with Element construct" do
208
+ @instructions = "replace('#main').with element_at('#helper')"
209
+ @html_source = '<html><body><div id="main">Blablabla</div><div id="helper">Lilu</div></body></html>'
210
+ @renderer = Lilu::Renderer.new(@instructions,@html_source)
211
+ result = Hpricot(@renderer.apply)
212
+ result.to_s.should == '<html><body><div id="helper">Lilu</div></body></html>'
213
+ end
214
+
215
+ it "should replace all elements with another using replace(:all,).with Element construct" do
216
+ @instructions = "replace(:all,'.main').with element_at('#helper')"
217
+ @html_source = '<html><body><div class="main">Blablabla</div><div class="main">Blublublua</div><div id="helper">Lilu</div></body></html>'
218
+ @renderer = Lilu::Renderer.new(@instructions,@html_source)
219
+ result = Hpricot(@renderer.apply)
220
+ result.to_s.should == '<html><body><div id="helper">Lilu</div></body></html>'
221
+ end
222
+
223
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.2
3
+ specification_version: 1
4
+ name: lilu
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.0
7
+ date: 2007-05-16 00:00:00 +03:00
8
+ summary: View subsystem that allows to keep pure HTML for views
9
+ require_paths:
10
+ - lib
11
+ email: yrashk@verbdev.com
12
+ homepage:
13
+ rubyforge_project:
14
+ description: View subsystem that allows to keep pure HTML for views
15
+ autorequire:
16
+ - rake
17
+ - hpricot
18
+ - lib/lilu
19
+ - lib/lilu_view
20
+ - lib/lilu_camping
21
+ default_executable: fixturease.rb
22
+ bindir: bin
23
+ has_rdoc: false
24
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
25
+ requirements:
26
+ - - ">"
27
+ - !ruby/object:Gem::Version
28
+ version: 0.0.0
29
+ version:
30
+ platform: ruby
31
+ signing_key:
32
+ cert_chain:
33
+ post_install_message:
34
+ authors:
35
+ - Yurii Rashkovskii
36
+ - Michael Holub
37
+ files:
38
+ - lib/lilu.rb
39
+ - lib/lilu_view.rb
40
+ - lib/lilu_camping.rb
41
+ - spec/lilu_spec.rb
42
+ test_files: []
43
+
44
+ rdoc_options: []
45
+
46
+ extra_rdoc_files: []
47
+
48
+ executables: []
49
+
50
+ extensions: []
51
+
52
+ requirements:
53
+ - hprico
54
+ dependencies: []
55
+