hexp 0.3.3 → 0.4.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +8 -3
  4. data/Changelog.md +32 -1
  5. data/Gemfile +0 -5
  6. data/Rakefile +21 -15
  7. data/hexp.gemspec +6 -4
  8. data/lib/hexp.rb +5 -14
  9. data/lib/hexp/core_ext/nil.rb +9 -0
  10. data/lib/hexp/css_selector.rb +2 -1
  11. data/lib/hexp/h.rb +9 -1
  12. data/lib/hexp/list.rb +6 -1
  13. data/lib/hexp/node.rb +9 -2
  14. data/lib/hexp/node/attributes.rb +1 -1
  15. data/lib/hexp/node/children.rb +4 -2
  16. data/lib/hexp/node/normalize.rb +24 -28
  17. data/lib/hexp/nokogiri/reader.rb +1 -1
  18. data/lib/hexp/text_node.rb +6 -0
  19. data/lib/hexp/unparser.rb +73 -0
  20. data/lib/hexp/version.rb +1 -1
  21. data/spec/integration/literal_syntax_spec.rb +2 -2
  22. data/spec/shared_helper.rb +1 -1
  23. data/spec/unit/hexp/builder_spec.rb +2 -2
  24. data/spec/unit/hexp/css_selector/attribute_spec.rb +24 -24
  25. data/spec/unit/hexp/css_selector/class_spec.rb +3 -3
  26. data/spec/unit/hexp/css_selector/comma_sequence_spec.rb +1 -1
  27. data/spec/unit/hexp/css_selector/element_spec.rb +2 -2
  28. data/spec/unit/hexp/css_selector/simple_sequence_spec.rb +8 -8
  29. data/spec/unit/hexp/css_selector/universal_spec.rb +1 -1
  30. data/spec/unit/hexp/dsl_spec.rb +3 -3
  31. data/spec/unit/hexp/h_spec.rb +2 -2
  32. data/spec/unit/hexp/node/attributes_spec.rb +4 -4
  33. data/spec/unit/hexp/node/class_spec.rb +7 -7
  34. data/spec/unit/hexp/node/normalize_spec.rb +14 -6
  35. data/spec/unit/hexp/node/rewrite_spec.rb +1 -1
  36. data/spec/unit/hexp/node/text_spec.rb +1 -1
  37. data/spec/unit/hexp/node/to_dom_spec.rb +1 -1
  38. data/spec/unit/hexp/node/to_html_spec.rb +17 -1
  39. data/spec/unit/hexp/nokogiri/equality_spec.rb +6 -6
  40. data/spec/unit/hexp/text_node_spec.rb +2 -2
  41. metadata +62 -34
  42. data/Gemfile.devtools +0 -55
  43. data/Gemfile.lock +0 -186
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cd5812a1b6d435382838232f4cec4d2fdb2162d2
4
- data.tar.gz: eeff9a4c48a878863a8bc020d4ae37d0dd01d8ee
3
+ metadata.gz: 7da79589fadbf74a0c0fa19ad9f409ca9f0dc855
4
+ data.tar.gz: 65e4754146881f1d5a23df35ae4b4f7a964ad886
5
5
  SHA512:
6
- metadata.gz: dbfde6d4249a6726cc2c492a26b14e2ad79b253844fb813eae65a1fa8eeedac4106a8dcc16155fcbe87312124448e6f6fbfb6078f151cb26eeb01dfbcbe659f1
7
- data.tar.gz: 8ad112e34c87099bf467b5fa1e5a6ff5795a8d4494c24f6c2cd8074b8edb87b57dfc3bccc176fc85d20ab0bcac1b4366d0b942acf922f98e348f5c35631e8826
6
+ metadata.gz: 82271f2b2f56f71a71b2897b3668c7dbb2b8283a73df551d4ad6ae790e2cec806fb6901e6253c300f5bf1d2b7a4244bdbb0fde09cf3faf414df00d3fd6d1cc84
7
+ data.tar.gz: fd56366470868b9c0df687b945e9da1ce17e2d94fbf81b4be38a454810ca684ba1c1d5bcfcee1f28d8fe80b45320b9072bea9b8ed1269b9e93bf1b3dce0272e0
data/.gitignore CHANGED
@@ -23,3 +23,4 @@ measurements/report.txt
23
23
 
24
24
  # Automatic Ruby switching
25
25
  .ruby-version
26
+ Gemfile.lock
@@ -1,13 +1,18 @@
1
+ # Currently fails on JRuby, partly due to Nokogiri behaving differently,
2
+ # and partly for some other reason I'm not sure about regarding
3
+ # Hexp::Node::Normalize
1
4
  language: ruby
