formular 0.2.1 → 0.2.2
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/.travis.yml +5 -4
- data/CHANGELOG.md +29 -2
- data/README.md +5 -4
- data/formular.gemspec +1 -1
- data/lib/formular/attributes.rb +10 -21
- data/lib/formular/builders/basic.rb +4 -3
- data/lib/formular/builders/bootstrap3.rb +2 -1
- data/lib/formular/builders/bootstrap4.rb +4 -4
- data/lib/formular/element.rb +54 -23
- data/lib/formular/element/bootstrap3.rb +40 -8
- data/lib/formular/element/bootstrap3/checkable_control.rb +5 -8
- data/lib/formular/element/bootstrap3/horizontal.rb +7 -7
- data/lib/formular/element/bootstrap3/input_group.rb +2 -2
- data/lib/formular/element/bootstrap4.rb +17 -8
- data/lib/formular/element/bootstrap4/checkable_control.rb +5 -4
- data/lib/formular/element/bootstrap4/custom_control.rb +8 -4
- data/lib/formular/element/bootstrap4/horizontal.rb +3 -3
- data/lib/formular/element/bootstrap4/input_group.rb +12 -0
- data/lib/formular/element/foundation6.rb +5 -5
- data/lib/formular/element/foundation6/checkable_control.rb +2 -4
- data/lib/formular/element/foundation6/input_group.rb +2 -2
- data/lib/formular/element/foundation6/{wrapped_control.rb → wrapped.rb} +4 -4
- data/lib/formular/element/modules/checkable.rb +10 -11
- data/lib/formular/element/modules/control.rb +12 -4
- data/lib/formular/element/modules/error.rb +6 -1
- data/lib/formular/element/modules/escape_value.rb +14 -0
- data/lib/formular/element/modules/hint.rb +5 -3
- data/lib/formular/element/modules/label.rb +5 -2
- data/lib/formular/element/modules/{wrapped_control.rb → wrapped.rb} +14 -13
- data/lib/formular/elements.rb +62 -19
- data/lib/formular/helper.rb +18 -4
- data/lib/formular/html_block.rb +1 -1
- data/lib/formular/html_escape.rb +19 -0
- data/lib/formular/path.rb +1 -6
- data/lib/formular/version.rb +1 -1
- metadata +16 -7
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'formular/element/module'
|
2
|
+
require 'formular/html_escape'
|
3
|
+
module Formular
|
4
|
+
class Element
|
5
|
+
module Modules
|
6
|
+
# include this module in an element to automatically escape the html of the value attribute
|
7
|
+
module EscapeValue
|
8
|
+
include Formular::Element::Module
|
9
|
+
include HtmlEscape
|
10
|
+
process_option :value, :html_escape
|
11
|
+
end # module EscapeValue
|
12
|
+
end # module Modules
|
13
|
+
end # class Element
|
14
|
+
end # module Formular
|
@@ -1,17 +1,19 @@
|
|
1
1
|
require 'formular/element/module'
|
2
|
+
require 'formular/html_escape'
|
2
3
|
module Formular
|
3
4
|
class Element
|
4
5
|
module Modules
|
5
6
|
# this module provides hints to a control when included.
|
6
7
|
module Hint
|
7
8
|
include Formular::Element::Module
|
9
|
+
include HtmlEscape
|
8
10
|
add_option_keys :hint, :hint_options
|
9
11
|
|
10
12
|
# options functionality (same as SimpleForm):
|
11
13
|
# options[:hint] == String return the string
|
12
14
|
module InstanceMethods
|
13
15
|
def hint_text
|
14
|
-
options[:hint] if has_hint?
|
16
|
+
html_escape(options[:hint]) if has_hint?
|
15
17
|
end
|
16
18
|
|
17
19
|
def has_hint?
|
@@ -22,12 +24,12 @@ module Formular
|
|
22
24
|
def hint_id
|
23
25
|
return hint_options[:id] if hint_options[:id]
|
24
26
|
|
25
|
-
id =
|
27
|
+
id = options[:id] || form_encoded_id
|
26
28
|
"#{id}_hint" if id
|
27
29
|
end
|
28
30
|
|
29
31
|
def hint_options
|
30
|
-
@hint_options ||=
|
32
|
+
@hint_options ||= options[:hint_options] || {}
|
31
33
|
end
|
32
34
|
end # module InstanceMethods
|
33
35
|
end # module Hint
|
@@ -1,10 +1,12 @@
|
|
1
1
|
require 'formular/element/module'
|
2
|
+
require 'formular/html_escape'
|
2
3
|
module Formular
|
3
4
|
class Element
|
4
5
|
module Modules
|
5
6
|
# this module provides label options and methods to a control when included.
|
6
7
|
module Label
|
7
8
|
include Formular::Element::Module
|
9
|
+
include HtmlEscape
|
8
10
|
add_option_keys :label, :label_options
|
9
11
|
|
10
12
|
# options functionality:
|
@@ -13,7 +15,8 @@ module Formular
|
|
13
15
|
# label as an option, you wont get one rendered
|
14
16
|
module InstanceMethods
|
15
17
|
def label_text
|
16
|
-
options[:label]
|
18
|
+
return if options[:label].nil? || options[:label] == false
|
19
|
+
html_escape(options[:label])
|
17
20
|
end
|
18
21
|
|
19
22
|
def has_label?
|
@@ -21,7 +24,7 @@ module Formular
|
|
21
24
|
end
|
22
25
|
|
23
26
|
def label_options
|
24
|
-
@label_options ||=
|
27
|
+
@label_options ||= options[:label_options] || {}
|
25
28
|
end
|
26
29
|
end # module InstanceMethods
|
27
30
|
end # module Label
|
@@ -8,9 +8,9 @@ module Formular
|
|
8
8
|
module Modules
|
9
9
|
# include this module to enable an element to render the entire wrapped input
|
10
10
|
# e.g. wrapper{label+control+hint+error}
|
11
|
-
module
|
11
|
+
module Wrapped
|
12
12
|
include Formular::Element::Module
|
13
|
-
include Control
|
13
|
+
# include Control
|
14
14
|
include Hint
|
15
15
|
include Error
|
16
16
|
include Label
|
@@ -32,15 +32,16 @@ module Formular
|
|
32
32
|
module InstanceMethods
|
33
33
|
def wrapper(&block)
|
34
34
|
wrapper_element = has_errors? ? :error_wrapper : :wrapper
|
35
|
-
builder.send(wrapper_element,
|
35
|
+
builder.send(wrapper_element, wrapper_options, &block)
|
36
36
|
end
|
37
37
|
|
38
38
|
def label
|
39
39
|
return '' unless has_label?
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
|
41
|
+
label_opts = label_options.dup
|
42
|
+
label_opts[:content] = label_text
|
43
|
+
label_opts[:labeled_control] = self
|
44
|
+
builder.label(label_opts).to_s
|
44
45
|
end
|
45
46
|
|
46
47
|
def error
|
@@ -52,22 +53,22 @@ module Formular
|
|
52
53
|
|
53
54
|
def hint
|
54
55
|
return '' unless has_hint?
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
builder.hint(
|
56
|
+
hint_opts = hint_options.dup
|
57
|
+
hint_opts[:content] = hint_text
|
58
|
+
hint_opts[:id] = hint_id # FIXME: this should work like a standard set_default
|
59
|
+
builder.hint(hint_opts).to_s
|
59
60
|
end
|
60
61
|
|
61
62
|
private
|
62
63
|
def error_options
|
63
|
-
@error_options ||=
|
64
|
+
@error_options ||= options[:error_options] || {}
|
64
65
|
end
|
65
66
|
|
66
67
|
def wrapper_options
|
67
|
-
@wrapper_options ||=
|
68
|
+
@wrapper_options ||= options[:wrapper_options] || {}
|
68
69
|
end
|
69
70
|
end # module InstanceMethods
|
70
|
-
end # module
|
71
|
+
end # module Wrapped
|
71
72
|
end # module Modules
|
72
73
|
end # class Element
|
73
74
|
end # module Formular
|
data/lib/formular/elements.rb
CHANGED
@@ -1,20 +1,21 @@
|
|
1
1
|
require 'formular/element'
|
2
2
|
require 'formular/element/module'
|
3
3
|
require 'formular/element/modules/container'
|
4
|
-
require 'formular/element/modules/
|
4
|
+
require 'formular/element/modules/wrapped'
|
5
5
|
require 'formular/element/modules/control'
|
6
6
|
require 'formular/element/modules/checkable'
|
7
7
|
require 'formular/element/modules/error'
|
8
|
+
require 'formular/element/modules/escape_value'
|
9
|
+
require 'formular/html_escape'
|
8
10
|
|
9
11
|
module Formular
|
10
12
|
class Element
|
11
13
|
# These three are really just provided for convenience when creating other elements
|
12
14
|
Container = Class.new(Formular::Element) { include Formular::Element::Modules::Container }
|
13
15
|
Control = Class.new(Formular::Element) { include Formular::Element::Modules::Control }
|
14
|
-
|
16
|
+
Wrapped = Class.new(Formular::Element) { include Formular::Element::Modules::Wrapped }
|
15
17
|
|
16
18
|
# define some base classes to build from or easily use elsewhere
|
17
|
-
Option = Class.new(Container) { tag :option }
|
18
19
|
OptGroup = Class.new(Container) { tag :optgroup }
|
19
20
|
Fieldset = Class.new(Container) { tag :fieldset }
|
20
21
|
Legend = Class.new(Container) { tag :legend }
|
@@ -23,6 +24,12 @@ module Formular
|
|
23
24
|
Span = Class.new(Container) { tag :span }
|
24
25
|
Small = Class.new(Container) { tag :small }
|
25
26
|
|
27
|
+
class Option < Container
|
28
|
+
tag :option
|
29
|
+
include Formular::Element::Modules::EscapeValue
|
30
|
+
end
|
31
|
+
|
32
|
+
|
26
33
|
class Hidden < Control
|
27
34
|
tag :input
|
28
35
|
set_default :type, 'hidden'
|
@@ -76,17 +83,17 @@ module Formular
|
|
76
83
|
|
77
84
|
# because this mutates attributes, we have to call this before rendering the start_tag
|
78
85
|
def method_tag
|
79
|
-
method =
|
86
|
+
method = options[:method]
|
80
87
|
|
81
88
|
case method
|
82
89
|
when /^get$/ # must be case-insensitive, but can't use downcase as might be nil
|
83
|
-
|
90
|
+
options[:method] = 'get'
|
84
91
|
''
|
85
92
|
when /^post$/, '', nil
|
86
|
-
|
93
|
+
options[:method] = 'post'
|
87
94
|
''
|
88
95
|
else
|
89
|
-
|
96
|
+
options[:method] = 'post'
|
90
97
|
Hidden.(value: method, name: '_method').to_s
|
91
98
|
end
|
92
99
|
end
|
@@ -140,12 +147,13 @@ module Formular
|
|
140
147
|
# as per MDN A label element can have both a 'for' attribute and a contained control element,
|
141
148
|
# as long as the for attribute points to the contained control element.
|
142
149
|
def labeled_control_id
|
143
|
-
return options[:labeled_control].
|
150
|
+
return options[:labeled_control].options[:id] if options[:labeled_control]
|
144
151
|
return builder.path(options[:attribute_name]).to_encoded_id if options[:attribute_name] && builder
|
145
152
|
end
|
146
153
|
end # class Label
|
147
154
|
|
148
155
|
class Submit < Formular::Element
|
156
|
+
include Formular::Element::Modules::EscapeValue
|
149
157
|
tag :input
|
150
158
|
|
151
159
|
set_default :type, 'submit'
|
@@ -153,26 +161,31 @@ module Formular
|
|
153
161
|
html { closed_start_tag }
|
154
162
|
end # class Submit
|
155
163
|
|
156
|
-
class Button <
|
157
|
-
|
158
|
-
add_option_keys :value
|
164
|
+
class Button < Container
|
165
|
+
include Formular::Element::Modules::Control
|
159
166
|
|
160
|
-
|
161
|
-
options[:value] || super
|
162
|
-
end
|
167
|
+
tag :button
|
163
168
|
end # class Button
|
164
169
|
|
165
170
|
class Input < Control
|
171
|
+
include HtmlEscape
|
172
|
+
|
166
173
|
tag :input
|
167
174
|
set_default :type, 'text'
|
175
|
+
process_option :value, :html_escape
|
176
|
+
|
168
177
|
html { closed_start_tag }
|
169
178
|
end # class Input
|
170
179
|
|
171
180
|
class Select < Control
|
172
181
|
include Formular::Element::Modules::Collection
|
182
|
+
include HtmlEscape
|
183
|
+
|
173
184
|
tag :select
|
174
185
|
|
175
|
-
add_option_keys :value
|
186
|
+
add_option_keys :value, :prompt, :include_blank
|
187
|
+
process_option :collection, :inject_placeholder
|
188
|
+
process_option :name, :name_array_if_multiple
|
176
189
|
|
177
190
|
html do |input|
|
178
191
|
concat start_tag
|
@@ -203,6 +216,32 @@ module Formular
|
|
203
216
|
end
|
204
217
|
|
205
218
|
private
|
219
|
+
# only append the [] to name if the multiple option is set
|
220
|
+
def name_array_if_multiple(name)
|
221
|
+
return unless name
|
222
|
+
|
223
|
+
options[:multiple] ? "#{name}[]" : name
|
224
|
+
end
|
225
|
+
|
226
|
+
# same handling as simple form
|
227
|
+
# prompt: a nil value option appears if we have no selected option
|
228
|
+
# include blank: includes our nil value option regardless (useful for optional fields)
|
229
|
+
def inject_placeholder(collection)
|
230
|
+
placeholder = if options[:include_blank]
|
231
|
+
placeholder_option(options[:include_blank])
|
232
|
+
elsif options[:prompt] && options[:value].nil?
|
233
|
+
placeholder_option(options[:prompt])
|
234
|
+
end
|
235
|
+
|
236
|
+
collection.unshift(placeholder) if placeholder
|
237
|
+
|
238
|
+
collection
|
239
|
+
end
|
240
|
+
|
241
|
+
def placeholder_option(value)
|
242
|
+
text = value.is_a?(String) ? html_escape(value) : ""
|
243
|
+
[text, ""]
|
244
|
+
end
|
206
245
|
|
207
246
|
def collection_to_options(collection)
|
208
247
|
collection.map do |item|
|
@@ -225,7 +264,7 @@ module Formular
|
|
225
264
|
|
226
265
|
opts[:value] = item.send(options[:value_method])
|
227
266
|
opts[:content] = item.send(options[:label_method])
|
228
|
-
opts[:selected] = 'selected' if opts[:value] == options[:value]
|
267
|
+
opts[:selected] = 'selected' if opts[:value].to_s == options[:value].to_s
|
229
268
|
|
230
269
|
Formular::Element::Option.new(opts).to_s
|
231
270
|
end
|
@@ -234,11 +273,11 @@ module Formular
|
|
234
273
|
class Checkbox < Control
|
235
274
|
tag :input
|
236
275
|
|
237
|
-
add_option_keys :unchecked_value, :include_hidden, :multiple
|
276
|
+
add_option_keys :unchecked_value, :checked_value, :include_hidden, :multiple
|
238
277
|
|
239
278
|
set_default :type, 'checkbox'
|
240
279
|
set_default :unchecked_value, :default_unchecked_value
|
241
|
-
set_default :value,
|
280
|
+
set_default :value, :default_checked_value # instead of reader value
|
242
281
|
set_default :include_hidden, true
|
243
282
|
|
244
283
|
include Formular::Element::Modules::Checkable
|
@@ -260,11 +299,15 @@ module Formular
|
|
260
299
|
def hidden_tag
|
261
300
|
return '' unless options[:include_hidden]
|
262
301
|
|
263
|
-
Hidden.(value: options[:unchecked_value], name:
|
302
|
+
Hidden.(value: options[:unchecked_value], name: options[:name]).to_s
|
264
303
|
end
|
265
304
|
|
266
305
|
private
|
267
306
|
|
307
|
+
def default_checked_value
|
308
|
+
options[:checked_value] || '1'
|
309
|
+
end
|
310
|
+
|
268
311
|
def default_unchecked_value
|
269
312
|
collection? ? '' : '0'
|
270
313
|
end
|
data/lib/formular/helper.rb
CHANGED
@@ -21,23 +21,37 @@ module Formular
|
|
21
21
|
}.freeze
|
22
22
|
|
23
23
|
class << self
|
24
|
+
def _builder
|
25
|
+
@builder || :basic
|
26
|
+
end
|
24
27
|
attr_writer :builder
|
25
28
|
|
26
|
-
def builder(name
|
27
|
-
|
29
|
+
def builder(name)
|
30
|
+
self.builder = name
|
31
|
+
end
|
32
|
+
|
33
|
+
def load_builder(name)
|
34
|
+
builder_const = BUILDERS.fetch(name, nil)
|
35
|
+
return name unless builder_const
|
36
|
+
|
28
37
|
require "formular/builders/#{name}"
|
29
|
-
|
38
|
+
Formular::Builders.const_get(builder_const)
|
30
39
|
end
|
31
40
|
end
|
32
41
|
|
33
42
|
private
|
34
43
|
|
35
44
|
def builder(model, **options)
|
36
|
-
|
45
|
+
builder_name = options.delete(:builder)
|
46
|
+
builder_name ||= Formular::Helper._builder
|
47
|
+
|
48
|
+
builder = Formular::Helper.load_builder(builder_name)
|
49
|
+
|
37
50
|
options[:model] ||= model
|
38
51
|
|
39
52
|
builder.new(options)
|
40
53
|
end
|
54
|
+
|
41
55
|
end # module Helper
|
42
56
|
|
43
57
|
module RailsHelper
|
data/lib/formular/html_block.rb
CHANGED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Formular
|
2
|
+
module HtmlEscape
|
3
|
+
# see activesupport/lib/active_support/core_ext/string/output_safety.rb
|
4
|
+
|
5
|
+
HTML_ESCAPE = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' }
|
6
|
+
HTML_ESCAPE_REGEXP = /[&"'><]/
|
7
|
+
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+)|(#[xX][\dA-Fa-f]+));)/
|
8
|
+
|
9
|
+
# A utility method for escaping HTML tag characters.
|
10
|
+
def html_escape(string)
|
11
|
+
string.to_s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE)
|
12
|
+
end
|
13
|
+
|
14
|
+
# A utility method for escaping HTML without affecting existing escaped entities.
|
15
|
+
def html_escape_once(string)
|
16
|
+
string.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/formular/path.rb
CHANGED
@@ -3,9 +3,6 @@ module Formular
|
|
3
3
|
# name #attribute without model...
|
4
4
|
# [User, :name] => user[name] #regular attribute
|
5
5
|
# [User, roles: 0, :name] => user[roles][][name]
|
6
|
-
|
7
|
-
# DISCUSS: Should we also enable the following for rails accepts_nested_attributes support
|
8
|
-
# [User, roles: 0, :name => user[roles_attributes][0][name]
|
9
6
|
def to_encoded_name
|
10
7
|
map.with_index do |segment, i|
|
11
8
|
first_or_last = i == 0 || i == size
|
@@ -20,9 +17,7 @@ module Formular
|
|
20
17
|
|
21
18
|
# need to inject the index in here... else we will end up with the same ids
|
22
19
|
# [User, :name] => user_name #regular attribute
|
23
|
-
# [User, roles: 0, :name] => user_roles_0_name
|
24
|
-
# DISCUSS: Should we also enable the following for rails accepts_nested_attributes support
|
25
|
-
# [User, roles: 0, :name => user_roles_attributes_0_name #nested rails
|
20
|
+
# [User, roles: 0, :name] => user_roles_0_name
|
26
21
|
def to_encoded_id
|
27
22
|
map { |segment| segment.is_a?(Array) ? segment.join('_') : segment }.join('_')
|
28
23
|
end
|
data/lib/formular/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: formular
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Sutterer
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2017-09-13 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: declarative
|
@@ -29,16 +29,22 @@ dependencies:
|
|
29
29
|
name: uber
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
31
31
|
requirements:
|
32
|
-
- - "
|
32
|
+
- - ">="
|
33
33
|
- !ruby/object:Gem::Version
|
34
34
|
version: 0.0.11
|
35
|
+
- - "<"
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 0.2.0
|
35
38
|
type: :runtime
|
36
39
|
prerelease: false
|
37
40
|
version_requirements: !ruby/object:Gem::Requirement
|
38
41
|
requirements:
|
39
|
-
- - "
|
42
|
+
- - ">="
|
40
43
|
- !ruby/object:Gem::Version
|
41
44
|
version: 0.0.11
|
45
|
+
- - "<"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.2.0
|
42
48
|
- !ruby/object:Gem::Dependency
|
43
49
|
name: bundler
|
44
50
|
requirement: !ruby/object:Gem::Requirement
|
@@ -174,22 +180,25 @@ files:
|
|
174
180
|
- lib/formular/element/bootstrap4/checkable_control.rb
|
175
181
|
- lib/formular/element/bootstrap4/custom_control.rb
|
176
182
|
- lib/formular/element/bootstrap4/horizontal.rb
|
183
|
+
- lib/formular/element/bootstrap4/input_group.rb
|
177
184
|
- lib/formular/element/foundation6.rb
|
178
185
|
- lib/formular/element/foundation6/checkable_control.rb
|
179
186
|
- lib/formular/element/foundation6/input_group.rb
|
180
|
-
- lib/formular/element/foundation6/
|
187
|
+
- lib/formular/element/foundation6/wrapped.rb
|
181
188
|
- lib/formular/element/module.rb
|
182
189
|
- lib/formular/element/modules/checkable.rb
|
183
190
|
- lib/formular/element/modules/collection.rb
|
184
191
|
- lib/formular/element/modules/container.rb
|
185
192
|
- lib/formular/element/modules/control.rb
|
186
193
|
- lib/formular/element/modules/error.rb
|
194
|
+
- lib/formular/element/modules/escape_value.rb
|
187
195
|
- lib/formular/element/modules/hint.rb
|
188
196
|
- lib/formular/element/modules/label.rb
|
189
|
-
- lib/formular/element/modules/
|
197
|
+
- lib/formular/element/modules/wrapped.rb
|
190
198
|
- lib/formular/elements.rb
|
191
199
|
- lib/formular/helper.rb
|
192
200
|
- lib/formular/html_block.rb
|
201
|
+
- lib/formular/html_escape.rb
|
193
202
|
- lib/formular/path.rb
|
194
203
|
- lib/formular/version.rb
|
195
204
|
homepage: http://trailblazer.to/gems/formular.html
|
@@ -212,7 +221,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
212
221
|
version: '0'
|
213
222
|
requirements: []
|
214
223
|
rubyforge_project:
|
215
|
-
rubygems_version: 2.
|
224
|
+
rubygems_version: 2.5.1
|
216
225
|
signing_key:
|
217
226
|
specification_version: 4
|
218
227
|
summary: Form builder based on Cells. Fast, Furious, and Framework-Agnostic.
|