From: Dan Book Date: Fri, 30 Oct 2015 05:43:46 +0000 (-0400) Subject: code, tests, docs X-Git-Tag: v0.001~26 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=catagits%2FDOM-Tiny.git;a=commitdiff_plain;h=d6512b506041e5f51cb53585efc6823ec5f3b109 code, tests, docs --- diff --git a/Build.PL b/Build.PL new file mode 100644 index 0000000..237996a --- /dev/null +++ b/Build.PL @@ -0,0 +1,114 @@ +# This Build.PL for DOM-Tiny was generated by +# Dist::Zilla::Plugin::ModuleBuildTiny::Fallback 0.020 +use strict; +use warnings; + +my %configure_requires = ( + 'Module::Build::Tiny' => '0.034', +); + +my @missing = grep { + ! eval "require $_; $_->VERSION($configure_requires{$_}); 1" +} keys %configure_requires; + +if (not @missing) +{ + # This section for DOM-Tiny was generated by Dist::Zilla::Plugin::ModuleBuildTiny 0.014. + use strict; + use warnings; + + use 5.010001; + # use Module::Build::Tiny 0.034; + Module::Build::Tiny::Build_PL(); +} +else +{ + if (not $ENV{PERL_MB_FALLBACK_SILENCE_WARNING}) + { + warn <<'EOW'; +*** WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING *** + +If you're seeing this warning, your toolchain is really, really old* and you'll +almost certainly have problems installing CPAN modules from this century. But +never fear, dear user, for we have the technology to fix this! + +If you're using CPAN.pm to install things, then you can upgrade it using: + + cpan CPAN + +If you're using CPANPLUS to install things, then you can upgrade it using: + + cpanp CPANPLUS + +If you're using cpanminus, you shouldn't be seeing this message in the first +place, so please file an issue on github. + +This public service announcement was brought to you by the Perl Toolchain +Gang, the irc.perl.org #toolchain IRC channel, and the number 42. + +---- + +* Alternatively, you are running this file manually, in which case you need +to learn to first fulfill all configure requires prerequisites listed in +META.yml or META.json -- or use a cpan client to install this distribution. + +You can also silence this warning for future installations by setting the +PERL_MB_FALLBACK_SILENCE_WARNING environment variable, but please don't do +that until you fix your toolchain as described above. + +EOW + sleep 10 if -t STDIN && (-t STDOUT || !(-f STDOUT || -c STDOUT)); + } + + + # This section was automatically generated by Dist::Zilla::Plugin::ModuleBuild v5.040. + use strict; + use warnings; + + require Module::Build; Module::Build->VERSION(0.28); + + + my %module_build_args = ( + "configure_requires" => { + "Module::Build::Tiny" => "0.034" + }, + "dist_abstract" => "Minimalistic HTML/XML DOM parser with CSS selectors", + "dist_author" => [ + "Dan Book " + ], + "dist_name" => "DOM-Tiny", + "dist_version" => "0.001", + "license" => "artistic_2", + "module_name" => "DOM::Tiny", + "recursive_test_files" => 1, + "requires" => { + "Carp" => 0, + "Class::Tiny::Chained" => 0, + "Exporter" => 0, + "List::Util" => 0, + "Scalar::Util" => 0, + "perl" => "5.010001" + }, + "test_requires" => { + "JSON::Tiny" => "0.41", + "Test::More" => "0.88" + } + ); + + + my %fallback_build_requires = ( + "JSON::Tiny" => "0.41", + "Test::More" => "0.88" + ); + + + unless ( eval { Module::Build->VERSION(0.4004) } ) { + delete $module_build_args{test_requires}; + $module_build_args{build_requires} = \%fallback_build_requires; + } + + my $build = Module::Build->new(%module_build_args); + + + $build->create_build_script; +} diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..17283c8 --- /dev/null +++ b/INSTALL @@ -0,0 +1,43 @@ +This is the Perl distribution DOM-Tiny. + +Installing DOM-Tiny is straightforward. + +## Installation with cpanm + +If you have cpanm, you only need one line: + + % cpanm DOM::Tiny + +If you are installing into a system-wide directory, you may need to pass the +"-S" flag to cpanm, which uses sudo to install the module: + + % cpanm -S DOM::Tiny + +## Installing with the CPAN shell + +Alternatively, if your CPAN shell is set up, you should just be able to do: + + % cpan DOM::Tiny + +## Manual installation + +As a last resort, you can manually install it. Download the tarball, untar it, +then build it: + + % perl Build.PL + % ./Build && ./Build test + +Then install it: + + % ./Build install + +If you are installing into a system-wide directory, you may need to run: + + % sudo ./Build install + +## Documentation + +DOM-Tiny documentation is available as POD. +You can run perldoc from a shell to read the documentation: + + % perldoc DOM::Tiny diff --git a/META.json b/META.json new file mode 100644 index 0000000..4b7d925 --- /dev/null +++ b/META.json @@ -0,0 +1,84 @@ +{ + "abstract" : "Minimalistic HTML/XML DOM parser with CSS selectors", + "author" : [ + "Dan Book " + ], + "dynamic_config" : 0, + "generated_by" : "Dist::Zilla version 5.040, CPAN::Meta::Converter version 2.150005", + "license" : [ + "artistic_2" + ], + "meta-spec" : { + "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", + "version" : 2 + }, + "name" : "DOM-Tiny", + "no_index" : { + "directory" : [ + "eg", + "examples", + "inc", + "share", + "t", + "xt" + ] + }, + "prereqs" : { + "configure" : { + "requires" : { + "Module::Build::Tiny" : "0.034" + } + }, + "develop" : { + "requires" : { + "Pod::Coverage::TrustPod" : "0", + "Test::Pod" : "1.41", + "Test::Pod::Coverage" : "1.08" + } + }, + "runtime" : { + "requires" : { + "Carp" : "0", + "Class::Tiny::Chained" : "0", + "Exporter" : "0", + "List::Util" : "0", + "Scalar::Util" : "0", + "perl" : "5.010001" + } + }, + "test" : { + "requires" : { + "JSON::Tiny" : "0.41", + "Test::More" : "0.88" + } + } + }, + "provides" : { + "DOM::Tiny" : { + "file" : "lib/DOM/Tiny.pm", + "version" : "0.001" + }, + "DOM::Tiny::CSS" : { + "file" : "lib/DOM/Tiny/CSS.pm", + "version" : "0.001" + }, + "DOM::Tiny::Collection" : { + "file" : "lib/DOM/Tiny/Collection.pm", + "version" : "0.001" + }, + "DOM::Tiny::Entities" : { + "file" : "lib/DOM/Tiny/Entities.pm", + "version" : "0.001" + }, + "DOM::Tiny::HTML" : { + "file" : "lib/DOM/Tiny/HTML.pm", + "version" : "0.001" + } + }, + "release_status" : "stable", + "version" : "0.001", + "x_contributors" : [ + "Dan Book " + ] +} + diff --git a/README.pod b/README.pod new file mode 100644 index 0000000..2f6ea69 --- /dev/null +++ b/README.pod @@ -0,0 +1,696 @@ +=pod + +=encoding utf8 + +=head1 NAME + +DOM::Tiny - Minimalistic HTML/XML DOM parser with CSS selectors + +=head1 SYNOPSIS + + use DOM::Tiny; + + # Parse + my $dom = DOM::Tiny->new('

