roda 2.0.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +26 -0
- data/README.rdoc +83 -22
- data/Rakefile +1 -1
- data/doc/release_notes/2.1.0.txt +124 -0
- data/lib/roda/plugins/assets.rb +17 -9
- data/lib/roda/plugins/class_level_routing.rb +5 -2
- data/lib/roda/plugins/delegate.rb +6 -3
- data/lib/roda/plugins/indifferent_params.rb +7 -0
- data/lib/roda/plugins/mailer.rb +18 -1
- data/lib/roda/plugins/multi_route.rb +2 -1
- data/lib/roda/plugins/path.rb +75 -6
- data/lib/roda/plugins/render.rb +33 -14
- data/lib/roda/plugins/static.rb +35 -0
- data/lib/roda/plugins/view_options.rb +161 -0
- data/lib/roda/plugins/view_subdirs.rb +6 -63
- data/lib/roda/version.rb +1 -1
- data/spec/composition_spec.rb +12 -0
- data/spec/matchers_spec.rb +34 -0
- data/spec/plugin/assets_spec.rb +112 -17
- data/spec/plugin/delete_empty_headers_spec.rb +12 -0
- data/spec/plugin/mailer_spec.rb +46 -3
- data/spec/plugin/module_include_spec.rb +17 -0
- data/spec/plugin/multi_route_spec.rb +10 -0
- data/spec/plugin/named_templates_spec.rb +6 -0
- data/spec/plugin/not_found_spec.rb +1 -1
- data/spec/plugin/path_spec.rb +76 -0
- data/spec/plugin/render_each_spec.rb +6 -0
- data/spec/plugin/render_spec.rb +40 -1
- data/spec/plugin/sinatra_helpers_spec.rb +5 -0
- data/spec/plugin/static_spec.rb +30 -0
- data/spec/plugin/view_options_spec.rb +117 -0
- data/spec/spec_helper.rb +5 -1
- data/spec/views/multiple-layout.erb +1 -0
- data/spec/views/multiple.erb +1 -0
- metadata +10 -4
- data/spec/plugin/static_path_info_spec.rb +0 -56
- data/spec/plugin/view_subdirs_spec.rb +0 -44
@@ -1,5 +1,10 @@
|
|
1
1
|
require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
|
2
2
|
|
3
|
+
begin
|
4
|
+
require 'tilt/erb'
|
5
|
+
rescue LoadError
|
6
|
+
warn "tilt not installed, skipping named_templates plugin test"
|
7
|
+
else
|
3
8
|
describe "named_templates plugin" do
|
4
9
|
it "adds template method method for naming templates, and have render recognize it" do
|
5
10
|
app(:bare) do
|
@@ -88,3 +93,4 @@ describe "named_templates plugin" do
|
|
88
93
|
body('/bar').should == 'bar43-barfoo42-baz'
|
89
94
|
end
|
90
95
|
end
|
96
|
+
end
|
data/spec/plugin/path_spec.rb
CHANGED
@@ -9,6 +9,15 @@ describe "path plugin" do
|
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
|
+
def path_script_name_app(*args, &block)
|
13
|
+
app(:bare) do
|
14
|
+
opts[:add_script_name] = true
|
15
|
+
plugin :path
|
16
|
+
path *args, &block
|
17
|
+
route{|r| send(r.path_info)}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
12
21
|
def path_block_app(b, *args, &block)
|
13
22
|
path_app(*args, &block)
|
14
23
|
app.route{|r| send(r.path_info, &b)}
|
@@ -58,6 +67,16 @@ describe "path plugin" do
|
|
58
67
|
body("foo_path", 'SCRIPT_NAME'=>'/baz').should == "/baz/bar/foo"
|
59
68
|
end
|
60
69
|
|
70
|
+
it "respects :add_script_name app option for automatically adding the script name" do
|
71
|
+
path_script_name_app(:foo){"/bar/foo"}
|
72
|
+
body("foo_path", 'SCRIPT_NAME'=>'/baz').should == "/baz/bar/foo"
|
73
|
+
end
|
74
|
+
|
75
|
+
it "supports :add_script_name=>false option for not automatically adding the script name" do
|
76
|
+
path_script_name_app(:foo, :add_script_name=>false){"/bar/foo"}
|
77
|
+
body("foo_path", 'SCRIPT_NAME'=>'/baz').should == "/bar/foo"
|
78
|
+
end
|
79
|
+
|
61
80
|
it "supports path method accepting a block when using :add_script_name" do
|
62
81
|
path_block_app(lambda{"c"}, :foo, :add_script_name=>true){|&block| "/bar/foo/#{block.call}"}
|
63
82
|
body("foo_path", 'SCRIPT_NAME'=>'/baz').should == "/baz/bar/foo/c"
|
@@ -91,3 +110,60 @@ describe "path plugin" do
|
|
91
110
|
body("foo_url", 'HTTP_HOST'=>'example.org', "rack.url_scheme"=>'http', 'SERVER_PORT'=>81).should == "http://example.org:81/bar/foo"
|
92
111
|
end
|
93
112
|
end
|
113
|
+
|
114
|
+
describe "path plugin" do
|
115
|
+
before do
|
116
|
+
app(:bare) do
|
117
|
+
plugin :path
|
118
|
+
route{|r| path(*env['path'])}
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
c = Class.new{attr_accessor :a}
|
123
|
+
app.path(c){|obj, *args| "/d/#{obj.a}/#{File.join(*args)}"}
|
124
|
+
@obj = c.new
|
125
|
+
@obj.a = 1
|
126
|
+
end
|
127
|
+
|
128
|
+
it "Roda#path respects classes and symbols registered via Roda.path" do
|
129
|
+
# Strings
|
130
|
+
body('path'=>'/foo/bar').should == '/foo/bar'
|
131
|
+
|
132
|
+
# Classes
|
133
|
+
body('path'=>@obj).should == '/d/1/'
|
134
|
+
body('path'=>[@obj, 'foo']).should == '/d/1/foo'
|
135
|
+
body('path'=>[@obj, 'foo', 'bar']).should == '/d/1/foo/bar'
|
136
|
+
end
|
137
|
+
|
138
|
+
it "Roda#path raises an error for an unrecognized class" do
|
139
|
+
# Strings
|
140
|
+
proc{body('path'=>:foo)}.should raise_error(Roda::RodaError)
|
141
|
+
end
|
142
|
+
|
143
|
+
it "Roda#path respects :add_script_name app option" do
|
144
|
+
app.opts[:add_script_name] = true
|
145
|
+
|
146
|
+
# Strings
|
147
|
+
body('path'=>'/foo/bar', 'SCRIPT_NAME'=>'/baz').should == '/baz/foo/bar'
|
148
|
+
|
149
|
+
# Classes
|
150
|
+
body('path'=>@obj, 'SCRIPT_NAME'=>'/baz').should == '/baz/d/1/'
|
151
|
+
body('path'=>[@obj, 'foo'], 'SCRIPT_NAME'=>'/baz').should == '/baz/d/1/foo'
|
152
|
+
body('path'=>[@obj, 'foo', 'bar'], 'SCRIPT_NAME'=>'/baz').should == '/baz/d/1/foo/bar'
|
153
|
+
end
|
154
|
+
|
155
|
+
it "Roda.path doesn't work with classes without blocks" do
|
156
|
+
proc{app.path(Class.new)}.should raise_error(Roda::RodaError)
|
157
|
+
end
|
158
|
+
|
159
|
+
it "Roda.path doesn't work with classes with paths or options" do
|
160
|
+
proc{app.path(Class.new, '/a'){}}.should raise_error(Roda::RodaError)
|
161
|
+
proc{app.path(Class.new, nil, :a=>1){}}.should raise_error(Roda::RodaError)
|
162
|
+
end
|
163
|
+
|
164
|
+
it "Roda.path doesn't work after freezing the app" do
|
165
|
+
app.freeze
|
166
|
+
proc{app.path(Class.new){|obj| ''}}.should raise_error
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
@@ -1,5 +1,10 @@
|
|
1
1
|
require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
|
2
2
|
|
3
|
+
begin
|
4
|
+
require 'tilt/erb'
|
5
|
+
rescue LoadError
|
6
|
+
warn "tilt not installed, skipping render_each plugin test"
|
7
|
+
else
|
3
8
|
describe "render_each plugin" do
|
4
9
|
it "calls render with each argument, returning joined string with all results" do
|
5
10
|
app(:bare) do
|
@@ -33,3 +38,4 @@ describe "render_each plugin" do
|
|
33
38
|
body("/c").should == 'rbar41 rbar42 rbar43 '
|
34
39
|
end
|
35
40
|
end
|
41
|
+
end
|
data/spec/plugin/render_spec.rb
CHANGED
@@ -107,6 +107,18 @@ describe "render plugin" do
|
|
107
107
|
body.strip.should == "<title>Alternative Layout: Home</title>\n<h1>Home</h1>\n<p>Hello Agent Smith</p>"
|
108
108
|
end
|
109
109
|
|
110
|
+
it "locals overrides" do
|
111
|
+
app(:bare) do
|
112
|
+
plugin :render, :views=>"./spec/views", :locals=>{:title=>'Home', :b=>'B'}, :layout_opts=>{:template=>'multiple-layout', :locals=>{:title=>'Roda', :a=>'A'}}
|
113
|
+
|
114
|
+
route do |r|
|
115
|
+
view("multiple", :locals=>{:b=>"BB"}, :layout_opts=>{:locals=>{:a=>'AA'}})
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
body.strip.should == "Roda:AA::Home:BB"
|
120
|
+
end
|
121
|
+
|
110
122
|
it ":layout=>true/false/string/hash/not-present respects plugin layout switch and template" do
|
111
123
|
app(:bare) do
|
112
124
|
plugin :render, :views=>"./spec/views", :layout_opts=>{:template=>'layout-yield', :locals=>{:title=>'a'}}
|
@@ -149,13 +161,20 @@ describe "render plugin" do
|
|
149
161
|
body('/h').gsub("\n", '').should == "<title>Roda: a</title>bar"
|
150
162
|
|
151
163
|
app.plugin :render, :layout=>nil
|
152
|
-
body.gsub("\n", '').should == "
|
164
|
+
body.gsub("\n", '').should == "HeaderbarFooter"
|
153
165
|
body('/a').gsub("\n", '').should == "bar"
|
154
166
|
body('/f').gsub("\n", '').should == "bar"
|
155
167
|
body('/s').gsub("\n", '').should == "<title>Roda: a</title>bar"
|
156
168
|
body('/h').gsub("\n", '').should == "<title>Roda: a</title>bar"
|
157
169
|
|
158
170
|
app.plugin :render, :layout=>false
|
171
|
+
body.gsub("\n", '').should == "HeaderbarFooter"
|
172
|
+
body('/a').gsub("\n", '').should == "bar"
|
173
|
+
body('/f').gsub("\n", '').should == "bar"
|
174
|
+
body('/s').gsub("\n", '').should == "<title>Roda: a</title>bar"
|
175
|
+
body('/h').gsub("\n", '').should == "<title>Roda: a</title>bar"
|
176
|
+
|
177
|
+
app.plugin :render, :layout_opts=>{:template=>'layout-alternative', :locals=>{:title=>'a'}}
|
159
178
|
body.gsub("\n", '').should == "<title>Alternative Layout: a</title>bar"
|
160
179
|
body('/a').gsub("\n", '').should == "bar"
|
161
180
|
body('/f').gsub("\n", '').should == "bar"
|
@@ -163,6 +182,26 @@ describe "render plugin" do
|
|
163
182
|
body('/h').gsub("\n", '').should == "<title>Roda: a</title>bar"
|
164
183
|
end
|
165
184
|
|
185
|
+
it "app :root option affects :views default" do
|
186
|
+
app
|
187
|
+
app.plugin :render
|
188
|
+
app.render_opts[:views].should == File.join(Dir.pwd, 'views')
|
189
|
+
|
190
|
+
app.opts[:root] = '/foo'
|
191
|
+
app.plugin :render
|
192
|
+
app.render_opts[:views].should == '/foo/views'
|
193
|
+
|
194
|
+
app.opts[:root] = '/foo/bar'
|
195
|
+
app.plugin :render
|
196
|
+
app.render_opts[:views].should == '/foo/bar/views'
|
197
|
+
|
198
|
+
app.opts[:root] = nil
|
199
|
+
app.plugin :render
|
200
|
+
app.render_opts[:views].should == File.join(Dir.pwd, 'views')
|
201
|
+
app.plugin :render, :views=>'bar'
|
202
|
+
app.render_opts[:views].should == File.join(Dir.pwd, 'bar')
|
203
|
+
end
|
204
|
+
|
166
205
|
it "inline layouts and inline views" do
|
167
206
|
app(:render) do
|
168
207
|
view({:inline=>'bar'}, :layout=>{:inline=>'Foo: <%= yield %>'})
|
@@ -83,6 +83,11 @@ describe "sinatra_helpers plugin" do
|
|
83
83
|
header('Content-Length').should == '11'
|
84
84
|
end
|
85
85
|
|
86
|
+
it 'supports #join' do
|
87
|
+
sin_app{body{'Hello World'}; nil}
|
88
|
+
req[2].join.should == 'Hello World'
|
89
|
+
end
|
90
|
+
|
86
91
|
it 'takes a String, Array, or other object responding to #each' do
|
87
92
|
sin_app{body 'Hello World'; nil}
|
88
93
|
body.should == 'Hello World'
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
describe "static plugin" do
|
4
|
+
it "adds support for serving static files" do
|
5
|
+
app(:bare) do
|
6
|
+
plugin :static, ['/about'], :root=>'spec/views'
|
7
|
+
|
8
|
+
route do
|
9
|
+
'a'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
body.should == 'a'
|
14
|
+
body('/about/_test.erb').should == File.read('spec/views/about/_test.erb')
|
15
|
+
end
|
16
|
+
|
17
|
+
it "respects the application's :root option" do
|
18
|
+
app(:bare) do
|
19
|
+
opts[:root] = File.expand_path('../../', __FILE__)
|
20
|
+
plugin :static, ['/about'], :root=>'views'
|
21
|
+
|
22
|
+
route do
|
23
|
+
'a'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
body.should == 'a'
|
28
|
+
body('/about/_test.erb').should == File.read('spec/views/about/_test.erb')
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'tilt/erb'
|
5
|
+
rescue LoadError
|
6
|
+
warn "tilt not installed, skipping view_options plugin test"
|
7
|
+
else
|
8
|
+
describe "view_options plugin view subdirs" do
|
9
|
+
before do
|
10
|
+
app(:bare) do
|
11
|
+
plugin :render, :views=>"."
|
12
|
+
plugin :view_options
|
13
|
+
|
14
|
+
route do |r|
|
15
|
+
append_view_subdir 'spec'
|
16
|
+
|
17
|
+
r.on "home" do
|
18
|
+
set_view_subdir 'spec/views'
|
19
|
+
view("home", :locals=>{:name => "Agent Smith", :title => "Home"}, :layout_opts=>{:locals=>{:title=>"Home"}})
|
20
|
+
end
|
21
|
+
|
22
|
+
r.on "about" do
|
23
|
+
append_view_subdir 'views'
|
24
|
+
render("about", :locals=>{:title => "About Roda"})
|
25
|
+
end
|
26
|
+
|
27
|
+
r.on "path" do
|
28
|
+
render('spec/views/about', :locals=>{:title => "Path"}, :layout_opts=>{:locals=>{:title=>"Home"}})
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should use set subdir if template name does not contain a slash" do
|
35
|
+
body("/home").strip.should == "<title>Roda: Home</title>\n<h1>Home</h1>\n<p>Hello Agent Smith</p>"
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should not use set subdir if template name contains a slash" do
|
39
|
+
body("/about").strip.should == "<h1>About Roda</h1>"
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should not change behavior when subdir is not set" do
|
43
|
+
body("/path").strip.should == "<h1>Path</h1>"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "view_options plugin" do
|
48
|
+
it "should set view and layout options and locals to use" do
|
49
|
+
app(:view_options) do
|
50
|
+
set_view_options :views=>'spec/views'
|
51
|
+
set_view_locals :title=>'About Roda'
|
52
|
+
set_layout_options :views=>'spec/views', :template=>'layout-alternative'
|
53
|
+
set_layout_locals :title=>'Home'
|
54
|
+
view('about')
|
55
|
+
end
|
56
|
+
|
57
|
+
body.strip.should == "<title>Alternative Layout: Home</title>\n<h1>About Roda</h1>"
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should merge multiple calls to set view and layout options and locals" do
|
61
|
+
app(:view_options) do
|
62
|
+
set_layout_options :views=>'spec/views', :template=>'multiple-layout', :ext=>'str'
|
63
|
+
set_view_options :views=>'spec/views', :ext=>'str'
|
64
|
+
set_layout_locals :title=>'About Roda'
|
65
|
+
set_view_locals :title=>'Home'
|
66
|
+
|
67
|
+
set_layout_options :ext=>'erb'
|
68
|
+
set_view_options :ext=>'erb'
|
69
|
+
set_layout_locals :a=>'A'
|
70
|
+
set_view_locals :b=>'B'
|
71
|
+
|
72
|
+
view('multiple')
|
73
|
+
end
|
74
|
+
|
75
|
+
body.strip.should == "About Roda:A::Home:B"
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should have set_view_locals have more precedence than plugin options, but less than view/render method options" do
|
79
|
+
app(:bare) do
|
80
|
+
plugin :render, :views=>"./spec/views", :locals=>{:title=>'Home', :b=>'B'}, :layout_opts=>{:template=>'multiple-layout', :locals=>{:title=>'About Roda', :a=>'A'}}
|
81
|
+
plugin :view_options
|
82
|
+
|
83
|
+
route do |r|
|
84
|
+
r.is 'c' do
|
85
|
+
view(:multiple)
|
86
|
+
end
|
87
|
+
|
88
|
+
set_view_locals :b=>'BB'
|
89
|
+
set_layout_locals :a=>'AA'
|
90
|
+
|
91
|
+
r.on 'b' do
|
92
|
+
set_view_locals :title=>'About'
|
93
|
+
set_layout_locals :title=>'Roda'
|
94
|
+
|
95
|
+
r.is 'a' do
|
96
|
+
view(:multiple)
|
97
|
+
end
|
98
|
+
|
99
|
+
view("multiple", :locals=>{:b => "BBB"}, :layout_opts=>{:locals=>{:a=>'AAA'}})
|
100
|
+
end
|
101
|
+
|
102
|
+
r.is 'a' do
|
103
|
+
view(:multiple)
|
104
|
+
end
|
105
|
+
|
106
|
+
view("multiple", :locals=>{:b => "BBB"}, :layout_opts=>{:locals=>{:a=>'AAA'}})
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
body('/c').strip.should == "About Roda:A::Home:B"
|
111
|
+
body('/b/a').strip.should == "Roda:AA::About:BB"
|
112
|
+
body('/b').strip.should == "Roda:AAA::About:BBB"
|
113
|
+
body('/a').strip.should == "About Roda:AA::Home:BB"
|
114
|
+
body.strip.should == "About Roda:AAA::Home:BBB"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
<%= title %>:<%= a %>::<%= yield %>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= title %>:<%= b %>
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: roda
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Evans
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-03-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -137,6 +137,7 @@ extra_rdoc_files:
|
|
137
137
|
- doc/release_notes/1.2.0.txt
|
138
138
|
- doc/release_notes/1.3.0.txt
|
139
139
|
- doc/release_notes/2.0.0.txt
|
140
|
+
- doc/release_notes/2.1.0.txt
|
140
141
|
files:
|
141
142
|
- CHANGELOG
|
142
143
|
- MIT-LICENSE
|
@@ -148,6 +149,7 @@ files:
|
|
148
149
|
- doc/release_notes/1.2.0.txt
|
149
150
|
- doc/release_notes/1.3.0.txt
|
150
151
|
- doc/release_notes/2.0.0.txt
|
152
|
+
- doc/release_notes/2.1.0.txt
|
151
153
|
- lib/roda.rb
|
152
154
|
- lib/roda/plugins/_erubis_escaping.rb
|
153
155
|
- lib/roda/plugins/all_verbs.rb
|
@@ -196,10 +198,12 @@ files:
|
|
196
198
|
- lib/roda/plugins/render_each.rb
|
197
199
|
- lib/roda/plugins/sinatra_helpers.rb
|
198
200
|
- lib/roda/plugins/slash_path_empty.rb
|
201
|
+
- lib/roda/plugins/static.rb
|
199
202
|
- lib/roda/plugins/static_path_info.rb
|
200
203
|
- lib/roda/plugins/streaming.rb
|
201
204
|
- lib/roda/plugins/symbol_matchers.rb
|
202
205
|
- lib/roda/plugins/symbol_views.rb
|
206
|
+
- lib/roda/plugins/view_options.rb
|
203
207
|
- lib/roda/plugins/view_subdirs.rb
|
204
208
|
- lib/roda/version.rb
|
205
209
|
- spec/assets/css/app.scss
|
@@ -259,11 +263,11 @@ files:
|
|
259
263
|
- spec/plugin/render_spec.rb
|
260
264
|
- spec/plugin/sinatra_helpers_spec.rb
|
261
265
|
- spec/plugin/slash_path_empty_spec.rb
|
262
|
-
- spec/plugin/
|
266
|
+
- spec/plugin/static_spec.rb
|
263
267
|
- spec/plugin/streaming_spec.rb
|
264
268
|
- spec/plugin/symbol_matchers_spec.rb
|
265
269
|
- spec/plugin/symbol_views_spec.rb
|
266
|
-
- spec/plugin/
|
270
|
+
- spec/plugin/view_options_spec.rb
|
267
271
|
- spec/plugin_spec.rb
|
268
272
|
- spec/redirect_spec.rb
|
269
273
|
- spec/request_spec.rb
|
@@ -281,6 +285,8 @@ files:
|
|
281
285
|
- spec/views/layout-yield.erb
|
282
286
|
- spec/views/layout.erb
|
283
287
|
- spec/views/layout.str
|
288
|
+
- spec/views/multiple-layout.erb
|
289
|
+
- spec/views/multiple.erb
|
284
290
|
homepage: http://roda.jeremyevans.net
|
285
291
|
licenses:
|
286
292
|
- MIT
|