Mojo::DOM58(3)

πŸ“¦
version 3.002 Β· 3 versions
, in fedora 43

πŸ’– 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:

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 type E.
my $title = $dom->at('title');
  • E[foo] β€” an E element that has a foo attribute.
my $links = $dom->find('a[href]');
  • E[foo="bar"] β€” an E element whose foo attribute value is exactly equal to bar.
my $case_sensitive = $dom->find('input[type="hidden"]');
my $case_sensitive = $dom->find('input[type=hidden]');
  • E[foo="bar" i] β€” an E element whose foo attribute value is exactly equal to any (ASCII-range) case-permutation of bar. ⚠️ 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] β€” an E element whose foo attribute value is exactly and case-sensitively equal to bar. ⚠️ 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"] β€” an E element whose foo attribute value is a list of whitespace-separated values, and one of them is exactly equal to bar.
my $foo = $dom->find('input[class~="foo"]');
my $foo = $dom->find('input[class~=foo]');
  • E[foo^="bar"] β€” an E element whose foo attribute value begins exactly with the string bar.
my $begins_with = $dom->find('input[name^="f"]');
my $begins_with = $dom->find('input[name^=f]');
  • E[foo$="bar"] β€” an E element whose foo attribute value ends exactly with the string bar.
my $ends_with = $dom->find('input[name$="o"]');
my $ends_with = $dom->find('input[name$=o]');
  • E[foo*="bar"] β€” an E element whose foo attribute value contains the substring bar.
my $contains = $dom->find('input[name*="fo"]');
my $contains = $dom->find('input[name*=fo]');
  • E[foo|="en"] β€” an E element whose foo attribute has a hyphen-separated list of values beginning (from the left) with en.
my $english = $dom->find('link[hreflang|=en]');
  • E:root β€” an E element, the root of the document.
my $root = $dom->at(':root');
  • E:nth-child(n) β€” an E element, the n-th child 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) β€” an E element, the n-th child 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) β€” an E element, the n-th sibling 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) β€” an E element, the n-th sibling 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 β€” an E element, the first child of its parent.
my $first = $dom->find('div p:first-child');
  • E:last-child β€” an E element, the last child of its parent.
my $last = $dom->find('div p:last-child');
  • E:first-of-type β€” an E element, the first sibling of its type.
my $first = $dom->find('div p:first-of-type');
  • E:last-of-type β€” an E element, the last sibling of its type.
my $last = $dom->find('div p:last-of-type');
  • E:only-child β€” an E element, the only child of its parent. So lonely πŸ₯Ί
my $lonely = $dom->find('div p:only-child');
  • E:only-of-type β€” an E element, the only sibling of its type.
my $lonely = $dom->find('div p:only-of-type');
  • E:empty β€” an E element that has no children (text nodes count too, so no sneaky hidden text πŸ‘€).
my $empty = $dom->find(':empty');
  • E:any-link β€” alias for E: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 β€” an E element 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, :link and :visited yield 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 for E:link.
  • E:scope β€” an E element 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 element E which is checked (for instance a radio-button or checkbox). Slay, checked βœ…
my $input = $dom->find(':checked');
  • E.warning β€” an E element whose class is warning.
my $warning = $dom->find('div.warning');
  • E#myid β€” an E element with ID equal to myid.
my $foo = $dom->at('div#foo');
  • E:not(s1, s2) β€” an E element that does not match either compound selector s1 or compound selector s2. ⚠️ 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) β€” an E element that matches compound selector s1 and/or compound selector s2. ⚠️ 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) β€” an E element, if either of the relative selectors rs1 or rs2, when evaluated with E as the :scope elements, 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) β€” an E element containing text content that substring matches string_or_regex case-insensitively, or that regex matches string_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 β€” an E element that belongs to the namespace alias A from 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 β€” an F element that is a descendant of an E element.
my $headlines = $dom->find('div h1');
  • E > F β€” an F element that is a child of an E element.
my $headlines = $dom->find('html > body > div > h1');
  • E + F β€” an F element immediately preceded by an E element.
my $second = $dom->find('h1 + h2');
  • E ~ F β€” an F element preceded by an E element.
my $second = $dom->find('h1 ~ h2');
  • E, F, G β€” elements of type E, F and G. The group chat selector, everyone's invited 🫢
my $headlines = $dom->find('h1, h2, h3');
  • E[foo=bar][bar=baz] β€” an E element 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 &amp; 123</div>"
$dom->new_tag('div', 'test & 123');

# "<div id="foo">test &amp; 123</div>"
$dom->new_tag('div', id => 'foo', 'test & 123');

# "<div data-foo="1" data-bar="test">test &amp; 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 &amp; (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 });

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 (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::DOM
  • HTML::TreeBuilder
  • XML::LibXML
  • XML::Twig
  • XML::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