Test

123

'); + + # Find + say $dom->at('#b')->text; + say $dom->find('p')->map('text')->join("\n"); + say $dom->find('[id]')->map(attr => 'id')->join("\n"); + + # Iterate + $dom->find('p[id]')->reverse->each(sub { say $_->{id} }); + + # Loop + for my $e ($dom->find('p[id]')->each) { + say $e->{id}, ':', $e->text; + } + + # Modify + $dom->find('div p')->last->append('

456

'); + $dom->find(':not(p)')->map('strip'); + + # Render + say "$dom"; + +=head1 DESCRIPTION + +L is a minimalistic and relaxed HTML/XML DOM parser with CSS +selector support based on L. It will even try to interpret broken +HTML and XML, so you should not use it for validation. + +=head1 NODES AND ELEMENTS + +When we parse an HTML/XML fragment, it gets turned into a tree of nodes. + + + + Hello + World! + + +There are currently eight different kinds of nodes, C, C, +C, C, C, C, C and C. Elements are nodes of +the type C. + + root + |- doctype (html) + +- tag (html) + |- tag (head) + | +- tag (title) + | +- raw (Hello) + +- tag (body) + +- text (World!) + +While all node types are represented as L objects, some methods like +L and L only apply to elements. + +=head1 CASE-SENSITIVITY + +L defaults to HTML semantics, that means all tags and attribute +names are lowercased and selectors need to be lowercase as well. + + # HTML semantics + my $dom = DOM::Tiny->new('