2
5
  script: "bundle exec rspec"
3
6
  rvm:
4
- - 1.9.2
5
7
  - 1.9.3
6
8
  - 2.0.0
7
- - 2.1.0
9
+ - 2.1.1
10
+ - 2.1.2
11
+ - jruby
12
+ - jruby-head
8
13
  - ruby-head
9
14
  matrix:
10
15
  allow_failures:
11
- - rvm: jruby-19mode
16
+ - rvm: jruby
12
17
  - rvm: ruby-head
13
18
  - rvm: jruby-head
@@ -1,5 +1,36 @@
1
1
  ### Development
2
- [full changelog](http://github.com/plexus/hexp/compare/v0.2.0...master)
2
+
3
+ [full diff](http://github.com/plexus/hexp/compare/v0.3.3...master)
4
+
5
+ ### v0.4.0.beta1
6
+
7
+ [full diff](http://github.com/plexus/hexp/compare/v0.3.3...v0.4.0.beta1)
8
+
9
+ * Do our own HTML unparsing, instead of going through Nokogiri,
10
+ causing a big speed improvement.
11
+ * Make H[] notation more lenient
12
+ * Make array around list of children optional
13
+ `H[:p, [H[:span, 'foo'], ' ', H[:span, 'bar']]]` =>
14
+ `H[:p, H[:span, 'foo'], ' ', H[:span, 'bar']]`
15
+ * Allow creating node lists without a wrapping node, e.g.
16
+ `H[H[:span, 'foo'], ' ', H[:span, 'bar']]`
17
+ * Make Hexp::List and Hexp::TextNode respond to to_html
18
+ * Add Hexp::Node#tag? as a complement to Hexp::Node#text?
19
+
20
+ ### v0.3.3
21
+
22
+ [full diff](http://github.com/plexus/hexp/compare/v0.3.0...v0.3.3)
23
+
24
+ * Bugfix regarding string values in attribute CSS selectors
25
+ * Update dependencies
26
+
27
+ ### v0.3.0
28
+
29
+ [full diff](http://github.com/plexus/hexp/compare/v0.2.0...v0.3.0)
30
+
31
+ * Improved CSS selector support
32
+ * Handle CDATA sections when parsing through Nokogiri
33
+ * Improved documentation
3
34
 
4
35
  ### v0.2.0
5
36
 
data/Gemfile CHANGED
@@ -1,8 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- group :development do
4
- gem 'devtools', github: 'rom-rb/devtools'
5
- eval File.read('Gemfile.devtools')
6
- end
7
-
8
3
  gemspec
data/Rakefile CHANGED
@@ -1,22 +1,19 @@
1
- require 'devtools'
2
1
  require 'rubygems/package_task'
3
2
 
4
- Devtools.init_rake_tasks
5
-
6
3
  # Redefine rake:ci:metrics to disable rubocop, will tackle that laundry list
7
4
  # some other time
8
- namespace :ci do
9
- desc 'Run metrics (except mutant, rubocop) and spec'
10
- task travis: %w[
11
- metrics:coverage
12
- spec:integration
13
- metrics:yardstick:verify
14
- metrics:flog
15
- metrics:flay
16
- ]
17
- # metrics:reek
18
- # metrics:rubocop
19
- end
5
+ # namespace :ci do
6
+ # desc 'Run metrics (except mutant, rubocop) and spec'
7
+ # task travis: %w[
8
+ # metrics:coverage
9
+ # spec:integration
10
+ # metrics:yardstick:verify
11
+ # metrics:flog
12
+ # metrics:flay
13
+ # ]
14
+ # # metrics:reek
15
+ # # metrics:rubocop
16
+ # end
20
17
 
21
18
 
22
19
  spec = Gem::Specification.load(File.expand_path('../hexp.gemspec', __FILE__))
@@ -45,3 +42,12 @@ task :doc2gh do
45
42
  sh "git push origin gh-pages"
46
43
  sh "git co master"
47
44
  end
45
+
46
+ require 'mutant'
47
+ task :default => :mutant
48
+
49
+ task :mutant do
50
+ pattern = ENV.fetch('PATTERN', 'Hexp*')
51
+ result = Mutant::CLI.run(%w[-Ilib -rhexp --use rspec --score 100] + [pattern])
52
+ fail unless result == Mutant::CLI::EXIT_SUCCESS
53
+ end
@@ -19,10 +19,12 @@ Gem::Specification.new do |gem|
19
19
 
20
20
  gem.add_runtime_dependency 'sass', '~> 3.2.19'
21
21
  gem.add_runtime_dependency 'nokogiri', '~> 1.6'
22
- gem.add_runtime_dependency 'ice_nine', '~> 0.9'
22
+ gem.add_runtime_dependency 'adamantium', '~> 0.2'
23
23
  gem.add_runtime_dependency 'equalizer', '~> 0.0'
24
24
 
25
- gem.add_development_dependency 'rake', '~> 10.1'
26
- gem.add_development_dependency 'rspec', '~> 2.14'
27
- gem.add_development_dependency 'benchmark_suite', '~> 1.0'
25
+ gem.add_development_dependency 'rake'
26
+ gem.add_development_dependency 'rspec'
27
+ gem.add_development_dependency 'benchmark_suite'
28
+ gem.add_development_dependency 'mutant-rspec'
29
+ gem.add_development_dependency 'rspec-its'
28
30
  end
@@ -1,12 +1,15 @@
1
1
  require 'delegate'
2
2
  require 'forwardable'
3
+ require 'pathname'
3
4
 
4
5
  require 'nokogiri' # TODO => replace with Builder
5
6
  require 'sass'
6
- require 'ice_nine'
7
+ require 'adamantium'
7
8
  require 'equalizer'
8
9
 
9
10
  module Hexp
11
+ ROOT = Pathname(__FILE__).dirname.parent
12
+
10
13
  # Inject the Hexp::DSL module into classes that include Hexp
11
14
  #
12
15
  # @param [Class] klazz
@@ -19,19 +22,6 @@ module Hexp
19
22
  klazz.send(:include, Hexp::DSL)
20
23
  end
21
24
 
22
- # Deep freeze an object
23
- #
24
- # Delegates to IceNine
25
- #
26
- # @param [Array] args
27
- # arguments to pass on
28
- # @return [Object]
29
- #
30
- # @api private
31
- def self.deep_freeze(*args)
32
- IceNine.deep_freeze(*args)
33
- end
34
-
35
25
  # Variant of ::Array with slightly modified semantics
36
26
  #
37
27
  # `Array()` is often used to wrap a value in an Array, unless it's already
@@ -134,3 +124,4 @@ require 'hexp/sass/selector_parser'
134
124
  require 'hexp/h'
135
125
 
136
126
  require 'hexp/builder'
127
+ require 'hexp/unparser'
@@ -0,0 +1,9 @@
1
+ # This file is *not* loaded by default. You have to explicitly require 'hexp/core_ext/nil'
2
+
3
+ class NilClass
4
+ HEXP_NIL = Hexp::TextNode.new('')
5
+
6
+ def to_hexp
7
+ HEXP_NIL
8
+ end
9
+ end
@@ -4,6 +4,7 @@ module Hexp
4
4
  #
5
5
  module Members
6
6
  include Equalizer.new(:members)
7
+ include Adamantium
7
8
 
8
9
  extend Forwardable
9
10
  def_delegator :@members, :empty?
@@ -19,7 +20,7 @@ module Hexp
19
20
  #
20
21
  # @api private
21
22
  def initialize(members)
22
- @members = Hexp.deep_freeze(members)
23
+ @members = members
23
24
  end
24
25
 
25
26
  # Create a class level collection constructor
@@ -1,5 +1,13 @@
1
1
  if defined?(::H) && ::H != Hexp::Node
2
2
  $stderr.puts "WARN: H is already defined, Hexp H[] shorthand not available"
3
3
  else
4
- H=Hexp::Node
4
+ module H
5
+ def self.[](*args)
6
+ if args.first.is_a? Symbol
7
+ Hexp::Node[*args]
8
+ else
9
+ Hexp::List[*args]
10
+ end
11
+ end
12
+ end
5
13
  end
@@ -2,6 +2,7 @@ module Hexp
2
2
  # A list of nodes
3
3
  #
4
4
  class List < DelegateClass(Array)
5
+ include Adamantium
5
6
 
6
7
  # Create new Hexp::List
7
8
  #
@@ -13,7 +14,7 @@ module Hexp
13
14
  #
14
15
  # @api public
15
16
  def initialize(nodes)
16
- super nodes.to_ary.freeze
17
+ super nodes.to_ary.map(&Node::Normalize.method(:coerce_node)).freeze
17
18
  end
18
19
 
19
20
  # Convenience constructor
@@ -82,5 +83,9 @@ module Hexp
82
83
  def eql?(other)
83
84
  self == other && self.class == other.class
84
85
  end
86
+
87
+ def to_html
88
+ each_with_object('') {|n,s| s << n.to_html}
89
+ end
85
90
  end
86
91
  end
@@ -53,11 +53,14 @@ module Hexp
53
53
  #
54
54
  class Node
55
55
  include Equalizer.new(:tag, :attributes, :children)
56
+ include Adamantium
56
57
  extend Forwardable
57
58
 
58
59
  include Hexp::Node::Attributes
59
60
  include Hexp::Node::Children
60
61
 
62
+ memoize :class_list
63
+
61
64
  # The HTML tag of this node
62
65
  #
63
66
  # @example
@@ -146,7 +149,7 @@ module Hexp
146
149
  # @api public
147
150
  #
148
151
  def to_html(options = {})
149
- to_dom(options).to_html
152
+ Unparser.new(options).call(self)
150
153
  end
151
154
 
152
155
  # Convert this node into a Nokogiri Document
@@ -200,6 +203,10 @@ module Hexp
200
203
  false
201
204
  end
202
205
 
206
+ def tag?(tag)
207
+ self.tag == tag
208
+ end
209
+
203
210
  # Return a new node, with a different tag
204
211
  #
205
212
  # @example
@@ -318,7 +325,7 @@ module Hexp
318
325
  # @api private
319
326
  #
320
327
  def inspect_name
321
- if defined?(H) && H == self
328
+ if defined?(H)
322
329
  'H'
323
330
  else
324
331
  self.name
@@ -88,7 +88,7 @@ module Hexp
88
88
  #
89
89
  # @api public
90
90
  def class_list
91
- @class_list ||= (attr('class') || '').split(' ').freeze
91
+ (attr('class') || '').split(' ')
92
92
  end
93
93
 
94
94
  # Remove a CSS class from this element
@@ -72,9 +72,10 @@ module Hexp
72
72
  # @return [Hexp::Node]
73
73
  #
74
74
  # @api public
75
- def set_children(new_children)
76
- H[tag, attributes, new_children]
75
+ def content(*args)
76
+ H[tag, attributes, *args]
77
77
  end
78
+ alias set_children content
78
79
 
79
80
  # Perform an action on each child node, and replace the node with the result
80
81
  #
@@ -94,6 +95,7 @@ module Hexp
94
95
  return to_enum(:map_children) unless block_given?
95
96
  H[tag, attributes, children.map(&blk)]
96
97
  end
98
+
97
99
  end
98
100
  end
99
101
  end
@@ -11,8 +11,8 @@ module Hexp
11
11
  # Hexp::Node::Normalize.new([:p, {class:'foo'}])
12
12
  #
13
13
  # @api public
14
- def initialize(node)
15
- @raw = node
14
+ def initialize(args)
15
+ @raw = args
16
16
  end
17
17
 
18
18
  # Normalize to strict hexp nodes, cfr SPEC.md for details
@@ -56,14 +56,10 @@ module Hexp
56
56
  #
57
57
  # @api private
58
58
  def children
59
- last = @raw.last
60
- if last.respond_to? :to_ary
61
- last.to_ary
62
- elsif @raw.count < 2 || last.instance_of?(Hash)
63
- []
64
- else
65
- [last]
66
- end
59
+ children = @raw.drop(1)
60
+ children = children.drop(1) if children.first.instance_of?(Hash)
61
+ children = children.first.to_ary if children.first.respond_to?(:to_ary)
62
+ children
67
63
  end
68
64
 
69
65
  # Normalize the third element of a hexp node, the list of children
@@ -72,24 +68,24 @@ module Hexp
72
68
  #
73
69
  # @api private
74
70
  def normalized_children
75
- Hexp::List[*
76
- children.map do |child|
77
- case child
78
- when Hexp::Node, Hexp::TextNode
79
- child
80
- when String
81
- Hexp::TextNode.new(child)
82
- when ->(ch) { ch.respond_to? :to_hexp }
83
- response = child.to_hexp
84
- raise FormatError, "to_hexp must return a Hexp::Node, got #{response.inspect}" unless response.instance_of?(Hexp::Node) || response.instance_of?(Hexp::TextNode)
85
- response
86
- when Array
87
- Hexp::Node[*child.map(&:freeze)]
88
- else
89
- raise FormatError, "Invalid value in Hexp literal : #{child.inspect} (#{child.class}) does not implement #to_hexp ; #{children.inspect}"
90
- end
91
- end
92
- ]
71
+ Hexp::List[* children ]
72
+ end
73
+
74
+ def self.coerce_node(node)
75
+ case node
76
+ when Hexp::Node, Hexp::TextNode
77
+ node
78
+ when String
79
+ Hexp::TextNode.new(node)
80
+ when ->(ch) { ch.respond_to? :to_hexp }
81
+ response = node.to_hexp
82
+ raise FormatError, "to_hexp must return a Hexp::Node, got #{response.inspect}" unless response.instance_of?(Hexp::Node) || response.instance_of?(Hexp::TextNode)
83
+ response
84
+ when Array
85
+ Hexp::Node[*node]
86
+ else
87
+ raise FormatError, "Invalid value in Hexp literal : #{node.inspect} (#{node.class}) does not implement #to_hexp"
88
+ end
93
89
  end
94
90
  end
95
91
 
@@ -20,7 +20,7 @@ module Hexp
20
20
  end
21
21
 
22
22
  recurse = ->(next_node) { call(next_node) }
23
- H[node.name.to_sym, attrs, node.children.map(&recurse)]
23
+ H[node.name.to_sym, attrs || {}, node.children.map(&recurse)]
24
24
  end
25
25
  end
26
26
  end
@@ -7,6 +7,8 @@ module Hexp
7
7
  # converted to `TextNode` instances, so there is usually no reason to instantiate
8
8
  # these yourself.
9
9
  class TextNode < DelegateClass(String)
10
+ include Adamantium
11
+
10
12
  # Inspect the TextNode
11
13
  #
12
14
  # This delegates to the underlying String, making it
@@ -142,5 +144,9 @@ module Hexp
142
144
  def select(&block)
143
145
  Node::Selection.new(self, block)
144
146
  end
147
+
148
+ def to_html(opts = {})
149
+ Unparser.new(opts).call(self)
150
+ end
145
151
  end
146
152
  end
@@ -0,0 +1,73 @@
1
+ module Hexp
2
+ class Unparser
3
+ APOS = ?'.freeze
4
+ QUOT = ?".freeze
5
+ LT = '<'.freeze
6
+ GT = '>'.freeze
7
+ SPACE = ' '.freeze
8
+ EQ = '='.freeze
9
+ AMP = '&'.freeze
10
+ FSLASH = '/'.freeze
11
+
12
+ E_AMP = '&amp;'.freeze
13
+ E_APOS = '&#x27;'.freeze
14
+ E_QUOT = '&quot;'.freeze
15
+ E_LT = '&lt;'.freeze
16
+ E_GT = '&gt;'.freeze
17
+
18
+ ESCAPE_ATTR_APOS = {AMP => E_AMP, APOS => E_APOS}
19
+ ESCAPE_ATTR_QUOT = {AMP => E_AMP, QUOT => E_QUOT}
20
+ ESCAPE_TEXT = {AMP => E_AMP, APOS => E_APOS, QUOT => E_QUOT, LT => E_LT, GT => E_GT}
21
+
22
+ ESCAPE_ATTR_APOS_REGEX = Regexp.new("[#{ESCAPE_ATTR_APOS.keys.join}]")
23
+ ESCAPE_ATTR_QUOT_REGEX = Regexp.new("[#{ESCAPE_ATTR_QUOT.keys.join}]")
24
+ ESCAPE_TEXT_REGEX = Regexp.new("[#{ESCAPE_TEXT.keys.join}]")
25
+
26
+ DEFAULT_OPTIONS = {
27
+ encoding: Encoding.default_external
28
+ }
29
+
30
+ def initialize(options)
31
+ @options = DEFAULT_OPTIONS.merge(options)
32
+ end
33
+
34
+ def call(node)
35
+ @buffer = String.new.force_encoding(@options[:encoding])
36
+ add_node(node)
37
+ @buffer.freeze
38
+ end
39
+
40
+ private
41
+
42
+ def add_node(node)
43
+ if node.text?
44
+ @buffer << escape_text(node)
45
+ else
46
+ add_tag(node.tag, node.attributes, node.children)
47
+ end
48
+ end
49
+
50
+ def add_tag(tag, attrs, children)
51
+ @buffer << LT << tag.to_s
52
+ unless attrs.empty?
53
+ attrs.each {|k,v| add_attr(k,v)}
54
+ end
55
+ @buffer << GT
56
+ children.each(&method(:add_node))
57
+ @buffer << LT << FSLASH << tag.to_s << GT
58
+ end
59
+
60
+ def add_attr(key, value)
61
+ @buffer << SPACE << key << EQ
62
+ add_attr_value(value)
63
+ end
64
+
65
+ def add_attr_value(value)
66
+ @buffer << APOS << value.gsub(ESCAPE_ATTR_APOS_REGEX, ESCAPE_ATTR_APOS) << APOS
67
+ end
68
+
69
+ def escape_text(text)
70
+ text.gsub(ESCAPE_TEXT_REGEX, ESCAPE_TEXT)
71
+ end
72
+ end
73
+ end