├── .gitignore ├── .travis.yml ├── .yardopts ├── Changelog.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bench └── node │ └── rewrite_bench.rb ├── code_of_conduct.md ├── config ├── devtools.yml ├── flay.yml ├── flog.yml ├── mutant.yml ├── reek.yml ├── rubocop.yml └── yardstick.yml ├── examples ├── filter.rb ├── from_nokogiri.rb ├── hexp_rails.rb ├── selector_rewriter_chaining.rb ├── todo.rb └── widget.rb ├── hexp.gemspec ├── lib ├── hexp-rails.rb ├── hexp.rb └── hexp │ ├── builder.rb │ ├── core_ext │ └── nil.rb │ ├── css_selector.rb │ ├── css_selector │ └── parser.rb │ ├── dom.rb │ ├── dsl.rb │ ├── errors.rb │ ├── h.rb │ ├── list.rb │ ├── mutable_tree_walk.rb │ ├── node.rb │ ├── node │ ├── attributes.rb │ ├── children.rb │ ├── css_selection.rb │ ├── domize.rb │ ├── normalize.rb │ ├── pp.rb │ ├── rewriter.rb │ └── selection.rb │ ├── nokogiri │ ├── equality.rb │ └── reader.rb │ ├── sass │ └── selector_parser.rb │ ├── text_node.rb │ ├── unparser.rb │ └── version.rb └── spec ├── integration └── literal_syntax_spec.rb ├── spec_helper.rb └── unit ├── hexp ├── builder_spec.rb ├── css_selector │ ├── attribute_spec.rb │ ├── class_spec.rb │ ├── comma_sequence_spec.rb │ ├── element_spec.rb │ ├── parser_spec.rb │ ├── simple_sequence_spec.rb │ └── universal_spec.rb ├── dsl_spec.rb ├── h_spec.rb ├── list_spec.rb ├── mutable_tree_walk_spec.rb ├── node │ ├── attr_spec.rb │ ├── attributes_spec.rb │ ├── children_spec.rb │ ├── class_spec.rb │ ├── css_selection_spec.rb │ ├── domize_spec.rb │ ├── normalize_spec.rb │ ├── pp_spec.rb │ ├── rewrite_spec.rb │ ├── selection_spec.rb │ ├── text_spec.rb │ ├── to_dom_spec.rb │ ├── to_hexp_spec.rb │ └── to_html_spec.rb ├── nokogiri │ ├── equality_spec.rb │ └── reader_spec.rb ├── parse_spec.rb ├── text_node_spec.rb └── unparser_spec.rb └── hexp_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | coverage 6 | InstalledFiles 7 | lib/bundler/man 8 | pkg 9 | rdoc 10 | spec/reports 11 | test/tmp 12 | test/version_tmp 13 | tmp 14 | vendor 15 | 16 | # YARD artifacts 17 | .yardoc 18 | _yardoc 19 | doc/ 20 | 21 | #yardstick report 22 | measurements/report.txt 23 | 24 | # Automatic Ruby switching 25 | .ruby-version 26 | Gemfile.lock 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | script: bundle exec rake $TASK 4 | 5 | sudo: false 6 | 7 | rvm: 8 | - 1.9.3 9 | - 2.0 10 | - 2.1 11 | - 2.2 12 | - jruby 13 | - jruby-head 14 | - rbx 15 | - ruby-head 16 | 17 | env: 18 | - TASK=rspec 19 | - TASK=mutant 20 | 21 | matrix: 22 | # Jruby should be taken out of this list. It fails now because of a 23 | # subtle Nokogiri incompatibility 24 | allow_failures: 25 | - rvm: jruby 26 | - rvm: ruby-head 27 | - rvm: jruby-head 28 | - rvm: rbx 29 | - env: TASK=mutant 30 | 31 | # Only run mutant on 2.2 32 | exclude: 33 | - rvm: 1.9.3 34 | env: TASK=mutant 35 | - rvm: 2.0 36 | env: TASK=mutant 37 | - rvm: 2.1 38 | env: TASK=mutant 39 | - rvm: jruby 40 | env: TASK=mutant 41 | - rvm: jruby-head 42 | env: TASK=mutant 43 | - rvm: rbx 44 | env: TASK=mutant 45 | - rvm: ruby-head 46 | env: TASK=mutant 47 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private -q - LICENSE 2 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | ### Development 2 | 3 | [full diff](http://github.com/plexus/hexp/compare/v0.4.6...master) 4 | 5 | ### v0.4.6 6 | 7 | * Bugfix: make sure the Node constructors correctly calls the 8 | constructor defined by Concord. This bug was being triggered by 9 | asset_packer. Not sure how or why, but this fixes it. 10 | 11 | ### v0.4.5 12 | 13 | * Bugfix: don't do entity escaping inside :script tags, also not when 14 | there's more than one text node as a child element 15 | 16 | ### v0.4.4 17 | 18 | * Drop the dependency on SASS, use Nokogiri instead for parsing CSS 19 | selectors 20 | 21 | ### v0.4.3 22 | 23 | Performance improvements 24 | 25 | * Introduce MutableTreeWalk to speed up css selection 26 | * Drop Adamantium. This means we have less of a guarantee of deep 27 | immutability, but it does speed things up 28 | * Prevent type coercion from happening if inputs are already valid 29 | * Raise an exception when a node's tag is not a Symbol 30 | 31 | ### v0.4.2 32 | 33 | * Added Hexp::List#append 34 | * set_attr now simply replaces the full attribute hash, use 35 | merge_attr for "smart" behavior. % is now an alias of merge_attr, 36 | not set_attr 37 | * Make the unparser aware of HTML "void" tags (tags that should not 38 | have a closing tag) 39 | 40 | ### v0.4.1 41 | 42 | * Make Hexp::List#+ return a Hexp::List 43 | * Add Hexp::Node#append as a convenient API for adding child nodes 44 | * Make Unparser Adamantium-immutable so instances can be included in 45 | Adamantiumized objects 46 | * Skip escaping inside ` 71 | if options[:no_escape].include?(tag) && children.all?(&:text?) 72 | children.each {|node| buffer << node } 73 | else 74 | children.each {|node| add_node(buffer, node) } 75 | end 76 | end 77 | 78 | def add_attr(buffer, key, value) 79 | buffer << SPACE << key << EQ 80 | add_attr_value(buffer, value) 81 | end 82 | 83 | def add_attr_value(buffer, value) 84 | buffer << APOS << value.gsub(ESCAPE_ATTR_APOS_REGEX, ESCAPE_ATTR_APOS) << APOS 85 | end 86 | 87 | def escape_text(text) 88 | text.gsub(ESCAPE_TEXT_REGEX, ESCAPE_TEXT) 89 | end 90 | 91 | def void?(tag) 92 | options[:void].include?(tag) 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/hexp/version.rb: -------------------------------------------------------------------------------- 1 | module Hexp 2 | VERSION = '0.4.6' 3 | end 4 | -------------------------------------------------------------------------------- /spec/integration/literal_syntax_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Constructing literal hexps' do 4 | it do 5 | expect(H[:p]).to eql Hexp::Node.new(:p, {}, []) 6 | end 7 | 8 | it do 9 | expect(H[:p, "foo"]).to eql Hexp::Node.new(:p, {}, ["foo"]) 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'hexp' 4 | require 'rspec/its' 5 | 6 | RSpec::Matchers.define :dom_eq do |other_dom| 7 | match do |dom| 8 | Hexp::Nokogiri::Equality.new(dom, other_dom).call 9 | end 10 | end 11 | 12 | RSpec.configure do |rspec| 13 | rspec.mock_with :rspec do |configuration| 14 | configuration.syntax = :expect 15 | end 16 | rspec.around(:each) do |example| 17 | Timeout.timeout(1, &example) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/unit/hexp/builder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::Builder do 4 | context 'with an empty block' do 5 | it 'should raise an exception' do 6 | expect { Hexp::Builder.new {}.to_hexp }.to raise_exception 7 | end 8 | end 9 | 10 | context 'with a tag and attributes passed to the constructor' do 11 | let(:builder) do 12 | Hexp::Builder.new :div, class: 'acatalectic' 13 | end 14 | it 'should use them as the root element' do 15 | expect(builder.to_hexp).to eq H[:div, class: 'acatalectic'] 16 | end 17 | end 18 | 19 | context 'with a block parameter' do 20 | it 'should pass the builder to the block' do 21 | Hexp::Builder.new do |builder| 22 | expect(Hexp::Builder === builder).to be true 23 | end 24 | end 25 | 26 | it 'should evaluate the block in the caller context' do 27 | this = self 28 | Hexp::Builder.new do |builder| 29 | expect(this).to eq self 30 | end 31 | end 32 | 33 | it 'should turn calls to the build object into elements' do 34 | hexp = Hexp::Builder.new do |builder| 35 | builder.div class: 'jintishi' do 36 | builder.br 37 | end 38 | end.to_hexp 39 | expect(hexp).to eq(H[:div, {class: 'jintishi'}, H[:br]]) 40 | end 41 | end 42 | 43 | context 'without a block parameter' do 44 | it 'should evaluate in the context of the builder' do 45 | this = self 46 | Hexp::Builder.new do 47 | this.expect(::Hexp::Builder === self).to this.be true 48 | end 49 | end 50 | 51 | it 'should turn bare method calls into elements' do 52 | hexp = Hexp::Builder.new do 53 | span do 54 | p({class: 'lyrical'}, "I'm with you in Rockland") 55 | end 56 | end.to_hexp 57 | expect(hexp).to eq(H[:span, H[:p, {class: 'lyrical'}, "I'm with you in Rockland"]]) 58 | end 59 | end 60 | 61 | describe 'composing' do 62 | it 'should allow inserting Hexpable values with <<' do 63 | hexp = Hexp::Builder.new do 64 | div do |builder| 65 | builder << ::H[:span] 66 | end 67 | end.to_hexp 68 | expect(hexp).to eq(H[:div, H[:span]]) 69 | end 70 | 71 | it 'should raise exception when inserting a non-hexp' do 72 | expect { 73 | Hexp::Builder.new {|b| b << Object.new } 74 | }.to raise_exception(Hexp::FormatError) 75 | end 76 | end 77 | 78 | describe Hexp::Builder::NodeBuilder do 79 | it 'lets you add CSS classes through method calls' do 80 | hexp = Hexp::Builder.new do 81 | div.milky.ponderous do 82 | blink 'My gracious, how wondrous' 83 | end 84 | end.to_hexp 85 | expect(hexp).to eq(H[:div, {class: 'milky ponderous'}, H[:blink, 'My gracious, how wondrous']]) 86 | end 87 | end 88 | 89 | it 'should add text nodes with text!' do 90 | hexp = Hexp::Builder.new do 91 | div do 92 | text! 'Babyface, bijou, scharmninkel' 93 | end 94 | end.to_hexp 95 | expect(hexp).to eq(H[:div, 'Babyface, bijou, scharmninkel']) 96 | end 97 | 98 | it 'should return an inspection string' do 99 | expect(Hexp::Builder.new { div }.inspect).to eq "#" 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/unit/hexp/css_selector/attribute_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::CssSelector::Attribute do 4 | subject(:selector) { described_class.new(name, operator, value) } 5 | let(:name) { nil } 6 | let(:operator) { nil } 7 | let(:value) { nil } 8 | 9 | describe 'without an operator' do 10 | let(:name) { 'href' } 11 | 12 | it 'should match elements with the attribute present' do 13 | expect(selector.matches? H[:a, href: 'http://foo']).to be true 14 | end 15 | 16 | it 'should match elements with an empty attribute present' do 17 | expect(selector.matches? H[:a, href: '']).to be true 18 | end 19 | 20 | it 'should not match elements without the attribute present' do 21 | expect(selector.matches? H[:a]).to be false 22 | end 23 | end 24 | 25 | describe 'with the "=" operator' do 26 | let(:name) { 'class' } 27 | let(:operator) { :equal } 28 | let(:value) { 'foo' } 29 | 30 | it "should match if the attribute's value is exactly equal to the given value" do 31 | expect(selector.matches? H[:a, class: 'foo']).to be true 32 | end 33 | 34 | it "should not match if the attribute's value contains more than the given value" do 35 | expect(selector.matches? H[:a, class: 'foofoo']).to be false 36 | end 37 | 38 | it "should not match if the attribute's value does not contain the given value" do 39 | expect(selector.matches? H[:a, class: 'fo']).to be false 40 | end 41 | end 42 | 43 | describe 'the "~=" operator' do 44 | let(:name) { 'class' } 45 | let(:operator) { :includes } 46 | let(:value) { 'foo' } 47 | 48 | it 'should match an entry in a space separated list' do 49 | expect(selector.matches? H[:a, class: 'foo bla baz']).to be true 50 | end 51 | 52 | it 'should return false if there is no entry that matches' do 53 | expect(selector.matches? H[:a, class: 'bla baz']).to be false 54 | end 55 | 56 | it 'should return false if there is no such attribute' do 57 | expect(selector.matches? H[:a]).to be false 58 | end 59 | end 60 | 61 | describe 'the "|=" operator' do 62 | let(:name) { 'id' } 63 | let(:operator) { :dash_match } 64 | let(:value) { 'foo' } 65 | 66 | it 'should match if the attribute starts with the value, followed by a dash' do 67 | expect(selector.matches? H[:a, id: 'foo-1']).to be_truthy 68 | end 69 | 70 | it 'should not match if the value is not at the start' do 71 | expect(selector.matches? H[:a, id: 'myfoo-1']).to be_falsey 72 | end 73 | 74 | it 'should not match if the value is not followed by a dash' do 75 | expect(selector.matches? H[:a, id: 'foo1']).to be_falsey 76 | end 77 | end 78 | 79 | describe 'the "^=" operator' do 80 | let(:name) { 'id' } 81 | let(:operator) { :prefix_match } 82 | let(:value) { 'foo' } 83 | 84 | it 'should match if the attribute is just the value' do 85 | expect(selector.matches? H[:a, id: 'foo']).to be true 86 | end 87 | 88 | it 'should match if the attribute starts with the value' do 89 | expect(selector.matches? H[:a, id: 'foohi']).to be true 90 | end 91 | 92 | it 'should not match if the value is not at the start' do 93 | expect(selector.matches? H[:a, id: 'myfoo-1']).to be false 94 | end 95 | end 96 | 97 | describe 'the "$=" operator' do 98 | let(:name) { 'id' } 99 | let(:operator) { :suffix_match } 100 | let(:value) { 'foo' } 101 | 102 | it 'should match if the attribute is just the value' do 103 | expect(selector.matches? H[:a, id: 'foo']).to be_truthy 104 | end 105 | 106 | it 'should match if the attribute ends starts with the value' do 107 | expect(selector.matches? H[:a, id: 'hifoo']).to be_truthy 108 | end 109 | 110 | it 'should not match if the value is not at the end' do 111 | expect(selector.matches? H[:a, id: 'foo-1']).to be_falsey 112 | end 113 | end 114 | 115 | describe 'the "*=" operator' do 116 | let(:name) { 'id' } 117 | let(:operator) { :substring_match } 118 | let(:value) { 'foo' } 119 | 120 | it 'should match if the attribute is just the value' do 121 | expect(selector.matches? H[:a, id: 'foo']).to be true 122 | end 123 | 124 | it 'should match if the attribute starts starts with the value' do 125 | expect(selector.matches? H[:a, id: 'foohi']).to be true 126 | end 127 | 128 | it 'should match if the attribute ends starts with the value' do 129 | expect(selector.matches? H[:a, id: 'hifoo']).to be true 130 | end 131 | 132 | it 'should not match if the value is not in the attribute' do 133 | expect(selector.matches? H[:a, id: 'yomofohoho']).to be false 134 | end 135 | end 136 | 137 | describe '$= with quoted string values' do 138 | subject(:selector) { Hexp::CssSelector::Parser.new('[src$="foo/bar.js"]').parse } 139 | 140 | it 'should match correctly' do 141 | expect(selector.matches? H[:script, src: "/tmp/foo/bar.js"]).to be true 142 | end 143 | 144 | it 'should only match at the end' do 145 | expect(selector.matches? H[:script, src: "/tmp/foo/bar.jsx"]).to be false 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /spec/unit/hexp/css_selector/class_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::CssSelector::Class do 4 | it 'should match elements having the giving class' do 5 | expect(described_class.new('big').matches?(H[:div, class: 'big'])).to be true 6 | end 7 | 8 | it 'should not match elements not having the given class' do 9 | expect(described_class.new('big').matches?(H[:div, class: 'small'])).to be false 10 | end 11 | 12 | it 'should work with elements with multiple classes' do 13 | expect(described_class.new('foo').matches?(H[:div, class: 'foo bar'])).to be true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/unit/hexp/css_selector/comma_sequence_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::CssSelector::CommaSequence do 4 | let(:comma_sequence) { Hexp::CssSelector::Parser.call(selector) } 5 | 6 | it 'has members' do 7 | described_class.new([:foo]).members == [:foo] 8 | end 9 | 10 | describe '#matches?' do 11 | context do 12 | let(:selector) { 'ul li, li' } 13 | let(:element) { H[:li, class: 'baz'] } 14 | 15 | it 'should match' do 16 | expect(comma_sequence.matches?(element)).to be true 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/unit/hexp/css_selector/element_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::CssSelector::Element do 4 | it 'should match elements with the same name' do 5 | expect(described_class.new('tag').matches?(H[:tag])).to be true 6 | end 7 | 8 | it 'should not match elements with a different name' do 9 | expect(described_class.new('spane').matches?(H[:div])).to be false 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/unit/hexp/css_selector/parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::CssSelector::Parser do 4 | HC = Hexp::CssSelector # Is there really no way to include constant lookup in this namespace ?! 5 | 6 | subject(:parse_tree) { described_class.call(selector) } 7 | 8 | 9 | context 'with a single tag' do 10 | let(:selector) { 'body' } 11 | it { 12 | should eq HC::CommaSequence[HC::Element.new('body')] 13 | } 14 | end 15 | 16 | context 'with SASS specific syntax' do 17 | let(:selector) { '&.foo' } 18 | it 'should raise an exception' do 19 | expect{parse_tree}.to raise_exception 20 | end 21 | end 22 | 23 | context 'with an element, class and id specifier' do 24 | let(:selector) { '#main a.strong' } 25 | it { 26 | should eq HC::CommaSequence[ 27 | HC::Sequence[ 28 | HC::SimpleSequence[ 29 | HC::Universal.new, 30 | HC::Id.new('main')], 31 | HC::SimpleSequence[ 32 | HC::Element.new('a'), 33 | HC::Class.new('strong')]]] 34 | } 35 | end 36 | 37 | context 'with an attribute selector' do 38 | let(:selector) { 'div[link=href]' } 39 | it { 40 | should eq HC::CommaSequence[ 41 | HC::SimpleSequence[ 42 | HC::Element.new('div'), 43 | HC::Attribute.new('link', :equal, 'href'), 44 | ]] 45 | } 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /spec/unit/hexp/css_selector/simple_sequence_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::CssSelector::SimpleSequence do 4 | context 'with a single element member' do 5 | let(:sequence) { described_class[Hexp::CssSelector::Element.new('div')] } 6 | 7 | it 'should match when the element has the same tag name' do 8 | expect(sequence.matches?(H[:div])).to be true 9 | end 10 | 11 | it 'should not match when the tag name differs' do 12 | expect(sequence.matches?(H[:span])).to be false 13 | end 14 | end 15 | 16 | context 'with a single class member' do 17 | let(:sequence) { described_class[Hexp::CssSelector::Class.new('mega')] } 18 | 19 | it 'should match when the element has a class by that name' do 20 | expect(sequence.matches?(H[:div, class: 'mega'])).to be true 21 | end 22 | 23 | it 'should not match when the element has no classes' do 24 | expect(sequence.matches?(H[:span])).to be false 25 | end 26 | 27 | it 'should not match when the element has no classes by that name' do 28 | expect(sequence.matches?(H[:span, class: 'megalopolis'])).to be false 29 | end 30 | end 31 | 32 | context 'with an element and class' do 33 | let(:sequence) do described_class[ 34 | Hexp::CssSelector::Element.new('div'), 35 | Hexp::CssSelector::Class.new('mega') 36 | ] 37 | end 38 | 39 | it 'should match if all parts are satisfied' do 40 | expect(sequence.matches?(H[:div, class: 'mega'])).to be true 41 | end 42 | 43 | it 'should not match if one parts is not satisfied' do 44 | expect(sequence.matches?(H[:div, class: 'foo'])).to be false 45 | expect(sequence.matches?(H[:span, class: 'mega'])).to be false 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/unit/hexp/css_selector/universal_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::CssSelector::Universal do 4 | it 'should match everything' do 5 | expect(subject.matches? H[:section]).to be true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/unit/hexp/dsl_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::DSL do 4 | let(:jotie) { 'Liefste, Hart en woorden houden voor jou stil' } 5 | let(:hexpable) do 6 | Class.new do 7 | include Hexp::DSL 8 | 9 | def initialize(klz, words) 10 | @class, @words = klz, words 11 | end 12 | 13 | def to_hexp 14 | H[:div, {class: @class}, [@words]] 15 | end 16 | end.new('prinses', jotie) 17 | end 18 | 19 | { 20 | tag: :div, 21 | attributes: {'class' => 'prinses'}, 22 | children: ['Liefste, Hart en woorden houden voor jou stil'], 23 | }.each do |method, result| 24 | it "should delegate `#{method}' to to_hexp" do 25 | expect(hexpable.public_send(method)).to eq(result) 26 | end 27 | end 28 | 29 | it "should delegate `to_html' to to_hexp" do 30 | expect(hexpable.to_html).to match \ 31 | %r{
Liefste, Hart en woorden houden voor jou stil
} 32 | end 33 | 34 | it "should delegate `attr' to to_hexp" do 35 | expect(hexpable.attr('class')).to eq('prinses') 36 | expect(hexpable.attr('class', 'scharminkel')).to eq( 37 | H[:div, {class: 'scharminkel'}, [jotie]] 38 | ) 39 | end 40 | 41 | it "should delegate `select' to to_hexp" do 42 | expect(hexpable.select{|el| el.text?}.to_a).to eq([jotie]) 43 | end 44 | 45 | it "should delegate `class?' to to_hexp" do 46 | expect(hexpable.class?(:prinses)).to be true 47 | expect(hexpable.class?(:prins)).to be false 48 | end 49 | 50 | it "should delegate `rewrite' to to_hexp" do 51 | expect(hexpable.rewrite {|el| 'De liefde speelt me parten' if el.text?}.to_hexp) 52 | .to eq H[:div, {class: 'prinses'}, ['De liefde speelt me parten']] 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /spec/unit/hexp/h_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'H notation' do 4 | context 'if H is already defined' do 5 | before do 6 | @old_H = Object.send(:remove_const, :H) 7 | @old_stderr, $stderr = $stderr, StringIO.new 8 | ::H = 'foo'.freeze 9 | end 10 | 11 | after do 12 | Object.send(:remove_const, :H) 13 | $stderr = @old_stderr 14 | ::H = @old_H 15 | end 16 | 17 | it 'should not override H' do 18 | expect(H).to eq('foo') 19 | end 20 | 21 | it 'should print out a warning on STDERR' do 22 | load 'hexp/h.rb' 23 | expect($stderr.string).to match(/WARN/) 24 | end 25 | 26 | end 27 | 28 | context 'if H is not set yet' do 29 | before do 30 | Object.send(:remove_const, :H) 31 | end 32 | 33 | it 'should define H' do 34 | load 'hexp/h.rb' 35 | expect(H).to be_a Module 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/unit/hexp/list_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::List do 4 | it 'should be equal to an Array with the same contents' do 5 | expect(Hexp::List[ H[:div] ]).to eq [ H[:div] ] 6 | end 7 | 8 | describe 'value and type equality' do 9 | it 'should not be #eql? to an Array with the same contents' do 10 | expect(Hexp::List[ H[:div] ]).to_not eql [ H[:div] ] 11 | end 12 | end 13 | 14 | describe 'inspect' do 15 | it 'should look exactly like an Array' do 16 | expect(Hexp::List[ H[:div] ].inspect).to eq '[H[:div]]' 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/unit/hexp/mutable_tree_walk_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Hexp::MutableTreeWalk do 2 | 3 | let(:node) { H[:ul, H[:p, [H[:li, 'foo', 'boo'], H[:li, 'bar']]]] } 4 | let(:walk) { Hexp::MutableTreeWalk.new(node) } 5 | 6 | it 'should start at the root' do 7 | expect(walk.current).to eql node 8 | end 9 | 10 | it 'should not have a parent at the root' do 11 | expect(walk.parent).to be_nil 12 | end 13 | 14 | it 'should descend to the children' do 15 | walk.next! 16 | expect(walk.current).to eql H[:p, [H[:li, ["foo", "boo"]], H[:li, ["bar"]]]] 17 | end 18 | 19 | it 'should go depth first' do 20 | 2.times { walk.next! } 21 | expect(walk.current).to eql H[:li, 'foo', 'boo'] 22 | end 23 | 24 | it 'should also do text nodes' do 25 | 3.times { walk.next! } 26 | expect(walk.current).to eq 'foo' 27 | end 28 | 29 | it 'should go left to right' do 30 | 4.times { walk.next! } 31 | expect(walk.current).to eq 'boo' 32 | end 33 | 34 | it 'should go back up and right' do 35 | 5.times { walk.next! } 36 | expect(walk.current).to eql H[:li, 'bar'] 37 | end 38 | 39 | it 'should finish on nil' do 40 | 7.times { walk.next! } 41 | expect(walk.current).to be_nil 42 | expect(walk.end?).to be true 43 | end 44 | 45 | it 'stays at the end' do 46 | 8.times { walk.next! } 47 | expect(walk.end?).to be true 48 | end 49 | 50 | it 'should allow replacements' do 51 | 2.times { walk.next! } 52 | walk.replace! H[:foo] 53 | 6.times { walk.next! } 54 | expect(walk.result).to eql H[:ul, H[:p, H[:foo], H[:li, 'bar']]] 55 | end 56 | 57 | it 'should allow replacements' do 58 | 7.times do 59 | walk.next! 60 | if !walk.end? && !walk.current.text? && walk.current.tag?(:li) 61 | walk.replace! H[:span, walk.current] 62 | end 63 | end 64 | 65 | expect(walk.result).to eql H[:ul, 66 | H[:p, 67 | H[:span, H[:li, 'foo', 'boo']], 68 | H[:span, H[:li, 'bar']]]] 69 | end 70 | 71 | 72 | end 73 | -------------------------------------------------------------------------------- /spec/unit/hexp/node/attr_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::Node, 'attr' do 4 | subject { hexp.attr(*args) } 5 | let(:hexp) { H[:div, class: 'foo'] } 6 | 7 | context 'with a single string argument' do 8 | let(:args) { ['class'] } 9 | 10 | it 'should return the attribute value by that name' do 11 | expect(subject).to eq('foo') 12 | end 13 | end 14 | 15 | context 'with a single symbol argument' do 16 | let(:args) { [:class] } 17 | 18 | it 'should return the attribute value by that name' do 19 | expect(subject).to eq('foo') 20 | end 21 | end 22 | 23 | context 'with two argument' do 24 | let(:args) { ['data-id', '7'] } 25 | 26 | it 'should return a new Hexp::Node' do 27 | expect(subject).to be_instance_of(Hexp::Node) 28 | end 29 | 30 | it 'should set the attribute value' do 31 | expect(subject.attributes['data-id']).to eq('7') 32 | end 33 | 34 | it 'should leave other attributes untouched' do 35 | expect(subject.attributes['class']).to eq('foo') 36 | end 37 | 38 | context 'with a nil value' do 39 | let(:args) { ['class', nil] } 40 | 41 | it 'should unset the attribute' do 42 | expect(subject.attributes).to eq({}) 43 | end 44 | end 45 | end 46 | 47 | context 'with too many arguments' do 48 | let(:args) { ['class', 'baz', 'bar'] } 49 | 50 | it 'should raise an ArgumentError' do 51 | expect{ subject }.to raise_error(ArgumentError) 52 | end 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /spec/unit/hexp/node/attributes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::Node::Attributes do 4 | describe 'attr?' do 5 | it 'should return true if the attribute is present' do 6 | expect(H[:a, href: '/foo'].has_attr?(:href)).to be true 7 | end 8 | 9 | it 'should return true if the attribute is present and empty' do 10 | expect(H[:a, href: ''].has_attr?(:href)).to be true 11 | end 12 | 13 | it 'should return false if the attribute is not present' do 14 | expect(H[:a].has_attr?(:href)).to be false 15 | end 16 | 17 | it 'should work with a string argument' do 18 | expect(H[:a, href: '/foo'].has_attr?('href')).to be true 19 | end 20 | end 21 | 22 | describe 'class_list' do 23 | context 'for a node without a class attribute' do 24 | subject(:node) { H[:div] } 25 | 26 | it 'should return an empty string' do 27 | expect(node.class_list).to eq [] 28 | end 29 | end 30 | 31 | context 'for a node with an empty class attribute' do 32 | subject(:node) { H[:div, class: ''] } 33 | 34 | it 'should return an empty string' do 35 | expect(node.class_list).to eq [] 36 | end 37 | end 38 | 39 | context 'for a node with a single class' do 40 | subject(:node) { H[:div, class: 'daklazz'] } 41 | 42 | it 'should return a list with the single class' do 43 | expect(node.class_list).to eq ['daklazz'] 44 | end 45 | end 46 | 47 | context 'for a node with multiple classes' do 48 | subject(:node) { H[:div, class: 'daklazz otherklazz foo'] } 49 | 50 | it 'should return a list with the single class' do 51 | expect(node.class_list).to eq ['daklazz', 'otherklazz', 'foo'] 52 | end 53 | end 54 | end 55 | 56 | describe 'remove_class' do 57 | context 'for a node without a class list' do 58 | it 'should be idempotent' do 59 | expect(H[:div].remove_class('foo')).to eq H[:div] 60 | end 61 | end 62 | 63 | context 'for a node with an empty class list' do 64 | it 'should remove the attribute' do 65 | expect(H[:div, class: ''].remove_class('foo')).to eq H[:div] 66 | end 67 | end 68 | 69 | context 'for a node with one class' do 70 | it 'should remove the class attribute when the class is removed' do 71 | expect(H[:div, class: 'foo'].remove_class('foo')).to eq H[:div] 72 | end 73 | 74 | it 'should return the node itself when an other class is removed' do 75 | expect(H[:div, class: 'foo'].remove_class('bar')).to eq H[:div, class: 'foo'] 76 | end 77 | end 78 | 79 | context 'with a class appearing multiple times in a class list' do 80 | it 'should remove all instances of the class' do 81 | expect(H[:div, class: 'foo foo'].remove_class('foo')).to eq H[:div] 82 | end 83 | end 84 | end 85 | 86 | describe 'set_attrs' do 87 | it 'should set attributes' do 88 | expect(H[:foo].set_attrs(class: 'bar')).to eq H[:foo, class: 'bar'] 89 | end 90 | 91 | it 'should override attributes' do 92 | expect(H[:foo, class: 'baz'].set_attrs(class: 'bar')).to eq H[:foo, class: 'bar'] 93 | end 94 | end 95 | 96 | describe 'merge_attrs' do 97 | describe 'when passing in a Hash' do 98 | it 'should set attributes' do 99 | expect(H[:foo].merge_attrs(class: 'bar')).to eq H[:foo, class: 'bar'] 100 | end 101 | 102 | it 'should merge class lists' do 103 | expect(H[:foo, class: 'baz'].merge_attrs(class: 'bar')).to eq H[:foo, class: 'baz bar'] 104 | end 105 | 106 | it 'should override attributes that are not class' do 107 | expect(H[:foo, src: 'baz'].set_attrs(src: 'bar')).to eq H[:foo, src: 'bar'] 108 | end 109 | 110 | it 'should merge keep both old and new attributes' do 111 | expect(H[:foo, class: 'baz'].merge_attrs(src: 'bar')).to eq H[:foo, class: 'baz', src: 'bar'] 112 | end 113 | end 114 | 115 | describe 'when passing in a Hexp::Node' do 116 | it 'should take the nodes attributes to merge with' do 117 | expect(H[:foo, class: 'klz1'].merge_attrs(H[:bla, class: 'klz2'])).to eq H[:foo, class: 'klz1 klz2'] 118 | end 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /spec/unit/hexp/node/children_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::Node::Children do 4 | describe 'empty?' do 5 | context 'for an empty node' do 6 | subject { H[:div] } 7 | it { should be_empty } 8 | end 9 | 10 | context 'for a node with children' do 11 | subject { H[:div, [H[:span, "hello"]]] } 12 | it { should_not be_empty } 13 | end 14 | end 15 | 16 | describe 'add_child' do 17 | it 'should return a new node with the child added' do 18 | expect(H[:div].add_child(H[:span])).to eq H[:div, H[:span]] 19 | end 20 | end 21 | 22 | describe 'text' do 23 | it 'should return all text nodes that are descendant of this node, combined' do 24 | expect(H[:div, [ 25 | "Hello,", 26 | H[:span, {class: 'big'}, "World!"] 27 | ] 28 | ].text 29 | ).to eq "Hello,World!" 30 | end 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /spec/unit/hexp/node/class_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::Node, 'class?' do 4 | context 'with no class attribute set' do 5 | it 'should return false' do 6 | expect(H[:p].class?('strong')).to be_falsey 7 | end 8 | end 9 | 10 | context 'with a single class set' do 11 | it 'should return true if the class name is the same' do 12 | expect(H[:p, class: 'strong'].class?('strong')).to be true 13 | end 14 | 15 | it 'should return false if the class name is not same' do 16 | expect(H[:p, class: 'strong'].class?('foo')).to be false 17 | end 18 | 19 | it 'should return false if the class name is a partial match' do 20 | expect(H[:p, class: 'strong'].class?('stron')).to be false 21 | end 22 | end 23 | 24 | context 'with multiple classes set' do 25 | it 'should return true if the class name is part of the class list' do 26 | expect(H[:p, class: 'banner strong'].class?('strong')).to be true 27 | end 28 | 29 | it 'should return false if the class name is not in the class list' do 30 | expect(H[:p, class: 'banner strong'].class?('foo')).to be false 31 | end 32 | 33 | it 'should return false if the class name is a partial match' do 34 | expect(H[:p, class: 'banner strong'].class?('er str')).to be false 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/unit/hexp/node/css_selection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::Node::CssSelection do 4 | subject(:selection) { described_class.new(hexp, selector) } 5 | 6 | context 'given a single tag' do 7 | let(:selector) { 'span' } 8 | 9 | context 'with a depth of 1' do 10 | let(:hexp) { H[:span] } 11 | 12 | it 'should match all nodes of that tag' do 13 | expect(selection.to_a).to eq [ H[:span] ] 14 | end 15 | end 16 | 17 | context 'with a depth of 2' do 18 | let(:hexp) { H[:span, {id: 'span-1'}, H[:span, id: 'span-2']] } 19 | 20 | it 'should match all nodes of that tag' do 21 | expect(selection.to_a).to eq [ hexp, H[:span, id: 'span-2'] ] 22 | end 23 | end 24 | end 25 | 26 | context 'given a tag and class' do 27 | let(:selector) { 'span.foo' } 28 | 29 | context 'with a depth of 1' do 30 | context 'with a matching tag and class' do 31 | let(:hexp) { H[:span, class: 'foo bar'] } 32 | its(:to_a) { should eq [ hexp ] } 33 | end 34 | 35 | context 'with only a matching tag' do 36 | let(:hexp) { H[:span] } 37 | its(:to_a) { should eq [] } 38 | end 39 | 40 | context 'with only a matching class' do 41 | let(:hexp) { H[:div, class: 'foo'] } 42 | its(:to_a) { should eq [] } 43 | end 44 | end 45 | end 46 | 47 | context 'given a sequence of tags' do 48 | let(:selector) { 'ul li' } 49 | 50 | context 'with a minimal matching tag' do 51 | let(:hexp) { H[:ul, H[:li]] } 52 | its(:to_a) { should eq [ H[:li] ] } 53 | end 54 | 55 | context 'with other tags in between' do 56 | let(:hexp) do 57 | H[:body, [ 58 | H[:ul, [ 59 | H[:span, [ 60 | H[:li, id: 'foo'], 61 | H[:li, id: 'bar']]], 62 | H[:li, id: 'baz']]]]] 63 | end 64 | its(:to_a) { should eq %w[foo bar baz].map{|id| H[:li, id: id] } } 65 | end 66 | end 67 | 68 | describe 'rewrite' do 69 | let(:hexp) do 70 | H[:ul, [ 71 | H[:li, [ 72 | H[:a, href: '/foo']]], 73 | H[:a, href: '/moo']]] 74 | end 75 | let(:selector) { 'li a' } 76 | 77 | it 'should only affect nodes that match the selection' do 78 | expect(selection.rewrite {|node| node.add_class('selected')}).to eq( 79 | H[:ul, [ 80 | H[:li, [ 81 | H[:a, href: '/foo', class: 'selected']]], 82 | H[:a, href: '/moo']]] 83 | ) 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/unit/hexp/node/domize_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::Node::Domize do 4 | def build_doc(&blk) 5 | Nokogiri::HTML::Builder.new(&blk).doc 6 | end 7 | 8 | subject { Hexp::Node::Domize.new(hexp).call } 9 | 10 | context 'with the same single node' do 11 | let(:dom) { build_doc { html } } 12 | let(:hexp) { Hexp::Node[:html] } 13 | 14 | it { should dom_eq(dom) } 15 | end 16 | 17 | context 'with a different single node' do 18 | let(:dom) { build_doc { html } } 19 | let(:hexp) { Hexp::Node[:body] } 20 | 21 | it { should_not dom_eq(dom) } 22 | end 23 | 24 | context 'with nested nodes' do 25 | let(:dom) { build_doc { html { div(class: 'big') } } } 26 | let(:hexp) { Hexp::Node[:html, [ [:div, class: 'big'] ]] } 27 | 28 | it { should dom_eq(dom) } 29 | end 30 | 31 | context 'with equal text nodes' do 32 | let(:dom) { build_doc { 33 | html do 34 | div(class: 'big') 35 | text "awesometown!" 36 | end 37 | } } 38 | let(:hexp) { 39 | Hexp::Node[:html, [ 40 | [:div, class: 'big'], 41 | "awesometown!" 42 | ] 43 | ] 44 | } 45 | 46 | it { should dom_eq(dom) } 47 | end 48 | 49 | context 'with differing text nodes' do 50 | let(:dom) { build_doc { 51 | html do 52 | div(class: 'big') 53 | text "awesomevillage!" 54 | end 55 | } } 56 | let(:hexp) { 57 | Hexp::Node[:html, [ 58 | [:div, class: 'big'], 59 | "awesometown!" 60 | ] 61 | ] 62 | } 63 | 64 | it { should_not dom_eq(dom) } 65 | end 66 | 67 | context 'with the :html5 option' do 68 | let(:hexp) { Hexp::Node[:html] } 69 | 70 | it 'should set a HTML5 style doctype' do 71 | dtd = hexp.to_dom(html5: true).children.first 72 | expect(dtd).to be_a Nokogiri::XML::DTD 73 | expect(dtd.name).to be_nil 74 | expect(dtd.external_id).to be_nil 75 | expect(dtd.system_id).to be_nil 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/unit/hexp/node/normalize_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::Node::Normalize, '#call' do 4 | subject(:normalized) { Hexp::Node[*node] } 5 | 6 | describe 'with a single element' do 7 | let(:node) { [:div] } 8 | 9 | it 'should treat the first as the tag' do 10 | expect(normalized.tag).to eq :div 11 | end 12 | it 'should set an empty attribute list' do 13 | expect(normalized.attributes).to eq Hash[] 14 | end 15 | it 'should set an empty children list' do 16 | expect(normalized.children).to eql Hexp::List[] 17 | end 18 | end 19 | 20 | describe 'with two parameters' do 21 | let(:node) { [:div, {class: 'foo'}] } 22 | 23 | it 'should treat the first as the tag' do 24 | expect(subject.tag).to be :div 25 | end 26 | it 'should treat the second as the attribute list, if it is a Hash' do 27 | expect(subject.attributes).to eql('class' => 'foo') 28 | end 29 | it 'should treat the second as a list of children, if it is an Array' do 30 | expect(subject.children).to eql Hexp::List[] 31 | end 32 | end 33 | 34 | describe 'with a single text child node' do 35 | let(:node) { [:div, "this is text in the div"] } 36 | 37 | it 'should set is as the single child' do 38 | expect(subject.children).to eql Hexp::List["this is text in the div"] 39 | end 40 | end 41 | 42 | describe 'with child nodes' do 43 | let(:node) { 44 | [:div, [ 45 | [:h1, "Big Title"], 46 | [:p, {class: 'greeting'}, "hello world"], 47 | "Some loose text" 48 | ] 49 | ] 50 | } 51 | 52 | it 'must normalize them recursively' do 53 | expect(subject.children).to eql Hexp::List[ 54 | Hexp::Node[:h1, {}, Hexp::List["Big Title"] ], 55 | Hexp::Node[:p, {class: 'greeting'}, Hexp::List["hello world"] ], 56 | "Some loose text" 57 | ] 58 | end 59 | end 60 | 61 | describe 'with an object that responds to to_hexp' do 62 | let(:hexpable) { 63 | Class.new do 64 | def to_hexp 65 | Hexp::Node[:em, "I am in your hexpz"] 66 | end 67 | end 68 | } 69 | let(:node) { 70 | [:div, [ hexpable.new ] ] 71 | } 72 | 73 | it 'must expand that object' do 74 | expect(subject.children).to eql Hexp::List[ 75 | Hexp::Node[:em, {}, Hexp::List["I am in your hexpz"] ] 76 | ] 77 | end 78 | end 79 | 80 | context 'with a nil in the child list' do 81 | let(:node) { [:div, [nil]] } 82 | it 'should raise an exception' do 83 | expect{normalized}.to raise_exception(Hexp::FormatError) 84 | end 85 | end 86 | 87 | context 'with multiple children not wrapped in an error' do 88 | let(:node) { [:div, {foo: 'bar'}, "hello", [:p], [:span, "foo"]] } 89 | 90 | it 'should work fine' do 91 | expect(normalized).to eql Hexp::Node[:div, {"foo"=>"bar"}, Hexp::List["hello", Hexp::Node[:p], Hexp::Node[:span, Hexp::List["foo"]]]] 92 | end 93 | end 94 | 95 | end 96 | -------------------------------------------------------------------------------- /spec/unit/hexp/node/pp_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::Node, 'pp' do 4 | subject { object.pp } 5 | 6 | context 'with no attributes or children' do 7 | let(:object) { H[:p, {}] } 8 | it { should == 'H[:p]'} 9 | end 10 | 11 | context 'with a single child' do 12 | let(:object) { H[:p, [ [:abbr, {title: 'YAGNI'}, "You ain't gonna need it"] ]] } 13 | it { should == %q^H[:p, [ 14 | H[:abbr, {"title"=>"YAGNI"}, [ 15 | "You ain't gonna need it"]]]]^.gsub(' '*22, '') 16 | } 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/unit/hexp/node/rewrite_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::Node, 'rewrite' do 4 | subject(:rewriter) { Hexp::Node::Rewriter.new(hexp, block) } 5 | 6 | let :hexp do 7 | H[:div, [ 8 | [:a], 9 | [:p], 10 | [:br]]] 11 | end 12 | 13 | context 'without a block' do 14 | subject { hexp.rewrite(&block) } 15 | let(:block) { nil } 16 | 17 | it 'returns a Rewriter' do 18 | expect(subject).to be_instance_of(Hexp::Node::Rewriter) 19 | end 20 | end 21 | 22 | context 'with a block that returns [child]' do 23 | let(:block) { proc {|child, parent| [child] } } 24 | 25 | it 'should return an identical hexpable' do 26 | expect(subject.to_hexp).to eq(hexp) 27 | end 28 | end 29 | 30 | context 'with multiple nestings' do 31 | let :hexp do 32 | H[:span, [super()]] 33 | end 34 | 35 | let :block do 36 | proc do |child, parent| 37 | @tags << [child.tag, parent.tag] 38 | nil 39 | end 40 | end 41 | 42 | it 'should traverse depth-first' do 43 | @tags = [] 44 | rewriter.to_hexp 45 | expect(@tags).to eq([[:a, :div], [:p, :div], [:br, :div], [:div, :span]]) 46 | end 47 | end 48 | 49 | context 'when adding nodes' do 50 | let :block do 51 | proc do |child, parent| 52 | raise 'got my own node back' if child.tag == :blockquote 53 | # wrap paragraphs in a
54 | if child.tag == :p 55 | [:blockquote, [child]] 56 | else 57 | [child] 58 | end 59 | end 60 | end 61 | 62 | it 'should not pass those nodes again to the block' do 63 | expect(rewriter.to_hexp).to eql H[:div, [ 64 | [:a], 65 | [:blockquote, [ 66 | [:p]]], 67 | [:br]]] 68 | end 69 | end 70 | 71 | context 'with a one parameter block' do 72 | let :hexp do 73 | H[:parent, [[:child]]] 74 | end 75 | 76 | let :block do 77 | proc do |child| 78 | expect(child).to eq(H[:child]) 79 | [child] 80 | end 81 | end 82 | 83 | it 'should receive the child node as its argument' do 84 | rewriter.to_hexp 85 | end 86 | end 87 | 88 | describe 'block response types' do 89 | context 'when responding with a single node' do 90 | let :block do 91 | proc do |child| 92 | H[:br] 93 | end 94 | end 95 | 96 | it 'should replace the existing node' do 97 | expect(rewriter.to_hexp).to eq H[:div, [ [:br] ]*3 ] 98 | end 99 | end 100 | 101 | context 'when responding with an array that starts with a Symbol' do 102 | let :block do 103 | proc do |child| 104 | [:br, {class: 'foo'} ] 105 | end 106 | end 107 | 108 | it 'should treat it as a node and replace the existing one' do 109 | expect(rewriter.to_hexp).to eq H[:div, [ [:br, {'class' => 'foo'}] ]*3 ] 110 | end 111 | end 112 | 113 | context 'when responding with a String' do 114 | let :hexp do 115 | H[:div, [ 116 | [:p] 117 | ] 118 | ] 119 | end 120 | 121 | let :block do 122 | proc do |child| 123 | "Hello" 124 | end 125 | end 126 | 127 | it 'should convert it to a text node' do 128 | expect(rewriter.to_hexp).to eq H[:div, [ Hexp::TextNode.new("Hello") ] ] 129 | end 130 | end 131 | 132 | 133 | context 'when responding with nil' do 134 | let :block do 135 | proc do |node| 136 | node if [:p, :br].include? node.tag 137 | end 138 | end 139 | 140 | it 'should remove the original node' do 141 | expect(rewriter.to_hexp).to eq H[:div, [ H[:p], H[:br] ]] 142 | end 143 | end 144 | end 145 | 146 | context 'when responding with something else than a Hexp, Array or String' do 147 | let :block do 148 | proc do |node| 149 | Object.new 150 | end 151 | end 152 | 153 | it 'should raise a FormatError' do 154 | expect{rewriter.to_hexp}.to raise_exception(Hexp::FormatError) 155 | end 156 | end 157 | 158 | context 'with a css selector argument' do 159 | let(:selector) { 'p.foo' } 160 | 161 | it 'should delegate to CssSelection, rather than Rewriter' do 162 | expect(Hexp::Node::CssSelection).to receive(:new).with(hexp, selector).and_return(double(:rewrite => hexp)) 163 | hexp.rewrite(selector) 164 | end 165 | end 166 | 167 | end 168 | -------------------------------------------------------------------------------- /spec/unit/hexp/node/selection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::Node::Selection do 4 | subject(:selection) { Hexp::Node::Selection.new(hexp, block) } 5 | let(:yielded_elements) { [] } 6 | let(:block) { proc {|el| yielded_elements << el } } 7 | let(:hexp) { H[:div, [[:span]]] } 8 | 9 | describe 'as Enumerable' do 10 | let(:block) { proc {|el| el.tag == :span} } 11 | 12 | it 'should enumerate elements for which the block returns trueish' do 13 | expect(selection.to_a).to eq [H[:span]] 14 | end 15 | end 16 | 17 | describe 'rewriting operations' do 18 | let(:block) { proc {|el| el.tag == :span} } 19 | 20 | it 'should perform them on elements that match' do 21 | expect(selection.attr('class', 'matched').to_hexp).to eq( 22 | H[:div, [[:span, {class: 'matched'}]]] 23 | ) 24 | end 25 | 26 | describe 'wrap' do 27 | let(:hexp) { H[:ul, [[:a, href: 'foo'], [:a, href: 'bar']]] } 28 | let(:block) { proc {|el| el.tag == :a} } 29 | 30 | it 'should be able to wrap element' do 31 | expect(selection.wrap(:li).to_hexp).to eq( 32 | H[:ul, [[:li, H[:a, href: 'foo']], [:li, H[:a, href: 'bar']]]] 33 | ) 34 | end 35 | end 36 | 37 | describe 'rewrite' do 38 | let(:hexp) { H[:ul, [[:a, href: 'foo'], [:span]]] } 39 | let(:block) { proc {|el| el.tag == :a} } 40 | 41 | it 'should work on matching elements, and skip the rest' do 42 | expect(selection.rewrite{ H[:br] }.to_hexp).to eq H[:ul, [[:br], [:span]]] 43 | end 44 | end 45 | end 46 | 47 | context 'with a single element' do 48 | let(:hexp) { H[:div] } 49 | 50 | it 'should be lazy' do 51 | expect(block).to_not receive(:call) 52 | selection 53 | end 54 | 55 | it 'should yield the root element when realized' do 56 | expect(block).to receive(:call).once.with(H[:div]) 57 | selection.each {} 58 | end 59 | end 60 | 61 | context 'with nested elements' do 62 | let(:hexp) { 63 | H[:div, [ 64 | [:span, "hello"], 65 | [:span, "world"]]]} 66 | 67 | it 'should traverse the whole tree once, depth-first' do 68 | selection.each {} 69 | expect(yielded_elements).to eq [ 70 | Hexp::TextNode.new("hello"), 71 | H[:span, "hello"], 72 | Hexp::TextNode.new("world"), 73 | H[:span, "world"], 74 | hexp 75 | ] 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/unit/hexp/node/text_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::Node do 4 | subject { described_class[:p] } 5 | 6 | its(:text?) { should be false } 7 | end 8 | -------------------------------------------------------------------------------- /spec/unit/hexp/node/to_dom_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::Node, 'to_dom' do 4 | subject { Hexp::Node[:blink] } 5 | 6 | it 'should delegate to Domize' do 7 | expect(Hexp::Node::Domize).to receive(:new).with(subject, {}).and_return( ->{ 'result' } ) 8 | expect(subject.to_dom).to eql 'result' 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/unit/hexp/node/to_hexp_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::Node, 'to_hexp' do 4 | let(:object) {Hexp::Node.new(:p) } 5 | subject { object.to_hexp } 6 | 7 | it { should == subject } 8 | end 9 | -------------------------------------------------------------------------------- /spec/unit/hexp/node/to_html_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::Node, 'to_html' do 4 | subject { Hexp::Node[:tt] } 5 | 6 | it 'should render HTML' do 7 | expect(subject.to_html).to eql '' 8 | end 9 | 10 | describe 'attribute escaping' do 11 | subject { Hexp::Node[:foo, {bar: "it's fine&dandy"}] } 12 | 13 | it 'should escape ampersand, single quote' do 14 | expect(subject.to_html).to eql "" 15 | end 16 | end 17 | 18 | describe 'text node escaping' do 19 | subject { Hexp::Node[:foo, "it's 5 > 3, & 6 < 3, \"fine chap\""] } 20 | 21 | it 'should escape ampersand, single quote, double quote, lower than, greater than' do 22 | expect(subject.to_html).to eql "it's 5 > 3, & 6 < 3, "fine chap"" 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/unit/hexp/nokogiri/equality_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::Nokogiri::Equality do 4 | let(:doc) { Nokogiri::HTML::Document.new } 5 | 6 | context 'two empty documents' do 7 | it 'should be equal' do 8 | expect(described_class.new(Nokogiri::HTML::Document.new, Nokogiri::HTML::Document.new).call).to be true 9 | end 10 | end 11 | 12 | context 'two nodes with the same attributes' do 13 | it 'should be equal' do 14 | node1 = Nokogiri::XML::Node.new('div', doc) 15 | node1['class'] = 'hello' 16 | node2 = Nokogiri::XML::Node.new('div', doc) 17 | node2['class'] = 'hello' 18 | 19 | expect(described_class.new(node1, node2).call).to be true 20 | end 21 | end 22 | 23 | context 'one node has an attribute more' do 24 | it 'should be equal' do 25 | node1 = Nokogiri::XML::Node.new('div', doc) 26 | node1['class'] = 'hello' 27 | node2 = Nokogiri::XML::Node.new('div', doc) 28 | node2['class'] = 'hello' 29 | node2['id'] = 'zigzag' 30 | 31 | expect(described_class.new(node1, node2).call).to be true 32 | end 33 | end 34 | 35 | context 'two nodes with the same children' do 36 | it 'should be equal' do 37 | node1 = Nokogiri::XML::Node.new('div', doc) 38 | node1 << Nokogiri::XML::Node.new('p', doc) 39 | node2 = Nokogiri::XML::Node.new('div', doc) 40 | node2 << Nokogiri::XML::Node.new('p', doc) 41 | 42 | expect(described_class.new(node1, node2).call).to be true 43 | end 44 | end 45 | 46 | context 'two nodes with a child of a different name' do 47 | it 'should not be equal' do 48 | node1 = Nokogiri::XML::Node.new('div', doc) 49 | node1 << Nokogiri::XML::Node.new('p', doc) 50 | node2 = Nokogiri::XML::Node.new('div', doc) 51 | node2 << Nokogiri::XML::Node.new('em', doc) 52 | 53 | expect(described_class.new(node1, node2).call).to be false 54 | end 55 | end 56 | 57 | context 'one node has a child more than the other, otherwise identical' do 58 | it 'should not be equal' do 59 | node1 = Nokogiri::XML::Node.new('div', doc) 60 | node1 << Nokogiri::XML::Node.new('p', doc) 61 | node2 = Nokogiri::XML::Node.new('div', doc) 62 | node2 << Nokogiri::XML::Node.new('p', doc) 63 | node2 << Nokogiri::XML::Node.new('em', doc) 64 | 65 | expect(described_class.new(node1, node2).call).to be false 66 | end 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /spec/unit/hexp/nokogiri/reader_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::Nokogiri::Reader do 4 | let(:doc) { Nokogiri::HTML::Document.new } 5 | 6 | 7 | 8 | end 9 | -------------------------------------------------------------------------------- /spec/unit/hexp/parse_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp, 'parse' do 4 | context 'with an empty document' do 5 | let(:html) { '' } 6 | 7 | it 'should raise an exception' do 8 | expect{ Hexp.parse(html) }.to raise_exception Hexp::ParseError 9 | end 10 | end 11 | 12 | it 'should parse a single tag' do 13 | expect(Hexp.parse('Hello!')).to eq H[:a, 'Hello!'] 14 | end 15 | 16 | it 'should parse nested tags' do 17 | expect(Hexp.parse('Ciao Bella')).to eq H[:a, ['Ciao ', H[:em, 'Bella']]] 18 | end 19 | 20 | it 'should parse attributes' do 21 | expect(Hexp.parse('Ciao Bella')).to eq H[:a, {href: 'pretty'}, 'Ciao Bella'] 22 | end 23 | 24 | it 'should parse style tags' do 25 | expect(Hexp.parse('')).to eq( 26 | H[:html, 27 | H[:head, 28 | H[:style, {type: 'text/css'}, 'h1 {font-weigth: 400;}'] 29 | ] 30 | ] 31 | ) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/unit/hexp/text_node_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::TextNode do 4 | subject { described_class.new('some string') } 5 | 6 | describe 'Node triplet' do 7 | its(:tag) { should be_nil } 8 | its(:attributes) { should eq({}) } 9 | its(:attributes) { should be_frozen } 10 | its(:children) { should eq([]) } 11 | its(:children) { should be_frozen } 12 | end 13 | 14 | describe 'DSL methods' do 15 | its(:text?) { should be true } 16 | its(:rewrite) { should eq(subject) } 17 | its(:to_hexp) { should eq(subject) } 18 | 19 | describe 'class?' do 20 | it 'should always return false' do 21 | expect(subject.class?('strong')).to be false 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/unit/hexp/unparser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp::Unparser do 4 | let(:unparser) { described_class.new({}) } 5 | let(:node) { H[:p, %q{Hello "world", it's great meet & chat >.<}] } 6 | let(:html) { unparser.call(node) } 7 | 8 | it 'should escape sensitive characters' do 9 | expect(html).to eql '

Hello "world", it's great meet & chat >.<

' 10 | end 11 | 12 | context 'inside a script tag' do 13 | let(:node) { H[:script, %q{Hello "world", }, %q{it's great meet & chat >.<}] } 14 | 15 | it 'should not escape' do 16 | expect(html).to eql %q{} 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/unit/hexp_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hexp, 'Array' do 4 | context 'with an array as input' do 5 | it 'should return the array' do 6 | expect(Hexp.Array([:foo])).to eq([:foo]) 7 | end 8 | end 9 | 10 | context 'with a single object as an input' do 11 | it 'should wrap it in an array' do 12 | expect(Hexp.Array(:foo)).to eq([:foo]) 13 | end 14 | end 15 | 16 | context 'with an object that responds to to_ary' do 17 | let(:array_like) do 18 | Class.new { def to_ary; [1,2,3] ; end }.new 19 | end 20 | 21 | it 'should return the result of to_ary' do 22 | expect(Hexp.Array(array_like)).to eq([1,2,3]) 23 | end 24 | end 25 | end 26 | 27 | describe Hexp, 'build' do 28 | it 'should delegate to Hexp::Builder.new' do 29 | block = proc {} 30 | expect(Hexp::Builder).to receive(:new).with(:div, class: 'moambe', &block) 31 | Hexp.build(:div, class: 'moambe', &block) 32 | end 33 | end 34 | --------------------------------------------------------------------------------