Hi!

'); + say $dom->at('p[id]')->text; + +If XML processing instructions are found, the parser will automatically switch +into XML mode and everything becomes case-sensitive. + + # XML semantics + my $dom = DOM::Tiny->new('

Hi!

'); + say $dom->at('P[ID]')->text; + +XML detection can also be disabled with the L method. + + # Force XML semantics + my $dom = DOM::Tiny->new->xml(1)->parse('

Hi!

'); + say $dom->at('P[ID]')->text; + + # Force HTML semantics + my $dom = DOM::Tiny->new->xml(0)->parse('

Hi!

'); + say $dom->at('p[id]')->text; + +=head1 METHODS + +L implements the following methods. + +=head2 all_text + + my $trimmed = $dom->all_text; + my $untrimmed = $dom->all_text(0); + +Extract text content from all descendant nodes of this element, smart +whitespace trimming is enabled by default. + + # "foo bar baz" + $dom->parse("
foo\n

bar

baz\n
")->at('div')->all_text; + + # "foo\nbarbaz\n" + $dom->parse("
foo\n

bar

baz\n
")->at('div')->all_text(0); + +=head2 ancestors + + my $collection = $dom->ancestors; + my $collection = $dom->ancestors('div ~ p'); + +Find all ancestor elements of this node matching the CSS selector and return a +L object containing these elements as L +objects. All selectors from L are supported. + + # List tag names of ancestor elements + say $dom->ancestors->map('tag')->join("\n"); + +=head2 append + + $dom = $dom->append('

I ♥ DOM::Tiny!

'); + +Append HTML/XML fragment to this node. + + # "

Test

123

" + $dom->parse('

Test

') + ->at('h1')->append('

123

')->root; + + # "

Test 123

" + $dom->parse('

Test

')->at('p') + ->child_nodes->first->append(' 123')->root; + +=head2 append_content + + $dom = $dom->append_content('

I ♥ DOM::Tiny!

'); + +Append HTML/XML fragment (for C and C nodes) or raw content to this +node's content. + + # "

Test123

" + $dom->parse('

Test

') + ->at('h1')->append_content('123')->root; + + # "
" + $dom->parse('
') + ->child_nodes->first->append_content('123 ')->root; + + # "

Test123

" + $dom->parse('

Test

')->at('p')->append_content('123')->root; + +=head2 at + + my $result = $dom->at('div ~ p'); + +Find first descendant element of this element matching the CSS selector and +return it as a L object or return C if none could be found. +All selectors from L are supported. + + # Find first element with "svg" namespace definition + my $namespace = $dom->at('[xmlns\:svg]')->{'xmlns:svg'}; + +=head2 attr + + my $hash = $dom->attr; + my $foo = $dom->attr('foo'); + $dom = $dom->attr({foo => 'bar'}); + $dom = $dom->attr(foo => 'bar'); + +This element's attributes. + + # Remove an attribute + delete $dom->attr->{id}; + + # Attribute without value + $dom->attr(selected => undef); + + # List id attributes + say $dom->find('*')->map(attr => 'id')->compact->join("\n"); + +=head2 child_nodes + + my $collection = $dom->child_nodes; + +Return a L object containing all child nodes of this +element as L objects. + + # "

123

" + $dom->parse('

Test123

')->at('p')->child_nodes->first->remove; + + # "" + $dom->parse('123')->child_nodes->first; + + # " Test " + $dom->parse('123')->child_nodes->last->content; + +=head2 children + + my $collection = $dom->children; + my $collection = $dom->children('div ~ p'); + +Find all child elements of this element matching the CSS selector and return a +L object containing these elements as L +objects. All selectors from L are supported. + + # Show tag name of random child element + say $dom->children->shuffle->first->tag; + +=head2 content + + my $str = $dom->content; + $dom = $dom->content('

I ♥ DOM::Tiny!

'); + +Return this node's content or replace it with HTML/XML fragment (for C +and C nodes) or raw content. + + # "Test" + $dom->parse('
Test
')->at('div')->content; + + # "

123

" + $dom->parse('

Test

