Mojo::DOM58(3)
π Mojo::DOM58 β the manual, but make it cute π
okay bestie, gather round because this is Mojo::DOM58(3) from the User Contributed Perl Documentation and it's literally the most chill little HTML/XML parser you will ever meet. πΈ
β¨ NAME β¨
Mojo::DOM58 β a minimalistic HTML/XML DOM parser with CSS selectors. That's the whole vibe. Tiny. Relaxed. Slays. π
β¨ SYNOPSIS β¨
Here's the group-chat rundown of everything it does, no cap:
use Mojo::DOM58;
# Parse
my $dom = Mojo::DOM58->new('<div><p id="a">Test</p><p id="b">123</p></div>');
# 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('<p id="c">456</p>');
$dom->at('#c')->prepend($dom->new_tag('p', id => 'd', '789'));
$dom->find(':not(p)')->map('strip');
# Render
say "$dom";
parse it, find stuff in it, loop over it, change it, print it back out. it's giving full lifecycle. π
β¨ DESCRIPTION β¨
So Mojo::DOM58 is a minimalistic and relaxed pure-perl HTML/XML DOM parser based on Mojo::DOM. It's lowkey built on top of these three big-deal specs:
- the HTML Living Standard β
https://html.spec.whatwg.org/ - Extensible Markup Language (XML) 1.0 β
https://www.w3.org/TR/xml/ - and matching based on CSS3 selectors β
https://www.w3.org/TR/selectors/
It will even try to interpret broken HTML and XML β like, it'll attempt it, bestie, it's not gonna judge you β but that means you should not use it for validation. π It's a lover, not a hall monitor.
π FORK INFO π
Mojo::DOM58 is a fork of Mojo::DOM and it tracks features and fixes to stay closely compatible with upstream. The way that it's different is only in:
- the standalone format π
- and compatibility with Perl 5.8 π¦
Anything that's a bug or a patch and is not related to those two changes should go straight to the Mojolicious issue tracker. Don't @ us, @ them. π
Oh and FYI: this release of Mojo::DOM58 is up to date with version 9.40 of Mojolicious. Iconic. π
πΈ NODES AND ELEMENTS πΈ
Okay so here's the tea: when we parse an HTML/XML fragment, it gets turned into a tree of nodes. Like this:
<!DOCTYPE html>
<html>
<head><title>Hello</title></head>
<body>World!</body>
</html>
There are currently eight different kinds of nodes: cdata, comment, doctype, pi, raw, root, tag and text.
And Elements? Those are just nodes of the type tag. That's it. That's the whole distinction. π
root
|- doctype (html)
+- tag (html)
|- tag (head)
| +- tag (title)
| +- raw (Hello)
+- tag (body)
+- text (World!)
Now, all node types get represented as Mojo::DOM58 objects β so everyone's equal in the eyes of the constructor π β but some methods like attr and namespace only apply to elements. So don't call them on a text node and act surprised. π
π¦ HTML AND XML π¦
Mojo::DOM58 defaults to HTML semantics β and what that means is all tags and attribute names are lowercased, and selectors need to be lowercase as well. lowercase era, periodt.
# HTML semantics
my $dom = Mojo::DOM58->new('<P ID="greeting">Hi!</P>');
say $dom->at('p[id]')->text;
But! If an XML declaration is found, the parser will automatically switch into XML mode and everything becomes case-sensitive. suddenly caps matter, main character energy for every letter.
# XML semantics
my $dom = Mojo::DOM58->new('<?xml version="1.0"?><P ID="greeting">Hi!</P>');
say $dom->at('P[ID]')->text;
And you can also force HTML or XML semantics with the xml method if you're the kind of girl who likes to be in control: π
# Force HTML semantics
my $dom = Mojo::DOM58->new->xml(0)->parse('<P ID="greeting">Hi!</P>');
say $dom->at('p[id]')->text;
# Force XML semantics
my $dom = Mojo::DOM58->new->xml(1)->parse('<P ID="greeting">Hi!</P>');
say $dom->at('P[ID]')->text;
That's it for part one, bestie β and there's more coming, so don't go anywhere. π«Ά
β¨ SELECTORS β¨
Okay bestie, this is the part you've been waiting for π Mojo::DOM58 runs a CSS selector engine built on top of Mojo::DOM::CSS, and every single CSS selector that makes sense for a standalone parser is supported. So basically: if it works in your stylesheet brain, it works here. Let's go through them one by one, no skipping, we're eating the whole plate π
*β literally any element. The catch-all, the main character of selectors π
my $all = $dom->find('*');
Eβ an element of typeE.
my $title = $dom->at('title');
E[foo]β anEelement that has afooattribute.
my $links = $dom->find('a[href]');
E[foo="bar"]β anEelement whosefooattribute value is exactly equal tobar.
my $case_sensitive = $dom->find('input[type="hidden"]');
my $case_sensitive = $dom->find('input[type=hidden]');
E[foo="bar" i]β anEelement whosefooattribute value is exactly equal to any (ASCII-range) case-permutation ofbar. β οΈ Heads up bestie, this selector is EXPERIMENTAL and might change without warning!
my $case_insensitive = $dom->find('input[type="hidden" i]');
my $case_insensitive = $dom->find('input[type=hidden i]');
my $case_insensitive = $dom->find('input[class~="foo" i]');
This one is part of Selectors Level 4 (https://dev.w3.org/csswg/selectors-4), which is still a work in progress π±
E[foo="bar" s]β anEelement whosefooattribute value is exactly and case-sensitively equal tobar. β οΈ Also EXPERIMENTAL, might change without warning!
my $case_sensitive = $dom->find('input[type="hidden" s]');
Also part of Selectors Level 4 (https://dev.w3.org/csswg/selectors-4), still a work in progress π±
E[foo~="bar"]β anEelement whosefooattribute value is a list of whitespace-separated values, and one of them is exactly equal tobar.
my $foo = $dom->find('input[class~="foo"]');
my $foo = $dom->find('input[class~=foo]');
E[foo^="bar"]β anEelement whosefooattribute value begins exactly with the stringbar.
my $begins_with = $dom->find('input[name^="f"]');
my $begins_with = $dom->find('input[name^=f]');
E[foo$="bar"]β anEelement whosefooattribute value ends exactly with the stringbar.
my $ends_with = $dom->find('input[name$="o"]');
my $ends_with = $dom->find('input[name$=o]');
E[foo*="bar"]β anEelement whosefooattribute value contains the substringbar.
my $contains = $dom->find('input[name*="fo"]');
my $contains = $dom->find('input[name*=fo]');
E[foo|="en"]β anEelement whosefooattribute has a hyphen-separated list of values beginning (from the left) withen.
my $english = $dom->find('link[hreflang|=en]');
E:rootβ anEelement, the root of the document.
my $root = $dom->at(':root');
E:nth-child(n)β anEelement, then-thchild of its parent.
my $third = $dom->find('div:nth-child(3)');
my $odd = $dom->find('div:nth-child(odd)');
my $even = $dom->find('div:nth-child(even)');
my $top3 = $dom->find('div:nth-child(-n+3)');
E:nth-last-child(n)β anEelement, then-thchild of its parent, but counting from the last one.
my $third = $dom->find('div:nth-last-child(3)');
my $odd = $dom->find('div:nth-last-child(odd)');
my $even = $dom->find('div:nth-last-child(even)');
my $bottom3 = $dom->find('div:nth-last-child(-n+3)');
E:nth-of-type(n)β anEelement, then-thsibling of its type.
my $third = $dom->find('div:nth-of-type(3)');
my $odd = $dom->find('div:nth-of-type(odd)');
my $even = $dom->find('div:nth-of-type(even)');
my $top3 = $dom->find('div:nth-of-type(-n+3)');
E:nth-last-of-type(n)β anEelement, then-thsibling of its type, counting from the last one.
my $third = $dom->find('div:nth-last-of-type(3)');
my $odd = $dom->find('div:nth-last-of-type(odd)');
my $even = $dom->find('div:nth-last-of-type(even)');
my $bottom3 = $dom->find('div:nth-last-of-type(-n+3)');
E:first-childβ anEelement, the first child of its parent.
my $first = $dom->find('div p:first-child');
E:last-childβ anEelement, the last child of its parent.
my $last = $dom->find('div p:last-child');
E:first-of-typeβ anEelement, the first sibling of its type.
my $first = $dom->find('div p:first-of-type');
E:last-of-typeβ anEelement, the last sibling of its type.
my $last = $dom->find('div p:last-of-type');
E:only-childβ anEelement, the only child of its parent. So lonely π₯Ί
my $lonely = $dom->find('div p:only-child');
E:only-of-typeβ anEelement, the only sibling of its type.
my $lonely = $dom->find('div p:only-of-type');
E:emptyβ anEelement that has no children (text nodes count too, so no sneaky hidden text π).
my $empty = $dom->find(':empty');
E:any-linkβ alias forE:link. β οΈ EXPERIMENTAL, might change without warning! This selector is part of Selectors Level 4 (https://dev.w3.org/csswg/selectors-4), which is still a work in progress π±E:linkβ anEelement being the source anchor of a hyperlink whose target is not yet visited (:link) or already visited (:visited). But bestie, important: Mojo::DOM58 is not stateful, so:any-link,:linkand:visitedyield exactly the same results. No browsing history here, it's not keeping receipts π
my $links = $dom->find(':any-link');
my $links = $dom->find(':link');
my $links = $dom->find(':visited');
E:visitedβ alias forE:link.E:scopeβ anEelement that's a designated reference element. β οΈ EXPERIMENTAL, might change without warning!
my $scoped = $dom->find('a:not(:scope > a)');
my $scoped = $dom->find('div :scope p');
my $scoped = $dom->find('~ p');
This selector is part of Selectors Level 4 (https://dev.w3.org/csswg/selectors-4), which is still a work in progress π±
E:checkedβ a user interface elementEwhich is checked (for instance a radio-button or checkbox). Slay, checked β
my $input = $dom->find(':checked');
E.warningβ anEelement whose class iswarning.
my $warning = $dom->find('div.warning');
E#myidβ anEelement withIDequal tomyid.
my $foo = $dom->at('div#foo');
E:not(s1, s2)β anEelement that does not match either compound selectors1or compound selectors2. β οΈ Note that support for compound selectors is EXPERIMENTAL and might change without warning!
my $others = $dom->find('div p:not(:first-child, :last-child)');
Support for compound selectors was added as part of Selectors Level 4 (https://dev.w3.org/csswg/selectors-4), which is still a work in progress π±
E:is(s1, s2)β anEelement that matches compound selectors1and/or compound selectors2. β οΈ EXPERIMENTAL, might change without warning!
my $headers = $dom->find(':is(section, article, aside, nav) h1');
This selector is part of Selectors Level 4 (https://dev.w3.org/csswg/selectors-4), which is still a work in progress π±
E:has(rs1, rs2)β anEelement, if either of the relative selectorsrs1orrs2, when evaluated withEas the:scopeelements, match an element. β οΈ EXPERIMENTAL, might change without warning!
my $link = $dom->find('a:has(> img)');
This selector is part of Selectors Level 4 (https://dev.w3.org/csswg/selectors-4), which is still a work in progress π± And also, be aware that this feature is currently marked "at-risk", so there's a high chance it gets removed completely. Protect :has at all costs π
E:text(string_or_regex)β anEelement containing text content that substring matchesstring_or_regexcase-insensitively, or that regex matchesstring_or_regex. For regular expressions use the format:text(/.../). β οΈ EXPERIMENTAL, might change without warning!
# Substring match
my $login = $dom->find(':text(Log in)');
# Regex match
my $login = $dom->find(':text(/Log ?in/)');
# Regex match (case-insensitive)
my $login = $dom->find(':text(/(?i:Log ?in)/)');
This one is a custom selector for Mojo::DOM58, not part of any spec. Homegrown icon π
A|Eβ anEelement that belongs to the namespace aliasAfrom CSS Namespaces Module Level 3 (https://www.w3.org/TR/css-namespaces-3/). Key/value pairs passed to selector methods are used to declare namespace aliases.
my $elem = $dom->find('lq|elem', lq => 'http://example.com/q-markup');
Using an empty alias searches for an element that belongs to no namespace.
my $div = $dom->find('|div');
E Fβ anFelement that is a descendant of anEelement.
my $headlines = $dom->find('div h1');
E > Fβ anFelement that is a child of anEelement.
my $headlines = $dom->find('html > body > div > h1');
E + Fβ anFelement immediately preceded by anEelement.
my $second = $dom->find('h1 + h2');
E ~ Fβ anFelement preceded by anEelement.
my $second = $dom->find('h1 ~ h2');
E, F, Gβ elements of typeE,FandG. The group chat selector, everyone's invited π«Ά
my $headlines = $dom->find('h1, h2, h3');
E[foo=bar][bar=baz]β anEelement whose attributes match all of the following attribute selectors.
my $links = $dom->find('a[foo^=b][foo$=ar]');
π OPERATORS π
Mojo::DOM58 overloads the following operators, so your objects can vibe as other things too β¨
array
my @nodes = @$dom;
Alias for "child_nodes".
# "<!-- Test -->"
$dom->parse('<!-- Test --><b>123</b>')->[0];
bool
my $bool = !!$dom;
Always true. Iconic behaviour honestly π
hash
my %attrs = %$dom;
Alias for "attr".
# "test"
$dom->parse('<div id="test">Test</div>')->at('div')->{id};
stringify
my $str = "$dom";
Alias for "to_string".
π FUNCTIONS π
Mojo::DOM58 implements the following functions, and you can import them individually, bestie π
tag_to_html
my $str = tag_to_html 'div', id => 'foo', 'safe content';
Generate an HTML/XML tag and render it right away. This is a significantly faster alternative to "new_tag" for template systems that have to generate a lot of tags. Speed queen behavior π
π METHODS π
Mojo::DOM58 implements the following methods. Let's meet them π
new
my $dom = Mojo::DOM58->new;
my $dom = Mojo::DOM58->new('<foo bar="baz">I β₯ Mojo::DOM58!</foo>');
Construct a new scalar-based Mojo::DOM58 object, and "parse" the HTML/XML fragment if necessary. Fresh out the box, no cap β¨
new_tag
my $tag = Mojo::DOM58->new_tag('div');
my $tag = $dom->new_tag('div');
my $tag = $dom->new_tag('div', id => 'foo', hidden => undef);
my $tag = $dom->new_tag('div', 'safe content');
my $tag = $dom->new_tag('div', id => 'foo', 'safe content');
my $tag = $dom->new_tag('div', data => {mojo => 'rocks'}, 'safe content');
my $tag = $dom->new_tag('div', id => 'foo', sub { 'unsafe content' });
okay so bestie, this one is literally the tag factory π
β it constructs a new Mojo::DOM58 object for an HTML/XML tag, with or without attributes and content. The data attribute may contain a hash reference with key/value pairs to generate attributes from. yes, you read that right, a whole hash of them, automatic.
Let's see the glow-ups in action:
# "<br>"
$dom->new_tag('br');
# "<div></div>"
$dom->new_tag('div');
# "<div id="foo" hidden></div>"
$dom->new_tag('div', id => 'foo', hidden => undef);
# "<div>test & 123</div>"
$dom->new_tag('div', 'test & 123');
# "<div id="foo">test & 123</div>"
$dom->new_tag('div', id => 'foo', 'test & 123');
# "<div data-foo="1" data-bar="test">test & 123</div>""
$dom->new_tag('div', data => {foo => 1, Bar => 'test'}, 'test & 123');
# "<div id="foo">test & 123</div>"
$dom->new_tag('div', id => 'foo', sub { 'test & 123' });
# "<div>Hello<b>Mojo!</b></div>"
$dom->parse('<div>Hello</div>')->at('div')
->append_content($dom->new_tag('b', 'Mojo!'))->root;
See how hidden => undef gives you a bare attribute with no value? iconic. And notice the content as a string comes back with & (that's the "safe content" line), while the content as a sub gives you a raw unescaped & (that's the "unsafe content" line) π. And the last one is a little combo move β you can shove a freshly-made tag straight into append_content and it slays.
all_text
my $text = $dom->all_text;
Extract text content from all descendant nodes of this element. For HTML documents, script and style elements are excluded β no cap, nobody wants a <script> tag's whole personality dumped into their text output π
# "foo\nbarbaz\n"
$dom->parse("<div>foo\n<p>bar</p>baz\n</div>")->at('div')->all_text;
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 collection containing these elements as Mojo::DOM58 objects. All selectors listed in SELECTORS are supported. the way that it just... walks up the family tree for you πΈ
# List tag names of ancestor elements
say $dom->ancestors->map('tag')->join("\n");
append
$dom = $dom->append('<p>I β₯ Mojo::DOM58!</p>');
$dom = $dom->append(Mojo::DOM58->new);
Append HTML/XML fragment to this node β and heads up, this works for all node types other than root.
# "<div><h1>Test</h1><h2>123</h2></div>"
$dom->parse('<div><h1>Test</h1></div>')
->at('h1')->append('<h2>123</h2>')->root;
# "<p>Test 123</p>"
$dom->parse('<p>Test</p>')->at('p')
->child_nodes->first->append(' 123')->root;
Look at that second one π«Ά β the text node got a sibling, not a child, and the <p> was like "we're matching now, periodt."
append_content
$dom = $dom->append_content('<p>I β₯ Mojo::DOM58!</p>');
$dom = $dom->append_content(Mojo::DOM58->new);
Append HTML/XML fragment (for root and tag nodes) or raw content to this node's content. It's giving "go inside, not next to" energy.
# "<div><h1>Test123</h1></div>"
$dom->parse('<div><h1>Test</h1></div>')
->at('h1')->append_content('123')->root;
# "<!-- Test 123 --><br>"
$dom->parse('<!-- Test --><br>')
->child_nodes->first->append_content('123 ')->root;
# "<p>Test<i>123</i></p>"
$dom->parse('<p>Test</p>')->at('p')->append_content('<i>123</i>')->root;
Yes, the 123 on a comment node comes out in front of the comment content β iykyk β¨
at
my $result = $dom->at('div ~ p');
my $result = $dom->at('svg|line', svg => 'http://www.w3.org/2000/svg');
Find the first descendant element of this element matching the CSS selector and return it as a Mojo::DOM58 object, or undef if none could be found. All selectors listed in SELECTORS are supported. fr fr, if it's not there you get undef, not a meltdown π
# Find first element with "svg" namespace definition
my $namespace = $dom->at('[xmlns\:svg]')->{'xmlns:svg'};
Trailing key/value pairs can be used to declare xml namespace aliases. π
# "<rect />"
$dom->parse('<svg xmlns="http://www.w3.org/2000/svg"><rect /></svg>')
->at('svg|rect', svg => 'http://www.w3.org/2000/svg');
attr
my $hash = $dom->attr;
my $foo = $dom->attr('foo');
$dom = $dom->attr({foo => 'bar'});
$dom = $dom->attr(foo => 'bar');
This element's attributes. That's it, that's the vibe β a whole hash of them, and you can grab, set, or nuke them like the main character you are π
# 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");
Setting an attribute to undef gives you a bare attribute with no value, and compact is right there to yeet the undefs before you print. obsessed. π
child_nodes
my $collection = $dom->child_nodes;
Return a collection containing all child nodes of this element as Mojo::DOM58 objects. nodes, bestie β text and comments included, not just elements.
# "<p><b>123</b></p>"
$dom->parse('<p>Test<b>123</b></p>')->at('p')->child_nodes->first->remove;
# "<!DOCTYPE html>"
$dom->parse('<!DOCTYPE html><b>123</b>')->child_nodes->first;
# " Test "
$dom->parse('<b>123</b><!-- Test -->')->child_nodes->last->content;
See? The first child of that <p> is the text Test, and remove just took it out. and the <!DOCTYPE html> counts as a child node too, which lowkey nobody expects π¦
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 collection containing these elements as Mojo::DOM58 objects. All selectors listed in SELECTORS are supported.
# Show tag name of random child element
say $dom->children->shuffle->first->tag;
children vs child_nodes β elements only vs everything. do not mix them up, it's giving rookie move π
content
my $str = $dom->content;
$dom = $dom->content('<p>I β₯ Mojo::DOM58!</p>');
$dom = $dom->content(Mojo::DOM58->new);
Return this node's content or replace it with HTML/XML fragment (for root and tag nodes) or raw content. getter and setter, one method, no extra charge π
# "<b>Test</b>"
$dom->parse('<div><b>Test</b></div>')->at('div')->content;
# "<div><h1>123</h1></div>"
$dom->parse('<div><h1>Test</h1></div>')->at('h1')->content('123')->root;
# "<p><i>123</i></p>"
$dom->parse('<p>Test</p>')->at('p')->content('<i>123</i>')->root;
# "<div><h1></h1></div>"
$dom->parse('<div><h1>Test</h1></div>')->at('h1')->content('')->root;
# " Test "
$dom->parse('<!-- Test --><br>')->child_nodes->first->content;
# "<div><!-- 123 -->456</div>"
$dom->parse('<div><!-- Test -->456</div>')
->at('div')->child_nodes->first->content(' 123 ')->root;
Setting it to an empty string wipes the content clean (iconic), and you can even rewrite a comment node's content. the range? unmatched. β¨
descendant_nodes
my $collection = $dom->descendant_nodes;
Return a collection containing all descendant nodes of this element as Mojo::DOM58 objects.
# "<p><b>123</b></p>"
$dom->parse('<p><!-- Test --><b>123<!-- 456 --></b></p>')
->descendant_nodes->grep(sub { $_->type eq 'comment' })
->map('remove')->first;
# "<p><b>test</b>test</p>"
$dom->parse('<p><b>123</b>456</p>')
->at('p')->descendant_nodes->grep(sub { $_->type eq 'text' })
->map(content => 'test')->first->root;
First one hunts down every comment node and removes it; second one finds every text node and rewrites it to test. grep plus map is a whole lifestyle π₯
find
my $collection = $dom->find('div ~ p');
my $collection = $dom->find('svg|line', svg => 'http://www.w3.org/2000/svg');
Find all descendant elements of this element matching the CSS selector and return a collection containing these elements as Mojo::DOM58 objects. All selectors listed in SELECTORS are supported. this is THE one, the main character method π
# 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;
Trailing key/value pairs can be used to declare xml namespace aliases.
# "<rect />"
$dom->parse('<svg xmlns="http://www.w3.org/2000/svg"><rect /></svg>')
->find('svg|rect', svg => 'http://www.w3.org/2000/svg')->first;
Also note the div.foo\.bar escape β a class with a literal dot in it needs that backslash or the selector gets confused. protect her. π
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 collection containing these elements as Mojo::DOM58 objects. All selectors listed in SELECTORS are supported.
# List tags of sibling elements after this node
say $dom->following->map('tag')->join("\n");
following_nodes
my $collection = $dom->following_nodes;
Return a collection containing all sibling nodes after this node as Mojo::DOM58 objects.
# "C"
$dom->parse('<p>A</p><!-- B -->C')->at('p')->following_nodes->last->content;
See how the last one is the text node C, not the comment? yeah. nodes. all of them. π
matches
my $bool = $dom->matches('div ~ p');
my $bool = $dom->matches('svg|line', svg => 'http://www.w3.org/2000/svg');
Check if this element matches the CSS selector. All selectors listed in SELECTORS are supported. Returns a boolean β true or false, no in-between, no "it's complicated" π
# True
$dom->parse('<p class="a">A</p>')->at('p')->matches('.a');
$dom->parse('<p class="a">A</p>')->at('p')->matches('p[class]');
# False
$dom->parse('<p class="a">A</p>')->at('p')->matches('.b');
$dom->parse('<p class="a">A</p>')->at('p')->matches('p[id]');
Trailing key/value pairs can be used to declare xml namespace aliases.
# True
$dom->parse('<svg xmlns="http://www.w3.org/2000/svg"><rect /></svg>')
->matches('svg|rect', svg => 'http://www.w3.org/2000/svg');
namespace
my $namespace = $dom->namespace;
Find this element's namespace, or return undef if none could be found. π
# "http://www.w3.org/2000/svg"
Mojo::DOM58->new('<svg xmlns:svg="http://www.w3.org/2000/svg"><svg:circle>3.14</svg:circle></svg>')->at('svg\:circle')->namespace;
# 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;
yes, that 3.14 is just sitting in there being cute and you're gonna have to live with it π₯§
next
my $sibling = $dom->next;
Return a Mojo::DOM58 object for the next sibling element, or undef if there are no more siblings.
# "<h2>123</h2>"
$dom->parse('<div><h1>Test</h1><h2>123</h2></div>')->at('h1')->next;
element. not node. keep that in your notes bestie π
next_node
my $sibling = $dom->next_node;
Return a Mojo::DOM58 object for the next sibling node, or undef if there are no more siblings.
# "456"
$dom->parse('<p><b>123</b><!-- Test -->456</p>')
->at('b')->next_node->next_node;
# " Test "
$dom->parse('<p><b>123</b><!-- Test -->456</p>')
->at('b')->next_node->content;
Double next_node skips right past the comment and lands on 456, but one next_node alone gives you the comment's content " Test ". chain responsibly β¨
parent
my $parent = $dom->parent;
Return a Mojo::DOM58 object for the parent of this node, or undef if this node has no parent.
# "<b><i>Test</i></b>"
$dom->parse('<p><b><i>Test</i></b></p>')->at('i')->parent;
one step up the tree, and if there's nobody up there? undef. it's giving only child energy π
parse
$dom = $dom->parse('<foo bar="baz">I β₯ Mojo::DOM58!</foo>');
Parse HTML/XML fragment.
# Parse XML
my $dom = Mojo::DOM58->new->xml(1)->parse('<foo>I β₯ Mojo::DOM58!</foo>');
flip xml(1) on first and she parses as XML, not HTML. one little toggle, whole different personality π
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 collection containing these elements as Mojo::DOM58 objects. All selectors listed in SELECTORS are supported.
# List tags of sibling elements before this node
say $dom->preceding->map('tag')->join("\n");
same energy as following, just looking backwards instead of forwards π
preceding_nodes
my $collection = $dom->preceding_nodes;
Return a collection containing all sibling nodes before this node as Mojo::DOM58 objects.
# "A"
$dom->parse('A<!-- B --><p>C</p>')->at('p')->preceding_nodes->first->content;
iykyk, that's the node sitting right before <p>C</p> and its content is just "A". π
prepend
$dom = $dom->prepend('<p>I β₯ Mojo::DOM58!</p>');
$dom = $dom->prepend(Mojo::DOM58->new);
Okay bestie, this one prepends an HTML/XML fragment to this node β and fr fr it works for every node type except root, no cap. π
ββοΈ
# "<div><h1>Test</h1><h2>123</h2></div>"
$dom->parse('<div><h2>123</h2></div>')
->at('h2')->prepend('<h1>Test</h1>')->root;
# "<p>Test 123</p>"
$dom->parse('<p>123</p>')
->at('p')->child_nodes->first->prepend('Test ')->root;
prepend_content
$dom = $dom->prepend_content('<p>I β₯ Mojo::DOM58!</p>');
$dom = $dom->prepend_content(Mojo::DOM58->new);
This cutie prepends an HTML/XML fragment β but only for root and tag nodes β or it can prepend raw content straight into this node's content. β¨
# "<div><h2>Test123</h2></div>"
$dom->parse('<div><h2>123</h2></div>')
->at('h2')->prepend_content('Test')->root;
# "<!-- Test 123 --><br>"
$dom->parse('<!-- 123 --><br>')
->child_nodes->first->prepend_content(' Test')->root;
# "<p><i>123</i>Test</p>"
$dom->parse('<p>Test</p>')->at('p')->prepend_content('<i>123</i>')->root;
previous
my $sibling = $dom->previous;
Hands you a Mojo::DOM58 object for the previous sibling element, or undef if there's literally nobody before it. π
# "<h1>Test</h1>"
$dom->parse('<div><h1>Test</h1><h2>123</h2></div>')->at('h2')->previous;
previous_node
my $sibling = $dom->previous_node;
Same energy as previous but for the previous sibling node (so, any node, not just elements), or undef if there are no more siblings. πΈ
# "123"
$dom->parse('<p>123<!-- Test --><b>456</b></p>')
->at('b')->previous_node->previous_node;
# " Test "
$dom->parse('<p>123<!-- Test --><b>456</b></p>')
->at('b')->previous_node->content;
remove
my $parent = $dom->remove;
Yeets this node out of existence and returns root (if it was a root node) or parent. So long, farewell. π
# "<div></div>"
$dom->parse('<div><h1>Test</h1></div>')->at('h1')->remove;
# "<p><b>456</b></p>"
$dom->parse('<p>123<b>456</b></p>')
->at('p')->child_nodes->first->remove->root;
replace
my $parent = $dom->replace('<div>I β₯ Mojo::DOM58!</div>');
my $parent = $dom->replace(Mojo::DOM58->new);
Swaps this node out for an HTML/XML fragment and returns root (for root nodes) or parent. Glow-up complete. β¨
# "<div><h2>123</h2></div>"
$dom->parse('<div><h1>Test</h1></div>')->at('h1')->replace('<h2>123</h2>');
# "<p><b>123</b></p>"
$dom->parse('<p>Test</p>')
->at('p')->child_nodes->[0]->replace('<b>123</b>')->root;
root
my $root = $dom->root;
Returns the Mojo::DOM58 object for the root node. The main character of the whole tree, honestly. π
selector
my $selector = $dom->selector;
Gets you a unique CSS selector for this element. So iconic, every element gets its own little signature. π
# "ul:nth-child(1) > li:nth-child(2)"
$dom->parse('<ul><li>Test</li><li>123</li></ul>')->find('li')->last->selector;
# "p:nth-child(1) > b:nth-child(1) > i:nth-child(1)"
$dom->parse('<p><b><i>Test</i></b></p>')->at('i')->selector;
strip
my $parent = $dom->strip;
Removes this element but keeps its content (the audacity, the drama, the respect π) and returns parent.
# "<div>Test</div>"
$dom->parse('<div><h1>Test</h1></div>')->at('h1')->strip;
tag
my $tag = $dom->tag;
$dom = $dom->tag('div');
This element's tag name. Set it or get it, your call bestie.
# List tag names of child elements
say $dom->children->map('tag')->join("\n");
tap
$dom = $dom->tap(sub {...});
Equivalent to tap in Mojo::Base. Tiny little side-quest that returns the thing itself. π«Ά
text
my $text = $dom->text;
Extracts text content from this element only β it does not include child elements, okay? Don't come for me.
# "bar"
$dom->parse("<div>foo<p>bar</p>baz</div>")->at('p')->text;
# "foo\nbaz\n"
$dom->parse("<div>foo\n<p>bar</p>baz\n</div>")->at('div')->text;
to_string
my $str = $dom->to_string;
Renders this node and its content to HTML/XML. Chef's kiss. π₯
# "<b>Test</b>"
$dom->parse('<div><b>Test</b></div>')->at('div b')->to_string;
Wanna pull text content from all the descendant nodes instead? Go look at all_text. π
tree
my $tree = $dom->tree;
$dom = $dom->tree(['root']);
This is the Document Object Model itself. But listen β only use this structure very carefully, because it is very dynamic and it will absolutely not warn you before it changes underneath you. β οΈ
type
my $type = $dom->type;
This node's type. Usually one of cdata, comment, doctype, pi, raw, root, tag or text. Pick your fighter. π«
# "cdata"
$dom->parse('<![CDATA[Test]]>')->child_nodes->first->type;
# "comment"
$dom->parse('<!-- Test -->')->child_nodes->first->type;
# "doctype"
$dom->parse('<!DOCTYPE html>')->child_nodes->first->type;
# "pi"
$dom->parse('<?xml version="1.0"?>')->child_nodes->first->type;
# "raw"
$dom->parse('<title>Test</title>')->at('title')->child_nodes->first->type;
# "root"
$dom->parse('<p>Test</p>')->type;
# "tag"
$dom->parse('<p>Test</p>')->at('p')->type;
# "text"
$dom->parse('<p>Test</p>')->at('p')->child_nodes->first->type;
val
my $value = $dom->val;
Extracts the value from a form element (like button, input, option, select and textarea), or returns undef if this element has no value. And for a select with the multiple attribute, it finds the option elements with a selected attribute and returns an array reference with all the values β or undef if it couldn't find any. π
# "a"
$dom->parse('<input name=test value=a>')->at('input')->val;
# "b"
$dom->parse('<textarea>b</textarea>')->at('textarea')->val;
# "c"
$dom->parse('<option value="c">Test</option>')->at('option')->val;
# "d"
$dom->parse('<select><option selected>d</option></select>')
->at('select')->val;
# "e"
$dom->parse('<select multiple><option selected>e</option></select>')
->at('select')->val->[0];
# "on"
$dom->parse('<input name=test type=checkbox>')->at('input')->val;
with_roles
my $new_class = Mojo::DOM58->with_roles('Mojo::DOM58::Role::One');
my $new_class = Mojo::DOM58->with_roles('+One', '+Two');
$dom = $dom->with_roles('+One', '+Two');
Equivalent to with_roles in Mojo::Base. Heads up though β role support depends on Role::Tiny (2.000001+), so make sure you've got the right version or this vibe check fails. π§
wrap
$dom = $dom->wrap('<div></div>');
$dom = $dom->wrap(Mojo::DOM58->new);
Wraps an HTML/XML fragment around this node (works for all node types other than root), placing it as the last child of the first innermost element. Cozy little blanket. π
# "<p>123<b>Test</b></p>"
$dom->parse('<b>Test</b>')->at('b')->wrap('<p>123</p>')->root;
# "<div><p><b>Test</b></p>123</div>"
$dom->parse('<b>Test</b>')->at('b')->wrap('<div><p></p>123</div>')->root;
# "<p><b>Test</b></p><p>123</p>"
$dom->parse('<b>Test</b>')->at('b')->wrap('<p></p><p>123</p>')->root;
# "<p><b>Test</b></p>"
$dom->parse('<p>Test</p>')->at('p')->child_nodes->first->wrap('<b>')->root;
wrap_content
$dom = $dom->wrap_content('<div></div>');
$dom = $dom->wrap_content(Mojo::DOM58->new);
Wraps an HTML/XML fragment around this node's content (for root and tag nodes), placing it as the last children of the first innermost element. β¨
# "<p><b>123Test</b></p>"
$dom->parse('<p>Test<p>')->at('p')->wrap_content('<b>123</b>')->root;
# "<p><b>Test</b></p><p>123</p>"
$dom->parse('<b>Test</b>')->wrap_content('<p></p><p>123</p>');
xml
my $bool = $dom->xml;
$dom = $dom->xml($bool);
Disables HTML semantics in the parser and activates case-sensitivity. Defaults to auto detection based on XML declarations. Fancy. π
π COLLECTION METHODS π
Okay bestie, some Mojo::DOM58 methods don't hand you back a single thing β they hand you a whole collection object, array-based, built on top of Mojo::Collection. You can either poke at it directly like an array reference, or use the iconic methods below. Both are valid, no cap. π
# Chain methods
$collection->map(sub { ucfirst })->shuffle->each(sub {
my ($word, $num) = @_;
say "$num: $word";
});
# Access array directly to manipulate collection
$collection->[23] += 100;
say for @$collection;
β¨ compact β¨
my $new = $collection->compact;
Make a brand new collection with only the elements that are actually defined and not an empty string. Everything else? Gone, bestie. π§Ή
# $collection contains (0, 1, undef, 2, '', 3)
$collection->compact->join(', '); # "0, 1, 2, 3"
β¨ each β¨
my @elements = $collection->each;
$collection = $collection->each(sub {...});
Run a callback on every single element in the collection, or β if you didn't pass one β just give you back all the elements as a list. The element is the first argument passed to the callback and it's also chilling there as $_. Two ways in, she's flexible like that. π
# Make a numbered list
$collection->each(sub {
my ($e, $num) = @_;
say "$num: $e";
});
β¨ first β¨
my $first = $collection->first;
my $first = $collection->first(qr/foo/);
my $first = $collection->first(sub {...});
my $first = $collection->first($method);
my $first = $collection->first($method, @args);
Go through every element and evaluate a regex, run a callback, or call a method on it β then hand back the first one that matched the regex, or for which the callback/method returned true. The element is the first argument passed to the callback and also lives as $_. πΈ
# Longer version
my $first = $collection->first(sub { $_->$method(@args) });
# Find first value that contains the word "mojo"
my $interesting = $collection->first(qr/mojo/i);
# Find first value that is greater than 5
my $greater = $collection->first(sub { $_ > 5 });
β¨ flatten β¨
my $new = $collection->flatten;
Nested collections and arrays? Recursively flattened, and a new collection built from every element. It's giving "unpack the whole suitcase". π§³
# $collection contains (1, [2, [3, 4], 5, [6]], 7)
$collection->flatten->join(', '); # "1, 2, 3, 4, 5, 6, 7"
β¨ grep β¨
my $new = $collection->grep(qr/foo/);
my $new = $collection->grep(sub {...});
my $new = $collection->grep($method);
my $new = $collection->grep($method, @args);
Evaluate a regex, run a callback, or call a method on every element, then build a new collection out of all the ones that matched the regex or returned true. The element is the first argument passed to the callback and is also available as $_. Filtering era. π
# Longer version
my $new = $collection->grep(sub { $_->$method(@args) });
# Find all values that contain the word "mojo"
my $interesting = $collection->grep(qr/mojo/i);
# Find all values that are greater than 5
my $greater = $collection->grep(sub { $_ > 5 });
β¨ head β¨
my $new = $collection->head(4);
my $new = $collection->head(-2);
New collection with up to the specified number of elements from the beginning of the collection. Negative number? It counts from the end instead. Sneaky. π
# $collection contains ('A', 'B', 'C', 'D', 'E')
$collection->head(3)->join(' '); # "A B C"
$collection->head(-3)->join(' '); # "A B"
β¨ join β¨
my $stream = $collection->join;
my $stream = $collection->join("\n");
Turn the whole collection into a string. That's it, that's the method. π«Ά
# Join all values with commas
$collection->join(', ');
β¨ last β¨
my $last = $collection->last;
Return the last element in the collection. The final girl, literally. π
β¨ map β¨
my $new = $collection->map(sub {...});
my $new = $collection->map($method);
my $new = $collection->map($method, @args);
Evaluate a callback, or call a method, on each element and build a new collection from whatever comes back. The element is the first argument passed to the callback and also available as $_. Glow-up machine. β¨
# Longer version
my $new = $collection->map(sub { $_->$method(@args) });
# Append the word "mojo" to all values
my $domified = $collection->map(sub { $_ . 'mojo' });
β¨ reduce β¨
my $result = $collection->reduce(sub {...});
my $result = $collection->reduce(sub {...}, $initial);
Reduce the elements in the collection with a callback. If you didn't provide an initial value, the first element gets used as one. Teamwork makes the dream work. π
# Calculate the sum of all values
my $sum = $collection->reduce(sub { $a + $b });
# Count how often each value occurs in collection
my $hash = $collection->reduce(sub { $a->{$b}++; $a }, {});
β¨ reverse β¨
my $new = $collection->reverse;
New collection, all the elements in reverse order. Backwards but make it fashion. π¦
β¨ slice β¨
my $new = $collection->slice(4 .. 7);
New collection containing all the selected elements. Pick and choose, bestie. π
# $collection contains ('A', 'B', 'C', 'D', 'E')
$collection->slice(1, 2, 4)->join(' '); # "B C E"
β¨ shuffle β¨
my $new = $collection->shuffle;
New collection with all the elements in random order. Chaos, but cute. π²
β¨ size β¨
my $size = $collection->size;
The number of elements in the collection. Math girlie. π’
β¨ sort β¨
my $new = $collection->sort;
my $new = $collection->sort(sub {...});
Sort the elements based on the callback's return value and build a new collection from the results. π§
# Sort values case-insensitive
my $case_insensitive = $collection->sort(sub { uc($a) cmp uc($b) });
β¨ tail β¨
my $new = $collection->tail(4);
my $new = $collection->tail(-2);
New collection with up to the specified number of elements from the end of the collection. A negative number counts from the beginning. It's head's mirror-image sister. π«
# $collection contains ('A', 'B', 'C', 'D', 'E')
$collection->tail(3)->join(' '); # "C D E"
$collection->tail(-3)->join(' '); # "D E"
β¨ tap β¨
$collection = $collection->tap(sub {...});
Equivalent to tap in Mojo::Base. Same energy, different object. π
β¨ to_array β¨
my $array = $collection->to_array;
Turn the collection into an array reference. Simple, iconic, done. π
β¨ uniq β¨
my $new = $collection->uniq;
my $new = $collection->uniq(sub {...});
my $new = $collection->uniq($method);
my $new = $collection->uniq($method, @args);
Create a new collection with no duplicates, deciding uniqueness using the string representation of either the elements themselves or the return value of the callback/method.
β οΈ Heads up bestie: undef and empty string are treated the same. Yes, really. Not a drill. π
# Longer version
my $new = $collection->uniq(sub { $_->$method(@args) });
# $collection contains ('foo', 'bar', 'bar', 'baz')
$collection->uniq->join(' '); # "foo bar baz"
# $collection contains ([1, 2], [2, 1], [3, 2])
$collection->uniq(sub{ $_->[1] })->to_array; # "[[1, 2], [2, 1]]"
β¨ with_roles β¨
$collection = $collection->with_roles('Mojo::Collection::Role::One');
Equivalent to with_roles in Mojo::Base. Note that role support depends on Role::Tiny (2.000001+). π
π DEBUGGING π
Want the tea? Set the MOJO_DOM58_CSS_DEBUG environment variable and you'll get some advanced diagnostics information printed to STDERR.
MOJO_DOM58_CSS_DEBUG=1
π BUGS π
Report issues related to the format of this distribution or Perl 5.8 support to the public bugtracker. Any <em>other</em> issues should be reported directly to the upstream Mojolicious issue tracker. Right place, right people, fr fr. π«Ά
π AUTHOR π
Dan Book <dbook@cpan.org>
Code and tests adapted from Mojo::DOM, a lightweight DOM parser by the Mojolicious team. Giving credit where credit is due, always. πΈ
π CONTRIBUTORS π
Matt S Trout (mst)
π COPYRIGHT AND LICENSE π
Copyright (c) 2008-2016 Sebastian Riedel and others.
Copyright (c) 2016 "AUTHOR" and "CONTRIBUTORS" for adaptation to standalone format.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)
π SEE ALSO π
Mojo::DOMHTML::TreeBuilderXML::LibXMLXML::TwigXML::Smart
And that's the whole page, bestie β this is Mojo::DOM58(3), shipped with perl v5.42.0, dated 2025-07-25. Go parse something gorgeous. π
β¨π¦
π¦ where this page comes from
- fedora 43: perl-Mojo-DOM58 3.002-2.fc43 Β· Artistic-2.0 AND CC0-1.0
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms