formless 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 81893b813f0da1305af226192c0715a5f63f883e
4
+ data.tar.gz: 9e2b6cc25e49ad246db6e6519862378d8a4fbd15
5
+ SHA512:
6
+ metadata.gz: 2161405866eb39ee0dc998818e8ea848b5d48b511a48a279627ddaa53bd21e763f51f2709b0dc2ca9a5db1c1ed150e3153c67c4a7e162cf991d42ae197808c4e
7
+ data.tar.gz: 3a6230c67266f323827608a1ff9baa2f2cacba1fa18cdf36fda32e7b8785eb11f8e76fdf7f6e2ed5e55e7d373570a0b34de695cfc13eb0d9e200782b1ed59906
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'nokogiri', '>=1.5'
@@ -0,0 +1,74 @@
1
+ Formless
2
+ ========
3
+
4
+ Formless provides a means to populate forms without the need for anything other than plain-old HTML. It removes the requirement for form population logic within your views, serving as a complete replacement for form builders.
5
+
6
+ Formless can be used with any existing libraries or frameworks. Its only dependancy is Nokogiri.
7
+
8
+
9
+ In Action
10
+ ---------
11
+ Install it...
12
+
13
+ gem install formless
14
+
15
+ Take some HTML...
16
+
17
+ <!DOCTYPE html>
18
+ <html>
19
+ <body>
20
+ <h1>Edit Person</h1>
21
+ <form id="edit_form" method="POST" action="./">
22
+ <input type="text" name="full_name" />
23
+ <input type="number" min="0" name="age" />
24
+ <label><input type="radio" name="gender" value="m"> Male</label>
25
+ <label><input type="radio" name="gender" value="f"> Female</label>
26
+ <select name="region">
27
+ <option>America</option>
28
+ <option>Europe</option>
29
+ <option>Oceania</option>
30
+ </select>
31
+ <input type="submit" value="Submit" />
32
+ </form>
33
+ </body>
34
+ </html>
35
+
36
+ And populate it...
37
+
38
+ selector = '#edit_form'
39
+ values = {name: 'Jeffrey', age: 29, gender: 'm', region: 'Europe'}
40
+ FormPopulator.new('<html>...</html>', selector).populate!(values).to_s #=> <!DOCTYPE html><html> ... </html>
41
+
42
+
43
+ How It Works
44
+ ------------
45
+ Nokogiri is used to parse the given HTML into a data structure that can be easily and reliably operated on. The keys in the provided hash are mapped to the `name` attribute of HTML elements. A collection of _field setters_, defaulting to `Formless::FieldSetters` are responsible for correctly setting the various field types, whilst _formatters_, defaulting to `Formless::Formatters` provide an opportunity to process the value before setting it, such as formatting dates.
46
+
47
+
48
+ Performance
49
+ -----------
50
+ Convenience is prioritised over performance; there are many less convenient but better performing solutions if that's your priority. With that said, there are ways to optimise your use of Formless. The most obvious optimisation is to re-use your Formless or Nokogiri NodeSet instances, to save re-parsing your HTML:
51
+
52
+ @form ||= Formless.new('...')
53
+ @form.populate({name: 'Bill', age: 31}).to_s #=> <!DOCTYPE html><html> ... </html>
54
+
55
+ Formless also provides two complementary `populate` methods. `populate!` modifies the nodeset associated with the Formless instance, whilst `populate` works on a copy of that nodeset. Where performance is important, `populate!` should be used. It's important to note however that you must explicitly set a field to a value for that field to be reset, so extra care must be taken:
56
+
57
+ @form ||= Formless.new('...')
58
+ @form.populate!({name: 'Bill', age: 31})
59
+ @form.populate!({name: 'John'}) #=> Age is still set to 31
60
+ @form.populate!({name: 'John', age: nil}) #=> Age is now set to empty
61
+
62
+
63
+ Comprehensive
64
+ -------------
65
+ Formless is intended to provide comprehensive support for HTML5 forms. Any contradiction to this is considered a bug. To summarise:
66
+
67
+ * All HTML5 input fields, including:
68
+ * Checkboxes
69
+ * Radio buttons
70
+ * Password's which are not populated by default
71
+ * Date and time fields: date, datetime, datetime-local, week, month
72
+ * Textarea fields
73
+ * Select fields, including multi-select fields
74
+ * Common-name fields, such as for use with the array idiom, e.g. name="favourites[]"
@@ -0,0 +1,8 @@
1
+ require './lib/formless'
2
+
3
+ run proc { |env|
4
+ request = Rack::Request.new(env)
5
+ html = open('./spec/form.html').read
6
+ form = Formless.new(html)
7
+ [200, {}, [form.populate(request.POST).to_s]]
8
+ }
@@ -0,0 +1,18 @@
1
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
2
+ require 'formless'
3
+
4
+ Gem::Specification.new 'formless', Formless::VERSION do |s|
5
+ s.summary = "Unobtrusive form populator for web applications."
6
+ s.description = "Completely transparent, unobtrusive form populator for web applications and content scrapers."
7
+ s.authors = ["Tom Wardrop"]
8
+ s.email = "tom@tomwardrop.com"
9
+ s.homepage = "http://github.com/wardrop/Formless"
10
+ s.files = Dir.glob(`git ls-files`.split("\n") - %w[.gitignore])
11
+ s.test_files = Dir.glob('spec/**/*_spec.rb')
12
+ s.rdoc_options = %w[--line-numbers --inline-source --title Scorched --encoding=UTF-8]
13
+
14
+ s.required_ruby_version = '>= 1.9.3'
15
+
16
+ s.add_dependency 'nokogiri', '~> 1.5'
17
+ s.add_development_dependency 'rspec', '~> 2.9'
18
+ end
@@ -0,0 +1,125 @@
1
+ require 'date'
2
+ require 'nokogiri'
3
+
4
+ class Formless
5
+ VERSION = '0.1'
6
+ DATE_FORMAT = '%d/%m/%Y'
7
+ DATETIME_FORMAT = '%d/%m/%Y %l:%M%P'
8
+
9
+ FieldSetters = {
10
+ textarea: proc { |node, values|
11
+ node.content = values.shift
12
+ },
13
+ radio: proc { |node, values|
14
+ node.delete('checked')
15
+ node['checked'] = 'checked' if values.include? node['value']
16
+ },
17
+ checkbox: proc { |node, values|
18
+ node.delete('checked')
19
+ node['checked'] = 'checked' if values.include? node['value']
20
+ },
21
+ select: proc { |node, values|
22
+ matches = node.css('option').to_a.each { |n| n.delete('selected') }.find_all do |n|
23
+ n.has_attribute?('value') ? values.include?(n['value']) : values.include?(n.content)
24
+ end
25
+ if !matches.empty?
26
+ if node.has_attribute? 'multiple'
27
+ matches.each { |n| n['selected'] = 'selected' }
28
+ else
29
+ matches.first['selected'] = 'selected'
30
+ end
31
+ elsif !node.has_attribute?('multiple') && value = values.compact.first
32
+ node << node.document.create_element('option', value, selected: 'selected')
33
+ end
34
+ },
35
+ password: proc { |node, values|
36
+ node['value'] = values.shift if options[:populate_passwords]
37
+ },
38
+ default: proc { |node, values|
39
+ node['value'] = values.shift
40
+ }
41
+ }
42
+
43
+ Formatters = {
44
+ [Date, Time] => proc { |node, value|
45
+ case node['type']
46
+ when 'date'
47
+ value.strftime('%F')
48
+ when 'datetime'
49
+ value.strftime('%FT%T%z')
50
+ when 'datetime-local'
51
+ value.strftime('%FT%T')
52
+ when 'week'
53
+ value.strftime('%G-W%V')
54
+ when 'month'
55
+ value.strftime('%G-%m')
56
+ else
57
+ (value.respond_to? :hour) ? value.strftime(DATETIME_FORMAT) : value.strftime(DATE_FORMAT)
58
+ end
59
+ },
60
+ nil => proc { '' }
61
+ }
62
+
63
+ attr_accessor :selector
64
+ attr_reader :options
65
+ attr_reader :nodeset
66
+
67
+ def nodeset=(html)
68
+ @nodeset = case html
69
+ when Nokogiri::XML::Node
70
+ Nokogiri::XML::NodeSet.new(html, [html])
71
+ when Nokogiri::XML::NodeSet
72
+ html
73
+ else
74
+ doc = Nokogiri.parse(html)
75
+ Nokogiri::XML::NodeSet.new(doc, [doc])
76
+ end
77
+ end
78
+
79
+ def initialize(html, selector = nil, **options)
80
+ self.nodeset = html
81
+ self.selector = selector
82
+ @options = {
83
+ field_setters: FieldSetters,
84
+ formatters: Formatters,
85
+ populate_passwords: false
86
+ }.merge!(options)
87
+ end
88
+
89
+ def field_setters
90
+ self.options[:field_setters]
91
+ end
92
+
93
+ def formatters
94
+ self.options[:formatters]
95
+ end
96
+
97
+ def populate(values, selector = nil, nodeset = self.nodeset)
98
+ populate!(values, selector, Nokogiri::XML::NodeSet.new(nodeset.document, nodeset.to_a.map! { |n| n.dup }))
99
+ end
100
+
101
+ def populate!(values, selector = nil, nodeset = self.nodeset)
102
+ nodeset = selector ? nodeset.css(selector) : nodeset
103
+ values.each do |field, value|
104
+ nodes = nodeset.css(%{[name="#{field}"]})
105
+ nodes = nodeset.css(%{[name="#{field}[]"]}) if nodes.empty?
106
+ nodes.each { |n| set_field(n, value) }
107
+ end
108
+ nodeset
109
+ end
110
+
111
+ private
112
+
113
+ def set_field(node, value)
114
+ setter = field_setters[(node['type'] || node.name).to_sym] || FieldSetters[:default]
115
+ instance_exec(node, [*format_value(node, value)], &setter)
116
+ end
117
+
118
+ def format_value(node, value)
119
+ condition, formatter = formatters.find do |conditions, block|
120
+ [*conditions].find { |c| c === value }
121
+ end
122
+ formatter ? instance_exec(node, value, &formatter) : value
123
+ end
124
+
125
+ end
@@ -0,0 +1,45 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
5
+ <style>
6
+ label { display: block; }
7
+ </style>
8
+ </head>
9
+ <body>
10
+ <h1>Edit Person</h1>
11
+ <form id="edit_form" method="POST" action="./">
12
+ <label>Password: <input type="password" name="password"></label>
13
+ <label>Unknown: <input type="unknown" name="unknown"></label>
14
+ <label>Full Name: <input type="text" name="full_name"></label>
15
+ <label>Age: <input type="number" min="0" name="age"></label>
16
+ <label><input type="radio" name="gender" value="m"> Male</label>
17
+ <label><input type="radio" name="gender" value="f"> Female</label>
18
+ <label>Region:
19
+ <select name="region">
20
+ <option>America</option>
21
+ <option>Europe</option>
22
+ <option value="Australia">Oceania</option>
23
+ </select>
24
+ </label>
25
+ <label>Foods:
26
+ <select name="foods[]" multiple>
27
+ <option>Pizza</option>
28
+ <option>Burger</option>
29
+ <option>Chips</option>
30
+ </select>
31
+ </label>
32
+ <textarea name="hobbies"></textarea>
33
+ <label>Birthday: <input type="date" name="birthday"></label>
34
+ <label>Breakfast: <input type="datetime" name="breakfast"></label>
35
+ <label>Dinner: <input type="datetime-local" name="dinner"></label>
36
+ <label>Favourite Week: <input type="week" name="favourite_week"></label>
37
+ <label>Favourite Month: <input type="month" name="favourite_month"></label>
38
+ <label><input type="checkbox" name="colours[]" value="red">Red</label>
39
+ <label><input type="checkbox" name="colours[]" value="green">Green</label>
40
+ <label><input type="checkbox" name="colours[]" value="blue">Blue</label>
41
+ <label><input type="checkbox" name="subscribe" value="yes"> Subscribe</label>
42
+ <div><input type="submit" value="Submit"></div>
43
+ </form>
44
+ </body>
45
+ </html>
@@ -0,0 +1,135 @@
1
+ require_relative './helper.rb'
2
+
3
+ describe Formless do
4
+ it "can take a html string" do
5
+ form = Formless.new(html)
6
+ form.nodeset.should be_a(Nokogiri::XML::NodeSet)
7
+ form.nodeset.to_s.strip.should == html.strip
8
+ end
9
+
10
+ it "can take a nokogiri node" do
11
+ doc = Nokogiri.parse(html)
12
+ form = Formless.new(doc)
13
+ form.nodeset.should be_a(Nokogiri::XML::NodeSet)
14
+ form.nodeset.document.should == doc
15
+ form.nodeset[0].should == doc
16
+ end
17
+
18
+ it "can take a nokogiri nodeset" do
19
+ nodeset = Nokogiri.parse(html).css('> *')
20
+ form = Formless.new(nodeset)
21
+ form.nodeset.should == nodeset
22
+ end
23
+
24
+ let(:form) do
25
+ Formless.new(html)
26
+ end
27
+
28
+ describe "field populating" do
29
+ example "default/unknown" do
30
+ form.populate(unknown: 'yay').css('[name=unknown]')[0]['value'].should == 'yay'
31
+ end
32
+
33
+ example "text" do
34
+ form.populate(full_name: 'William Fisher').css('[name=full_name]')[0]['value'].should == 'William Fisher'
35
+ end
36
+
37
+ example "textarea" do
38
+ form.populate(hobbies: 'fishing, camping').css('[name=hobbies]')[0].content.should == 'fishing, camping'
39
+ end
40
+
41
+ example "radio" do
42
+ form.populate(gender: 'f').css('[name=gender][value=f]')[0].has_attribute?('checked').should == true
43
+ end
44
+
45
+ example "checkbox" do
46
+ form.populate(subscribe: 'yes').css('[name=subscribe]')[0].has_attribute?('checked').should == true
47
+ end
48
+
49
+ example "select" do
50
+ form.populate(region: 'Europe').
51
+ css('[name=region] > option').find { |n| n.has_attribute?('selected') }.text.should == 'Europe'
52
+ form.populate(region: 'Australia').
53
+ css('[name=region] > option').find { |n| n.has_attribute?('selected') }.text.should == 'Oceania'
54
+ # Will add non-existant values
55
+ form.populate(region: 'Middle East').
56
+ css('[name=region] > option').find { |n| n.has_attribute?('selected') }.text.should == 'Middle East'
57
+ end
58
+
59
+ example "multi-select" do
60
+ # Also tests order independance
61
+ form.populate('foods[]' => ['Chips', 'Pizza']).css('[name="foods[]"] > option').select { |n|
62
+ n.has_attribute?('selected')
63
+ }.map { |v| v.text }.should == ['Pizza', 'Chips']
64
+
65
+ # Will automatically append square brackets if not found.
66
+ form.populate('foods' => ['Pizza', 'Chips']).css('[name="foods[]"] > option').select { |n|
67
+ n.has_attribute?('selected')
68
+ }.map { |v| v.text }.should == ['Pizza', 'Chips']
69
+ end
70
+
71
+ example "date" do
72
+ date = Date.today
73
+ form.populate(birthday: date).css('[name=birthday]')[0]['value'].should == date.strftime('%F')
74
+ end
75
+
76
+ example "datetime" do
77
+ time = DateTime.now
78
+ form.populate(breakfast: time).css('[name=breakfast]')[0]['value'].should == time.strftime('%FT%T%z')
79
+ end
80
+
81
+ example "datetime-local" do
82
+ time = DateTime.now
83
+ form.populate(dinner: time).css('[name=dinner]')[0]['value'].should == time.strftime('%FT%T')
84
+ end
85
+
86
+ example "month" do
87
+ time = DateTime.new(2013, 9, 2, 11, 30, 15)
88
+ form.populate(favourite_month: time).css('[name=favourite_month]')[0]['value'].should == '2013-09'
89
+ end
90
+
91
+ example "week" do
92
+ time = DateTime.new(2013, 9, 2, 11, 30, 15)
93
+ form.populate(favourite_week: time).css('[name=favourite_week]')[0]['value'].should == '2013-W36'
94
+ end
95
+
96
+ example "array-like field names" do
97
+ form.populate('colours[]' => %w{blue red}).css('[name="colours[]"][checked=checked]').map { |n| n['value'] }.should == ['red', 'blue']
98
+ end
99
+ end
100
+
101
+ it "outputs a Nokogiri nodeset" do
102
+ form.populate({}).should be_a(Nokogiri::XML::NodeSet)
103
+ end
104
+
105
+ it "can modify the original nodeset, or a copy" do
106
+ form.populate!({full_name: 'Bob Bobinski'}).css('[name=full_name]')[0]['value'].should == 'Bob Bobinski'
107
+ form.populate({full_name: 'Harold Haroldo'}).css('[name=full_name]')[0]['value'].should == 'Harold Haroldo'
108
+ form.populate!({}).css('[name=full_name]')[0]['value'].should == 'Bob Bobinski'
109
+ end
110
+
111
+ it "can take a CSS selector to narrow the nodeset" do
112
+ form.populate!({full_name: 'Tony Jones', hobbies: 'stuff'}, 'input')
113
+ form.nodeset.css('[name=full_name]')[0]['value'].should == 'Tony Jones'
114
+ form.nodeset.css('[name=hobbies]')[0].content.should_not == 'stuff'
115
+ end
116
+
117
+ describe "configuration" do
118
+ it "can toggle whether password fields are populated" do
119
+ form.populate(password: 'ilovemonkeys').css('[name=password]')[0]['value'].should == nil
120
+ form.options[:populate_passwords] = true
121
+ form.populate(password: 'ilovemonkeys').css('[name=password]')[0]['value'].should == 'ilovemonkeys'
122
+ end
123
+
124
+ it "allows field setters to be overridden" do
125
+ form.options[:field_setters] = Formless::FieldSetters.merge(password: proc { |node| node['value'] = 'meow' })
126
+ form.populate(password: 'ilovemonkeys').css('[name=password]')[0]['value'].should == 'meow'
127
+ end
128
+
129
+ it "allows formatters to be overridden" do
130
+ form.options[:formatters] = Formless::Formatters.merge('yes' => proc { |n,v| v == 'yes' ? 1 : 0 })
131
+ form.populate({full_name: 'yes'}).css('[name=full_name]')[0]['value'].should == '1'
132
+ end
133
+ end
134
+
135
+ end
@@ -0,0 +1,15 @@
1
+ require 'rack/test'
2
+ require_relative '../lib/formless.rb'
3
+
4
+ module GlobalConfig
5
+ extend RSpec::SharedContext
6
+ let(:html) do
7
+ open(File.join __dir__, 'form.html').read
8
+ end
9
+ end
10
+
11
+ RSpec.configure do |c|
12
+ c.alias_example_to :they
13
+ # c.backtrace_clean_patterns = []
14
+ c.include GlobalConfig
15
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: formless
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Tom Wardrop
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-04-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '2.9'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '2.9'
41
+ description: Completely transparent, unobtrusive form populator for web applications
42
+ and content scrapers.
43
+ email: tom@tomwardrop.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - Gemfile
49
+ - README.md
50
+ - config.ru
51
+ - formless.gemspec
52
+ - lib/formless.rb
53
+ - spec/form.html
54
+ - spec/formless_spec.rb
55
+ - spec/helper.rb
56
+ homepage: http://github.com/wardrop/Formless
57
+ licenses: []
58
+ metadata: {}
59
+ post_install_message:
60
+ rdoc_options:
61
+ - --line-numbers
62
+ - --inline-source
63
+ - --title
64
+ - Scorched
65
+ - --encoding=UTF-8
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - '>='
71
+ - !ruby/object:Gem::Version
72
+ version: 1.9.3
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubyforge_project:
80
+ rubygems_version: 2.0.0
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: Unobtrusive form populator for web applications.
84
+ test_files:
85
+ - spec/formless_spec.rb