')->at('h1')->content('123')->root; + + # "

123

" + $dom->parse('

Test

')->at('p')->content('123')->root; + + # "

" + $dom->parse('

Test

')->at('h1')->content('')->root; + + # " Test " + $dom->parse('
')->child_nodes->first->content; + + # "
456
" + $dom->parse('
456
') + ->at('div')->child_nodes->first->content(' 123 ')->root; + +=head2 descendant_nodes + + my $collection = $dom->descendant_nodes; + +Return a L object containing all descendant nodes of +this element as L objects. + + # "

123

" + $dom->parse('

123

') + ->descendant_nodes->grep(sub { $_->type eq 'comment' }) + ->map('remove')->first; + + # "

testtest

" + $dom->parse('

123456

') + ->at('p')->descendant_nodes->grep(sub { $_->type eq 'text' }) + ->map(content => 'test')->first->root; + +=head2 find + + my $collection = $dom->find('div ~ p'); + +Find all descendant elements of this element matching the CSS selector and +return a L object containing these elements as +L objects. All selectors from L are +supported. + + # Find a specific element and extract information + my $id = $dom->find('div')->[23]{id}; + + # Extract information from multiple elements + my @headers = $dom->find('h1, h2, h3')->map('text')->each; + + # Count all the different tags + my $hash = $dom->find('*')->reduce(sub { $a->{$b->tag}++; $a }, {}); + + # Find elements with a class that contains dots + my @divs = $dom->find('div.foo\.bar')->each; + +=head2 following + + my $collection = $dom->following; + my $collection = $dom->following('div ~ p'); + +Find all sibling elements after this node matching the CSS selector and return +a L object containing these elements as L +objects. All selectors from L are supported. + + # List tags of sibling elements after this node + say $dom->following->map('tag')->join("\n"); + +=head2 following_nodes + + my $collection = $dom->following_nodes; + +Return a L object containing all sibling nodes after +this node as L objects. + + # "C" + $dom->parse('

A

C')->at('p')->following_nodes->last->content; + +=head2 matches + + my $bool = $dom->matches('div ~ p'); + +Check if this element matches the CSS selector. All selectors from +L are supported. + + # True + $dom->parse('

A

')->at('p')->matches('.a'); + $dom->parse('

A

')->at('p')->matches('p[class]'); + + # False + $dom->parse('

A

')->at('p')->matches('.b'); + $dom->parse('

A

')->at('p')->matches('p[id]'); + +=head2 namespace + + my $namespace = $dom->namespace; + +Find this element's namespace or return C if none could be found. + + # Find namespace for an element with namespace prefix + my $namespace = $dom->at('svg > svg\:circle')->namespace; + + # Find namespace for an element that may or may not have a namespace prefix + my $namespace = $dom->at('svg > circle')->namespace; + +=head2 new + + my $dom = DOM::Tiny->new; + my $dom = DOM::Tiny->new('I ♥ DOM::Tiny!'); + +Construct a new scalar-based L object and L HTML/XML +fragment if necessary. + +=head2 next + + my $sibling = $dom->next; + +Return L object for next sibling element or C if there are no +more siblings. + + # "

123

" + $dom->parse('

Test

123

')->at('h1')->next; + +=head2 next_node + + my $sibling = $dom->next_node; + +Return L object for next sibling node or C if there are no +more siblings. + + # "456" + $dom->parse('

123456

') + ->at('b')->next_node->next_node; + + # " Test " + $dom->parse('

123456

') + ->at('b')->next_node->content; + +=head2 parent + + my $parent = $dom->parent; + +Return L object for parent of this node or C if this node has +no parent. + +=head2 parse + + $dom = $dom->parse('I ♥ DOM::Tiny!'); + +Parse HTML/XML fragment with L. + + # Parse XML + my $dom = DOM::Tiny->new->xml(1)->parse($xml); + +=head2 preceding + + my $collection = $dom->preceding; + my $collection = $dom->preceding('div ~ p'); + +Find all sibling elements before this node matching the CSS selector and return +a L object containing these elements as L +objects. All selectors from L are supported. + + # List tags of sibling elements before this node + say $dom->preceding->map('tag')->join("\n"); + +=head2 preceding_nodes + + my $collection = $dom->preceding_nodes; + +Return a L object containing all sibling nodes before +this node as L objects. + + # "A" + $dom->parse('A

C

