hexp 0.0.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/.travis.yml +12 -3
  2. data/Changelog.md +9 -0
  3. data/Gemfile +3 -5
  4. data/Gemfile.devtools +20 -18
  5. data/Gemfile.lock +97 -84
  6. data/Rakefile +16 -0
  7. data/config/flay.yml +2 -2
  8. data/config/flog.yml +1 -1
  9. data/config/reek.yml +42 -18
  10. data/config/rubocop.yml +31 -0
  11. data/config/yardstick.yml +39 -1
  12. data/examples/from_nokogiri.rb +77 -0
  13. data/examples/selector_rewriter_chaining.rb +14 -0
  14. data/examples/todo.rb +138 -0
  15. data/examples/widget.rb +64 -0
  16. data/hexp.gemspec +8 -3
  17. data/lib/hexp.rb +103 -2
  18. data/lib/hexp/builder.rb +256 -0
  19. data/lib/hexp/css_selector.rb +205 -0
  20. data/lib/hexp/css_selector/parser.rb +74 -0
  21. data/lib/hexp/css_selector/sass_parser.rb +22 -0
  22. data/lib/hexp/dom.rb +0 -2
  23. data/lib/hexp/dsl.rb +27 -0
  24. data/lib/hexp/errors.rb +21 -0
  25. data/lib/hexp/h.rb +5 -2
  26. data/lib/hexp/list.rb +67 -9
  27. data/lib/hexp/node.rb +197 -41
  28. data/lib/hexp/node/attributes.rb +176 -0
  29. data/lib/hexp/node/children.rb +44 -0
  30. data/lib/hexp/node/css_selection.rb +73 -0
  31. data/lib/hexp/node/domize.rb +52 -6
  32. data/lib/hexp/node/normalize.rb +19 -9
  33. data/lib/hexp/node/pp.rb +32 -0
  34. data/lib/hexp/node/rewriter.rb +52 -0
  35. data/lib/hexp/node/selector.rb +59 -0
  36. data/lib/hexp/nokogiri/equality.rb +61 -0
  37. data/lib/hexp/nokogiri/reader.rb +27 -0
  38. data/lib/hexp/sass/selector_parser.rb +4 -0
  39. data/lib/hexp/text_node.rb +129 -9
  40. data/lib/hexp/version.rb +1 -1
  41. data/notes +34 -0
  42. data/spec/shared_helper.rb +6 -0
  43. data/spec/spec_helper.rb +2 -6
  44. data/spec/unit/hexp/builder_spec.rb +101 -0
  45. data/spec/unit/hexp/css_selector/attribute_spec.rb +137 -0
  46. data/spec/unit/hexp/css_selector/class_spec.rb +15 -0
  47. data/spec/unit/hexp/css_selector/comma_sequence_spec.rb +20 -0
  48. data/spec/unit/hexp/css_selector/element_spec.rb +11 -0
  49. data/spec/unit/hexp/css_selector/parser_spec.rb +51 -0
  50. data/spec/unit/hexp/css_selector/simple_sequence_spec.rb +48 -0
  51. data/spec/unit/hexp/dsl_spec.rb +55 -0
  52. data/spec/unit/hexp/h_spec.rb +38 -0
  53. data/spec/unit/hexp/list_spec.rb +19 -0
  54. data/spec/unit/hexp/node/attr_spec.rb +55 -0
  55. data/spec/unit/hexp/node/attributes_spec.rb +125 -0
  56. data/spec/unit/hexp/node/children_spec.rb +33 -0
  57. data/spec/unit/hexp/node/class_spec.rb +37 -0
  58. data/spec/unit/hexp/node/css_selection_spec.rb +86 -0
  59. data/spec/unit/hexp/node/normalize_spec.rb +12 -6
  60. data/spec/unit/hexp/node/rewrite_spec.rb +67 -30
  61. data/spec/unit/hexp/node/selector_spec.rb +78 -0
  62. data/spec/unit/hexp/node/text_spec.rb +7 -0
  63. data/spec/unit/hexp/node/to_dom_spec.rb +1 -1
  64. data/spec/unit/hexp/nokogiri/reader_spec.rb +8 -0
  65. data/spec/unit/hexp/parse_spec.rb +23 -0
  66. data/spec/unit/hexp/text_node_spec.rb +25 -0
  67. data/spec/unit/hexp_spec.rb +33 -0
  68. metadata +129 -16
  69. data/lib/hexp/format_error.rb +0 -8
