hexp 0.0.1 → 0.2.0

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.
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'