')->at('p')->preceding_nodes->first->content; + +=head2 prepend + + $dom = $dom->prepend('

I ♥ DOM::Tiny!

'); + +Prepend HTML/XML fragment to this node. + + # "

Test

123

" + $dom->parse('

123

') + ->at('h2')->prepend('

Test

')->root; + + # "

Test 123

" + $dom->parse('

123

') + ->at('p')->child_nodes->first->prepend('Test ')->root; + +=head2 prepend_content + + $dom = $dom->prepend_content('

I ♥ DOM::Tiny!

'); + +Prepend HTML/XML fragment (for C and C nodes) or raw content to this +node's content. + + # "

Test123

" + $dom->parse('

123

') + ->at('h2')->prepend_content('Test')->root; + + # "
" + $dom->parse('
') + ->child_nodes->first->prepend_content(' Test')->root; + + # "

123Test

" + $dom->parse('

Test

')->at('p')->prepend_content('123')->root; + +=head2 previous + + my $sibling = $dom->previous; + +Return L object for previous sibling element or C if there +are no more siblings. + + # "

Test

" + $dom->parse('

Test

123

')->at('h2')->previous; + +=head2 previous_node + + my $sibling = $dom->previous_node; + +Return L object for previous sibling node or C if there are +no more siblings. + + # "123" + $dom->parse('

123456

') + ->at('b')->previous_node->previous_node; + + # " Test " + $dom->parse('

123456

') + ->at('b')->previous_node->content; + +=head2 remove + + my $parent = $dom->remove; + +Remove this node and return L (for C nodes) or L. + + # "
" + $dom->parse('

Test

')->at('h1')->remove; + + # "

456

" + $dom->parse('

123456

') + ->at('p')->child_nodes->first->remove->root; + +=head2 replace + + my $parent = $dom->replace('
I ♥ DOM::Tiny!
'); + +Replace this node with HTML/XML fragment and return L (for C +nodes) or L. + + # "

123

" + $dom->parse('

Test

')->at('h1')->replace('

123

'); + + # "

123

" + $dom->parse('

Test

') + ->at('p')->child_nodes->[0]->replace('123')->root; + +=head2 root + + my $root = $dom->root; + +Return L object for C node. + +=head2 strip + + my $parent = $dom->strip; + +Remove this element while preserving its content and return L. + + # "
Test
" + $dom->parse('

Test

')->at('h1')->strip; + +=head2 tag + + my $tag = $dom->tag; + $dom = $dom->tag('div'); + +This element's tag name. + + # List tag names of child elements + say $dom->children->map('tag')->join("\n"); + +=head2 tap + + $dom = $dom->tap(sub {...}); + +Alias for L. + +=head2 text + + my $trimmed = $dom->text; + my $untrimmed = $dom->text(0); + +Extract text content from this element only (not including child elements), +smart whitespace trimming is enabled by default. + + # "foo baz" + $dom->parse("
foo\n

bar

baz\n
")->at('div')->text; + + # "foo\nbaz\n" + $dom->parse("
foo\n

bar

baz\n
")->at('div')->text(0); + +=head2 to_string + + my $str = $dom->to_string; + +Render this node and its content to HTML/XML. + + # "Test" + $dom->parse('
Test
')->at('div b')->to_string; + +=head2 tree + + my $tree = $dom->tree; + $dom = $dom->tree(['root']); + +Document Object Model. Note that this structure should only be used very +carefully since it is very dynamic. + +=head2 type + + my $type = $dom->type; + +This node's type, usually C, C, C, C, C, +C, C or C. + + # "cdata" + $dom->parse('')->child_nodes->first->type; + + # "comment" + $dom->parse('')->child_nodes->first->type; + + # "doctype" + $dom->parse('')->child_nodes->first->type; + + # "pi" + $dom->parse('')->child_nodes->first->type; + + # "raw" + $dom->parse('Test')->at('title')->child_nodes->first->type; + + # "root" + $dom->parse('

Test

')->type; + + # "tag" + $dom->parse('

Test

')->at('p')->type; + + # "text" + $dom->parse('

Test