@@ -0,0 +1,31 @@
1
+ AllCops:
2
+ Includes:
3
+ - '../**/*.rake'
4
+ Excludes:
5
+ - '../vendor/**'
6
+
7
+ # Avoid parameter lists longer than five parameters.
8
+ ParameterLists:
9
+ Max: 3
10
+ CountKeywordArgs: true
11
+
12
+ # Avoid more than `Max` levels of nesting.
13
+ BlockNesting:
14
+ Max: 3
15
+
16
+ # Align with the style guide.
17
+ CollectionMethods:
18
+ PreferredMethods:
19
+ collect: 'map'
20
+ inject: 'reduce'
21
+ find: 'detect'
22
+ find_all: 'select'
23
+
24
+ # Do not force public/protected/private keyword to be indented at the same
25
+ # level as the def keyword. My personal preference is to outdent these keywords
26
+ # because I think when scanning code it makes it easier to identify the
27
+ # sections of code and visually separate them. When the keyword is at the same
28
+ # level I think it sort of blends in with the def keywords and makes it harder
29
+ # to scan the code and see where the sections are.
30
+ AccessControl:
31
+ Enabled: false
@@ -1,2 +1,40 @@
1
1
  ---
2
- threshold: 77.5
2
+ threshold: 93.7
3
+ rules:
4
+ ApiTag::Presence:
5
+ enabled: true
6
+ exclude: []
7
+ ApiTag::Inclusion:
8
+ enabled: true
9
+ exclude: []
10
+ ApiTag::ProtectedMethod:
11
+ enabled: true
12
+ exclude: []
13
+ ApiTag::PrivateMethod:
14
+ enabled: false
15
+ exclude: []
16
+ ExampleTag:
17
+ enabled: false
18
+ exclude:
19
+ - Hexp::Nokogiri::Equality#call
20
+ - Hexp::Nokogiri::Equality#equal_class?
21
+ - Hexp::Nokogiri::Equality#equal_name?
22
+ - Hexp::Nokogiri::Equality#equal_children?
23
+ - Hexp::Nokogiri::Equality#compare_children
24
+ - Hexp::Nokogiri::Equality#equal_attributes?
25
+ - Hexp::Nokogiri::Equality#equal_text?
26
+ ReturnTag:
27
+ enabled: true
28
+ exclude: []
29
+ Summary::Presence:
30
+ enabled: true
31
+ exclude: []
32
+ Summary::Length:
33
+ enabled: true
34
+ exclude: []
35
+ Summary::Delimiter:
36
+ enabled: true
37
+ exclude: []
38
+ Summary::SingleLine:
39
+ enabled: true
40
+ exclude: []
@@ -0,0 +1,77 @@
1
+ $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
2
+
3
+ require 'hexp'
4
+ require 'minitest/autorun'
5
+
6
+ module Hexp
7
+ def self.from_nokogiri(node)
8
+ attrs = node.attributes.map do |k,v|
9
+ [k.to_sym, v.value]
10
+ end
11
+ children = node.children.map do |child|
12
+ case child
13
+ when ::Nokogiri::XML::Text
14
+ Hexp::TextNode.new(child.text)
15
+ when ::Nokogiri::XML::Node
16
+ from_nokogiri(child)
17
+ end
18
+ end
19
+ H[node.name.to_sym, Hash[attrs], children]
20
+ end
21
+ end
22
+
23
+ class Nokogiri::XML::Node
24
+ def to_hexp
25
+ Hexp.from_nokogiri(self)
26
+ end
27
+ end
28
+
29
+ class Nokogiri::XML::Document
30
+ end
31
+
32
+ describe Hexp, 'from_nokigiri' do
33
+ def doc
34
+ @doc ||= Nokogiri::HTML::Document.new
35
+ end
36
+
37
+ it 'should convert a single node' do
38
+ h3 = Nokogiri::XML::Node.new "h3", doc
39
+ Hexp.from_nokogiri(h3).must_equal H[:h3]
40
+ end
41
+
42
+ it 'should extract arguments, if there are any' do
43
+ h3 = Nokogiri::XML::Node.new "h3", doc
44
+ h3[:class] = 'foo'
45
+ Hexp.from_nokogiri(h3).must_equal H[:h3, {class: 'foo'}]
46
+ end
47
+
48
+ it 'should convert children' do
49
+ div = Nokogiri::XML::Node.new "div", doc
50
+ p = Nokogiri::XML::Node.new "p", doc
51
+ span = Nokogiri::XML::Node.new "span", doc
52
+ div << p
53
+ div << span
54
+
55
+ Hexp.from_nokogiri(div).must_equal H[:div, [[:p], [:span]]]
56
+ end
57
+
58
+ it 'should convert text nodes' do
59
+ div = Nokogiri::XML::Node.new "div", doc
60
+ p = Nokogiri::XML::Node.new "p", doc
61
+ text = Nokogiri::XML::Text.new "text", doc
62
+ div << p
63
+ div << text
64
+
65
+ Hexp.from_nokogiri(div).must_equal H[:div, [[:p], "text"]]
66
+ end
67
+ end
68
+
69
+ # >> Run options: --seed 54619
70
+ # >>
71
+ # >> # Running tests:
72
+ # >>
73
+ # >> ....
74
+ # >>
75
+ # >> Finished tests in 0.003244s, 1233.0547 tests/s, 1233.0547 assertions/s.
76
+ # >>
77
+ # >> 4 tests, 4 assertions, 0 failures, 0 errors, 0 skips
@@ -0,0 +1,14 @@
1
+ d=H[:div, %w(foo bar baz).map{|klz| [:p, class: klz]}]
2
+
3
+ #=> H[:div, [H[:p, {"class"=>"foo"}], H[:p, {"class"=>"bar"}], H[:p, {"class"=>"baz"}]]]
4
+
5
+ d.select {|node|node.class? 'bar'} #=> #<Hexp::Node::Selector>
6
+ .wrap(:span) #=> #<Hexp::Node::Rewriter>
7
+ .attr('data-x', '77') #=> #<Hexp::Node::Rewriter>
8
+ .wrap(:foo, 'hello' => 'jow') #=> #<Hexp::Node::Rewriter>
9
+ .attr('faz', 'foz').to_html(:include_doctype => false)
10
+
11
+ # <div>
12
+ # <p class="foo"></p>
13
+ # <foo hello="jow" faz="foz"><span data-x="77"><p class="bar"></p></span></foo><p class="baz"></p>
14
+ # </div>
@@ -0,0 +1,138 @@
1
+ $:.unshift File.expand_path('../../lib', __FILE__)
2
+ $:.unshift File.expand_path('../../examples', __FILE__)
3
+
4
+ require 'sinatra'
5
+ require 'hexp'
6
+ require 'widget'
7
+
8
+ class EntryStore
9
+ def self.store(entry)
10
+ @counter ||= 0
11
+ @entries ||= {}
12
+ entry.id = (@counter+=1) unless entry.id
13
+ @entries[entry.id] = entry
14
+ end
15
+
16
+ def self.find(id)
17
+ @entries[id]
18
+ end
19
+
20
+ def self.all
21
+ @entries ||= {}
22
+ @entries.values || []
23
+ end
24
+
25
+ def self.delete(id)
26
+ @entries.delete(id)
27
+ end
28
+ end
29
+
30
+ class Entry < Widget(:span)
31
+ attribute :id, Integer
32
+ attribute :description, String
33
+
34
+ def widget
35
+ [ description ]
36
+ end
37
+ end
38
+
39
+ class EntryList
40
+ def initialize(entries)
41
+ @entries = entries
42
+ end
43
+
44
+ def to_hexp
45
+ H[:ul,
46
+ @entries.map do |entry|
47
+ H[:li, entry]
48
+ end
49
+ ]
50
+ end
51
+ end
52
+
53
+ class AddEntryForm
54
+ def to_hexp
55
+ H[:form, {method: 'POST', action: '/'}, [
56
+ H[:input, {type: 'text', name: 'entry_description'}],
57
+ H[:input, {type: 'submit'}, "Add"]
58
+ ]
59
+ ]
60
+ end
61
+ end
62
+
63
+ class Layout
64
+ include Hexp
65
+
66
+ def initialize(*contents)
67
+ @contents = contents
68
+ p @contents
69
+ end
70
+
71
+ def to_hexp
72
+ H[:html, [
73
+ H[:head],
74
+ H[:body, @contents ]
75
+ ]
76
+ ]
77
+ end
78
+ end
79
+
80
+ class ListPage
81
+ include Hexp
82
+ TITLE = 'Todo List'
83
+
84
+ def to_hexp
85
+ hexp = Layout.new(
86
+ EntryList.new(EntryStore.all),
87
+ AddEntryForm.new
88
+ )
89
+ hexp = add_title(hexp)
90
+ hexp = wrap_entry_forms(hexp)
91
+ end
92
+
93
+ def title
94
+ H[:title, TITLE]
95
+ end
96
+
97
+ def add_title(tree)
98
+ tree.rewrite do |node|
99
+ if node.tag == :head
100
+ H[:head, node.attributes, node.children + [title]]
101
+ end
102
+ end
103
+ end
104
+
105
+ def wrap_entry_forms(tree)
106
+ tree.rewrite do |node|
107
+ if node.class? 'entry'
108
+ H[:form, {method: 'POST', action: "/#{node.attr('data-id')}"}, [
109
+ node,
110
+ H[:input, type: 'hidden', name: '_method', value: 'DELETE'],
111
+ H[:input, type: 'submit', value: '-']
112
+ ]
113
+ ]
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ get '/' do
120
+ ListPage.new.to_html
121
+ end
122
+
123
+ post '/' do
124
+ @entry = Entry.new(description: params['entry_description'])
125
+ EntryStore.store(@entry)
126
+
127
+ ListPage.new.to_html
128
+ end
129
+
130
+ delete '/:id' do
131
+ EntryStore.delete params[:id].to_i
132
+
133
+ ListPage.new.to_html
134
+ end
135
+
136
+ get '/handlebars' do
137
+ Entry.handlebars.to_html
138
+ end
@@ -0,0 +1,64 @@
1
+ $:.unshift File.expand_path '../../lib', __FILE__
2
+
3
+ require 'hexp'
4
+ require 'virtus'
5
+
6
+ class Widget
7
+ include Hexp
8
+ include Virtus
9
+
10
+ attribute :tag, Symbol, default: :div
11
+ attribute :data, Hash
12
+
13
+ def to_hexp
14
+ H[tag, html_attributes, widget]
15
+ end
16
+
17
+ def html_attributes
18
+ attrs = Hash[data.map {|k,v| ["data-#{k}", v]}].merge(class: [self.class.widget_name, 'widget']*' ' )
19
+ if attribute_set.any?{|attribute| attribute.name == :id} && self.id
20
+ attrs['data-id'] = self.id
21
+ end
22
+ attrs
23
+ end
24
+
25
+ def self.widget_name
26
+ self.name.downcase
27
+ end
28
+
29
+ def self.handlebars
30
+ H[:script, {:type => 'application/x-handlebars-template', :id => widget_name+'-template'},
31
+ self.new(
32
+ Hash[
33
+ attribute_set
34
+ .reject {|attribute| [:data, :tag].include?(attribute.name) }
35
+ .map {|attribute| [attribute.name, "{{#{attribute.name}}}"] }
36
+ ]
37
+ )
38
+ ]
39
+ end
40
+ end
41
+
42
+ def Widget(tag)
43
+ Class.new(Widget) do
44
+ attribute :tag, Symbol, default: tag
45
+ end
46
+ end
47
+
48
+
49
+ # class Entry < Widget(:p)
50
+ # attribute :name, String
51
+ # attribute :date, String # !> assigned but unused variable - type
52
+
53
+ # def widget
54
+ # [
55
+ # [:span, name],
56
+ # [:span, date]
57
+ # ]
58
+ # end
59
+ # end
60
+
61
+ # puts Entry.new(name: 'foo', date: '2013-06-17', data: {id: 17}).to_html
62
+ # puts
63
+ # puts Entry.handlebars.to_html
64
+ # !> instance variable @default not initialized
@@ -10,13 +10,18 @@ Gem::Specification.new do |gem|
10
10
  gem.description = 'HTML expressions'