')->at('p')->child_nodes->first->type; + +=head2 val + + my $value = $dom->val; + +Extract value from form element (such as C + + +EOF +is $dom->at('p')->val, undef, 'no value'; +is $dom->at('input')->val, 'A', 'right value'; +is $dom->at('input:checked')->val, 'B', 'right value'; +is $dom->at('input:checked[type=radio]')->val, 'C', 'right value'; +is_deeply $dom->at('select')->val, ['I', 'J'], 'right values'; +is $dom->at('select option')->val, 'F', 'right value'; +is $dom->at('select optgroup option:not([selected])')->val, 'H', 'right value'; +is $dom->find('select')->[1]->at('option')->val, 'N', 'right value'; +is $dom->find('select')->[1]->val, undef, 'no value'; +is_deeply $dom->find('select')->[2]->val, undef, 'no value'; +is $dom->find('select')->[2]->at('option')->val, 'Q', 'right value'; +is_deeply $dom->find('select')->last->val, 'D', 'right value'; +is_deeply $dom->find('select')->last->at('option')->val, 'R', 'right value'; +is $dom->at('textarea')->val, 'M', 'right value'; +is $dom->at('button')->val, 'O', 'right value'; +is $dom->find('form input')->last->val, 'P', 'right value'; + +# PoCo example with whitespace sensitive text +$dom = DOM::Tiny->new(< + + + 1286823 + Homer Simpson + + home + + + + + 1286822 + Marge Simpson + + home + 742 Evergreen Terrace +Springfield, VT 12345 USA + + + +EOF +is $dom->find('entry')->[0]->at('displayName')->text, 'Homer Simpson', + 'right text'; +is $dom->find('entry')->[0]->at('id')->text, '1286823', 'right text'; +is $dom->find('entry')->[0]->at('addresses')->children('type')->[0]->text, + 'home', 'right text'; +is $dom->find('entry')->[0]->at('addresses formatted')->text, + "742 Evergreen Terrace\nSpringfield, VT 12345 USA", 'right text'; +is $dom->find('entry')->[0]->at('addresses formatted')->text(0), + "742 Evergreen Terrace\nSpringfield, VT 12345 USA", 'right text'; +is $dom->find('entry')->[1]->at('displayName')->text, 'Marge Simpson', + 'right text'; +is $dom->find('entry')->[1]->at('id')->text, '1286822', 'right text'; +is $dom->find('entry')->[1]->at('addresses')->children('type')->[0]->text, + 'home', 'right text'; +is $dom->find('entry')->[1]->at('addresses formatted')->text, + '742 Evergreen Terrace Springfield, VT 12345 USA', 'right text'; +is $dom->find('entry')->[1]->at('addresses formatted')->text(0), + "742 Evergreen Terrace\nSpringfield, VT 12345 USA", 'right text'; +is $dom->find('entry')->[2], undef, 'no result'; +is $dom->find('entry')->size, 2, 'right number of elements'; + +# Find attribute with hyphen in name and value +$dom = DOM::Tiny->new(< + + +EOF +is $dom->find('[http-equiv]')->[0]{content}, 'text/html', 'right attribute'; +is $dom->find('[http-equiv]')->[1], undef, 'no result'; +is $dom->find('[http-equiv="content-type"]')->[0]{content}, 'text/html', + 'right attribute'; +is $dom->find('[http-equiv="content-type"]')->[1], undef, 'no result'; +is $dom->find('[http-equiv^="content-"]')->[0]{content}, 'text/html', + 'right attribute'; +is $dom->find('[http-equiv^="content-"]')->[1], undef, 'no result'; +is $dom->find('head > [http-equiv$="-type"]')->[0]{content}, 'text/html', + 'right attribute'; +is $dom->find('head > [http-equiv$="-type"]')->[1], undef, 'no result'; + +# Find "0" attribute value +$dom = DOM::Tiny->new(<Zero +O&gTn>e +EOF +is $dom->find('a[accesskey]')->[0]->text, 'Zero', 'right text'; +is $dom->find('a[accesskey]')->[1]->text, 'O&gTn>e', 'right text'; +is $dom->find('a[accesskey]')->[2], undef, 'no result'; +is $dom->find('a[accesskey=0]')->[0]->text, 'Zero', 'right text'; +is $dom->find('a[accesskey=0]')->[1], undef, 'no result'; +is $dom->find('a[accesskey^=0]')->[0]->text, 'Zero', 'right text'; +is $dom->find('a[accesskey^=0]')->[1], undef, 'no result'; +is $dom->find('a[accesskey$=0]')->[0]->text, 'Zero', 'right text'; +is $dom->find('a[accesskey$=0]')->[1], undef, 'no result'; +is $dom->find('a[accesskey~=0]')->[0]->text, 'Zero', 'right text'; +is $dom->find('a[accesskey~=0]')->[1], undef, 'no result'; +is $dom->find('a[accesskey*=0]')->[0]->text, 'Zero', 'right text'; +is $dom->find('a[accesskey*=0]')->[1], undef, 'no result'; +is $dom->find('a[accesskey=1]')->[0]->text, 'O&gTn>e', 'right text'; +is $dom->find('a[accesskey=1]')->[1], undef, 'no result'; +is $dom->find('a[accesskey^=1]')->[0]->text, 'O&gTn>e', 'right text'; +is $dom->find('a[accesskey^=1]')->[1], undef, 'no result'; +is $dom->find('a[accesskey$=1]')->[0]->text, 'O&gTn>e', 'right text'; +is $dom->find('a[accesskey$=1]')->[1], undef, 'no result'; +is $dom->find('a[accesskey~=1]')->[0]->text, 'O&gTn>e', 'right text'; +is $dom->find('a[accesskey~=1]')->[1], undef, 'no result'; +is $dom->find('a[accesskey*=1]')->[0]->text, 'O&gTn>e', 'right text'; +is $dom->find('a[accesskey*=1]')->[1], undef, 'no result'; +is $dom->at('a[accesskey*="."]'), undef, 'no result'; + +# Empty attribute value +$dom = DOM::Tiny->new(< + test + +after +EOF +is $dom->tree->[0], 'root', 'right type'; +is $dom->tree->[1][0], 'tag', 'right type'; +is $dom->tree->[1][1], 'foo', 'right tag'; +is_deeply $dom->tree->[1][2], {bar => ''}, 'right attributes'; +is $dom->tree->[1][4][0], 'text', 'right type'; +is $dom->tree->[1][4][1], "\n test\n", 'right text'; +is $dom->tree->[3][0], 'tag', 'right type'; +is $dom->tree->[3][1], 'bar', 'right tag'; +is $dom->tree->[3][4][0], 'text', 'right type'; +is $dom->tree->[3][4][1], 'after', 'right text'; +is "$dom", < + test + +after +EOF + +# Case-insensitive attribute values +$dom = DOM::Tiny->new(<A

+

B

+

C

+EOF +is $dom->find('.foo')->map('text')->join(','), 'A,B', 'right result'; +is $dom->find('.FOO')->map('text')->join(','), 'C', 'right result'; +is $dom->find('[class=foo]')->map('text')->join(','), 'A', 'right result'; +is $dom->find('[class=foo i]')->map('text')->join(','), 'A,C', 'right result'; +is $dom->find('[class="foo" i]')->map('text')->join(','), 'A,C', 'right result'; +is $dom->find('[class="foo bar"]')->size, 0, 'no results'; +is $dom->find('[class="foo bar" i]')->map('text')->join(','), 'B', + 'right result'; +is $dom->find('[class~=foo]')->map('text')->join(','), 'A,B', 'right result'; +is $dom->find('[class~=foo i]')->map('text')->join(','), 'A,B,C', + 'right result'; +is $dom->find('[class*=f]')->map('text')->join(','), 'A,B', 'right result'; +is $dom->find('[class*=f i]')->map('text')->join(','), 'A,B,C', 'right result'; +is $dom->find('[class^=F]')->map('text')->join(','), 'C', 'right result'; +is $dom->find('[class^=F i]')->map('text')->join(','), 'A,B,C', 'right result'; +is $dom->find('[class$=O]')->map('text')->join(','), 'C', 'right result'; +is $dom->find('[class$=O i]')->map('text')->join(','), 'A,C', 'right result'; + +# Nested description lists +$dom = DOM::Tiny->new(< +
A
+
+
+
B +
C +
+
+ +EOF +is $dom->find('dl > dd > dl > dt')->[0]->text, 'B', 'right text'; +is $dom->find('dl > dd > dl > dd')->[0]->text, 'C', 'right text'; +is $dom->find('dl > dt')->[0]->text, 'A', 'right text'; + +# Nested lists +$dom = DOM::Tiny->new(< +
    +
  • + A +
      +
    • B
    • + C +
    +
  • +