11
11
  gem.summary = gem.description
12
12
  gem.homepage = 'https://github.com/plexus/hexp'
13
+ gem.license = 'MIT'
13
14
 
14
15
  gem.require_paths = %w[lib]
15
16
  gem.files = `git ls-files`.split($/)
16
17
  gem.test_files = `git ls-files -- spec`.split($/)
17
18
  gem.extra_rdoc_files = %w[README.md]
18
19
 
19
- gem.add_dependency 'nokogiri', '~> 1.5.9'
20
- gem.add_dependency 'ice_nine', '~> 0.7.0'
21
- gem.add_dependency 'equalizer', '~> 0.0.5'
20
+ gem.add_runtime_dependency 'sass' , '~> 3.2'
21
+ gem.add_runtime_dependency 'nokogiri' , '~> 1.6'
22
+ gem.add_runtime_dependency 'ice_nine' , '~> 0.8'
23
+ gem.add_runtime_dependency 'equalizer' , '~> 0.0'
24
+
25
+ gem.add_development_dependency 'rake', '~> 10.1'
26
+ gem.add_development_dependency 'rspec', '~> 2.14'
22
27
  end
@@ -1,27 +1,128 @@
1
1
  require 'delegate'
2
2
  require 'forwardable'
3
3
 
4
- require 'nokogiri'
4
+ require 'nokogiri' # TODO => replace with Builder
5
+ require 'sass'
5
6
  require 'ice_nine'