+ +EOF +is $dom->find('div > ul > li')->[0]->text, 'A', 'right text'; +is $dom->find('div > ul > li')->[1], undef, 'no result'; +is $dom->find('div > ul li')->[0]->text, 'A', 'right text'; +is $dom->find('div > ul li')->[1]->text, 'B', 'right text'; +is $dom->find('div > ul li')->[2], undef, 'no result'; +is $dom->find('div > ul ul')->[0]->text, 'C', 'right text'; +is $dom->find('div > ul ul')->[1], undef, 'no result'; + +# Unusual order +$dom + = DOM::Tiny->new('Ok!'); +is $dom->at('a:not([href$=foo])[href^=h]')->text, 'Ok!', 'right text'; +is $dom->at('a:not([href$=example.com])[href^=h]'), undef, 'no result'; +is $dom->at('a[href^=h]#foo.bar')->text, 'Ok!', 'right text'; +is $dom->at('a[href^=h]#foo.baz'), undef, 'no result'; +is $dom->at('a[href^=h]#foo:not(b)')->text, 'Ok!', 'right text'; +is $dom->at('a[href^=h]#foo:not(a)'), undef, 'no result'; +is $dom->at('[href^=h].bar:not(b)[href$=m]#foo')->text, 'Ok!', 'right text'; +is $dom->at('[href^=h].bar:not(b)[href$=m]#bar'), undef, 'no result'; +is $dom->at(':not(b)#foo#foo')->text, 'Ok!', 'right text'; +is $dom->at(':not(b)#foo#bar'), undef, 'no result'; +is $dom->at(':not([href^=h]#foo#bar)')->text, 'Ok!', 'right text'; +is $dom->at(':not([href^=h]#foo#foo)'), undef, 'no result'; + +# Slash between attributes +$dom = DOM::Tiny->new('
'); +is_deeply $dom->at('input')->attr, + {type => 'checkbox', value => '/a/', checked => undef}, 'right attributes'; +is "$dom", '
', 'right result'; + +# Dot and hash in class and id attributes +$dom = DOM::Tiny->new('

A

B

'); +is $dom->at('p.a\#b\.c')->text, 'A', 'right text'; +is $dom->at(':not(p.a\#b\.c)')->text, 'B', 'right text'; +is $dom->at('p#a\#b\.c')->text, 'B', 'right text'; +is $dom->at(':not(p#a\#b\.c)')->text, 'A', 'right text'; + +# Extra whitespace +$dom = DOM::Tiny->new('< span>a< /span>bc'); +is $dom->at('span')->text, 'a', 'right text'; +is $dom->at('span + b')->text, 'b', 'right text'; +is $dom->at('b + span')->text, 'c', 'right text'; +is "$dom", 'abc', 'right result'; + +# Selectors with leading and trailing whitespace +$dom = DOM::Tiny->new('
works
'); +is $dom->at(' div b ')->text, 'works', 'right text'; +is $dom->at(' :not( #foo ) ')->text, 'works', 'right text'; + +# "0" +$dom = DOM::Tiny->new('0'); +is "$dom", '0', 'right result'; +$dom->append_content('☃'); +is "$dom", '0☃', 'right result'; +is $dom->parse(''), '', 'successful roundtrip'; +is $dom->parse(''), '', 'successful roundtrip'; +is $dom->parse(''), '', 'successful roundtrip'; +is $dom->parse(''), '', 'successful roundtrip'; + +# Not self-closing +$dom = DOM::Tiny->new('
< div >
test
123'); +is $dom->at('div > div > pre')->text, 'test', 'right text'; +is "$dom", '
test
123
', 'right result'; +$dom = DOM::Tiny->new('

'); +is $dom->find('p > svg > circle')->size, 2, 'two circles'; +is "$dom", '

', + 'right result'; + +# "image" +$dom = DOM::Tiny->new('test'); +is $dom->at('img')->{src}, 'foo.png', 'right attribute'; +is "$dom", 'test', 'right result'; + +# "title" +$dom = DOM::Tiny->new(' <p>test<'); +is $dom->at('title')->text, '

test<', 'right text'; +is "$dom", ' <p>test<', 'right result'; + +# "textarea" +$dom = DOM::Tiny->new(''); +is $dom->at('textarea#a')->text, '

test<', 'right text'; +is "$dom", '', 'right result'; + +# Comments +$dom = DOM::Tiny->new(< + +