6
7
  require 'equalizer'
7
8
 
8
9
  module Hexp
10
+ # Inject the Hexp::DSL module into classes that include Hexp
11
+ #
12
+ # @param klazz [Class] The class that included Hexp
13
+ #
14
+ # @return [Class]
15
+ # @api private
16
+ #
17
+ def self.included(klazz)
18
+ klazz.send(:include, Hexp::DSL)
19
+ end
20
+
21
+ # Deep freeze an object
22
+ #
23
+ # Delegates to IceNine
24
+ #
25
+ # @param args [Array] arguments to pass on
26
+ # @return Object
27
+ # @api private
28
+ #
9
29
  def self.deep_freeze(*args)
10
30
  IceNine.deep_freeze(*args)
11
31
  end
32
+
33
+ # Variant of ::Array with slightly modified semantics
34
+ #
35
+ # Array() is often used to wrap a value in an Array, unless it's already
36
+ # an array. However if your object implements #to_a, then Array() will use
37
+ # that value. Because of this objects that aren't Array-like will get
38
+ # converted as well, such as Struct objects.
39
+ #
40
+ # This implementation relies on #to_ary, which signals that the Object is
41
+ # a drop-in replacement for an actual Array.
42
+ #
43
+ # @param arg [Object]
44
+ # @return [Array]
45
+ # @api private
46
+ #
47
+ def self.Array(arg)
48
+ if arg.respond_to? :to_ary
49
+ arg.to_ary
50
+ else
51
+ [ arg ]
52
+ end
53
+ end
54
+
55
+ # Parse HTML to Hexp
56
+ #
57
+ # The input have a single root element. If there are multiple only the first
58
+ # will be converted. If there is no root element (e.g. an empty document, or
59
+ # only a DTD or comment) then an error is raised
60
+ #
61
+ # @example
62
+ # Hexp.parse('<div>hello</div>') #=> H[:div, "hello"]
63
+ #
64
+ # @param html [String] A HTML document
65
+ # @return [Hexp::Node]
66
+ # @api public
67
+ #
68
+ def self.parse(html)
69
+ root = Nokogiri(html).root
70
+ raise Hexp::ParseError, "Failed to parse HTML : no document root" if root.nil?
71
+ Hexp::Nokogiri::Reader.new.call(root)
72
+ end
73
+
74
+ # Use builder syntax to create a Hexp
75
+ #
76
+ # (see Hexp::Builder)
77
+ #
78
+ # @example
79
+ # list = Hexp.build do
80
+ # ul do
81
+ # 3.times do |i|
82
+ # li i.to_s
83
+ # end
84
+ # end
85
+ # end
86
+ #
87
+ # @param args [Array]
88
+ # @return [Hexp::Builder]
89
+ # @api public
90
+ #
91
+ def self.build(*args, &block)
92
+ Hexp::Builder.new(*args, &block)
93
+ end
94
+
12
95
  end
13
96
 
14
97
  require 'hexp/version'
98
+ require 'hexp/dsl'
99
+
100
+
101
+ require 'hexp/node/attributes'
102
+ require 'hexp/node/children'
15
103
 
16
104
  require 'hexp/node'
17
105
  require 'hexp/node/normalize'
18
106
  require 'hexp/node/domize'
19
107
  require 'hexp/node/pp'
108
+ require 'hexp/node/rewriter'
109
+ require 'hexp/node/selector'
110
+ require 'hexp/node/css_selection'
20
111
 
21
112
  require 'hexp/text_node'
22
113
  require 'hexp/list'
23
114
  require 'hexp/dom'
24
115
 
25
- require 'hexp/nokogiri/equality'
116
+ require 'hexp/css_selector'
117
+ require 'hexp/css_selector/sass_parser'
118
+ require 'hexp/css_selector/parser'
119
+
120
+ require 'hexp/errors'
121
+
122
+ require 'hexp/nokogiri/equality' # TODO => replace this with equivalent-xml
123
+ require 'hexp/nokogiri/reader'
124
+ require 'hexp/sass/selector_parser'
26
125
 
27
126
  require 'hexp/h'
127
+
128
+ require 'hexp/builder'