XML::Descent(3pm)

πŸ“¦
version 1.04 Β· 4 versions
, in debian trixie, ubuntu resolute, ubuntu stonking

πŸ’– XML::Descent πŸ’–

okay so bestie, this is XML::Descent(3pm) from the User Contributed Perl Documentation, and it is literally about recursive descent XML parsing. buckle up, she's a long one but she's iconic ✨


✨ NAME ✨

XML::Descent β€” recursive descent XML parsing πŸ¦‹


✨ VERSION ✨

this doc is describing XML::Descent version 1.04 πŸ“Œ


✨ SYNOPSIS ✨

use XML::Descent;

# Create parser
my $p = XML::Descent->new( { Input => \$xml } );

# Setup handlers
$p->on(
  folder => sub {
    my ( $elem, $attr ) = @_;

    $p->on(
      url => sub {
        my ( $elem, $attr ) = @_;
        my $link = {
          name => $attr->{name},
          url  => $p->text
        };
      }
    );

    my $folder = $p->walk;
    $folder->{name} = $attr->{name};
  }
);

# Parse
my $res = $p->walk;

✨ DESCRIPTION ✨

so the way people usually parse XML is either DOM (where you build one big data structure that holds the whole document tree πŸŽ„) or SAX (where you just get a callback fired for every single element in the XML πŸ“£).

but here's the thing β€” XML grammar is recursive, so writing recursive parsers for it just makes sense, lowkey. and that's exactly what XML::Descent lets you do. slay πŸ’…

usually you make a new XML::Descent and then define handlers for the elements you actually care about:

my $p = XML::Descent->new( { Input => \$xml } );
$p->on(
  link => sub {
    my ( $elem, $attr ) = @_;
    print "Found link: ", $attr->{url}, "\n";
    $p->walk;    # recurse
  }
);
$p->walk;        # parse

a handler gives you a cozy little lexical scope that sticks around until the closing tag of whatever element triggered that handler finally shows up. periodt πŸŽ€

when you call the parsing methods walk, text and xml at the top level, they parse the entire XML document. but when you call them recursively inside a handler, they only parse the chunk of the document nested inside the node that triggered the handler. get it? πŸ‘‘

and yes β€” you can define new handlers inside a handler, and their scope is limited to just the XML inside the node that triggered the handler. nesting is the vibe πŸͺ†


✨ INTERFACE ✨

🌸 new( { options } )

makes a brand new XML::Descent for you. options come in as a hash reference.

the only option XML::Descent recognises directly is Input, which should be a reference to the object that provides the XML source. any value you could pass as the first argument to XML::TokeParser->new is allowed here, no cap.

all the other options get handed straight off to XML::TokeParser. go read that module's docs for the deets, bestie πŸ“š

🌸 walk

parses part of the XML document tree and triggers any handlers that match the elements it finds. when you call it recursively from inside a handler, walk visits all the elements below the element that triggered the handler, and then it comes back. it's giving strolling through the tree πŸšΆβ€β™€οΈπŸŒ³

🌸 on( [ element names ], handler )

registers a handler to be called when the named element shows up. you can pass multiple element names as an array reference. and you can register multiple handlers in one on call by supplying a bunch of element, handler pairs. efficiency queen πŸ’…

calling on inside a handler defines a nested local handler whose scope is limited to the containing element.

handlers get called with three arguments: the name of the element that triggered the handler, a hash of the element's attributes, and a user defined context value β€” peep "context" for more about that third one πŸ‘€

for example:

$p = XML::Descent->new( { Input => \$some_xml } );

# Global handler - trigger anywhere an <options> tag is found
$p->on(
  options => sub {
    my ( $elem, $attr, $ctx ) = @_;

    # Define a nested handler for <name> elements that only
    # applies within the <options> handler.
    $p->on(
      name => sub {
        my ( $elem, $attr, $ctx ) = @_;
        # Get the inner text of the name element
        my $name = $p->text;
        print "Name: $name\n";
      }
    );

    # Recursively walk elements inside <options> triggering
    # any handlers
    $p->walk;
  }
);

# Start parsing
$p->walk;

a handler may call one of the parsing methods (walk, text, xml or get_token) to consume any nested XML before it returns. but if none of the parsing methods get called, the nested XML is automatically discarded so the parser can properly move past the current element. it cleans up after itself, we love a self-sufficient icon 🧹✨

nested handlers temporarily override another handler with the same name.

a handler named '*' will trigger for all elements that don't have an explicit handler of their own. and a nested '*' handler hides all handlers defined in containing scopes. skrrt πŸ’€

also, as a lil shorthand, you can specify a path to a nested element:

$p->on( 'a/b/c' => sub {
  print "Woo!\n";
})->walk;

which is exactly the same as:

$p->on( a => sub {
  $p->on( b => sub {
    $p->on( c => sub {
      print "Woo!\n";
    })->walk;
  })->walk;
})->walk;
⚠️ heads up bestie: that shorthand only applies to on β€” not to the other methods that accept element names. don't try it elsewhere, it will not work 😭

🌸 inherit( [ element names ] )

inherits handlers from the containing scope. you'd typically use this to import handlers that would otherwise get masked by a catch-all '*' handler.

$p->on(
  'a' => sub {
    my ( $elem, $attr, $ctx ) = @_;
    my $link = $attr->{href} || '';
    my $text = $p->text;
    print "Link: $text ($link)\n";
  }
);

$p->on(
  'special' => sub {
    my ( $elem, $attr, $ctx ) = @_;

    # Within <special> we want to handle all
    # tags apart from <a> by printing them out
    $p->on(
      '*' => sub {
        my ( $elem, $attr, $ctx ) = @_;
        print "Found: $elem\n";
      }
    );

    # Get the handler for <a> from our containing
    # scope.
    $p->inherit( 'a' );
    $p->walk;
  }
);

the inherited handler is whatever handler would have applied in the containing scope for an element with that given name. like:

$p->on( '*' => sub { print "Whatever\n"; $p->walk; } );
$p->on(
  'interesting' => sub {
    # Inherits the default 'Whatever' handler because that's the
    # handler that would have been called for <frob> in the
    # containing scope
    $p->inherit( 'frob' );
    # Handle everything else ourselves
    #p->on('*', sub { $p->walk; });
  }
);

🌸 before

registers a handler to be called before the existing handler for an element. same as on, you can target multiple elements by passing an array ref 🫢

🌸 after

registers a handler to be called after the existing handler for an element. again, just like on, multiple elements can be targeted with an array ref 🫢

🌸 context

every single time a handler gets called, a new scope is created for it. that's what lets you define nested handlers in the first place. the current scope holds a user context variable, which you can use for stuff like keeping track of an object you're filling up with values parsed out of the XML. the context value gets inherited from the parent scope, but you can override it locally whenever you want. main character energy πŸ‘‘

for example:

my $root = {};

# Set the outermost context
$p->context( $root );

# Handle HTML <a href...> links /anywhere/
$p->on(
  'a' => sub {
    my ( $elem, $attr, $ctx ) = @_;
    my $link = {
      href => $attr->{href},
      text => $p->text
    };
    push @{ $ctx->{links} }, $link;
  }
);

# Links in the body are stored in a nested
# object.
$p->on(
  'body' => sub {
    my ( $elem, $attr, $ctx ) = @_;
    my $body = {};
    # Set the context
    $p->context( $body );
    $p->walk;
    $ctx->{body} = $body;
  }
);

$p->walk;

okay so notice how the handler for <a href...> tags just drops its results into the current context object β€” whatever that happens to be. which means outside of any <body> tag, links land in $root, but inside a <body> they land in a nested object ("$root->{body}"). and the <a> handler itself knows absolutely nothing about this. it's giving modular, it's giving separation of concerns, we stan πŸ’–

with no parameter, context returns the current context. and guess what β€” the current context is also passed as the third argument to handlers. two ways to get it, choose your fighter ✨

🌸 text

returns any text contained within the current element. XML markup just gets yeeted into the void πŸ—‘οΈ

🌸 xml

returns the unparsed inner XML of the current element. for example:

$p->on(
  'item' => sub {
    my ( $elem, $attr, $ctx ) = @_;
    my $item_source = $p->xml;
    print "Item: $item_source\n";
  }
);

so if <item> contains XHTML (for example), that handler up there would correctly capture it without recursively parsing any of the elements inside it. so if you're parsing

<feed>
  <item>This is the <i>first story</i>.</item>
  <item>This is <b>another story</b>.</item>
</feed>

you would print

Item: This is the <i>first story</i>.
Item: This is <b>another story</b>.

ate that, no crumbs 🍽️

🌸 get_path

called inside a handler, this returns the path that leads to the current element. for example:

$p->on(
  'here' => sub {
    my ( $elem, $attr, $ctx ) = @_;
    print "I am here: ", $p->get_path, "\n";
    $p->walk;
  }
);

and if you applied that to this XML

<outer>
  <inner>
    <here />
  </inner>
  <here />
</outer>

it would print

I am here: /outer/inner/here
I am here: /outer/here

look at her, always knows where she is πŸ—ΊοΈπŸ’…

🌸 get_token

XML::Descent is built on top of XML::TokeParser, which splits an XML document into a stream of tokens representing start tags, end tags, literal text, comments and processing instructions.

inside an element, get_token returns the exact same stream of tokens that XML::TokeParser would produce. it returns undef once all the tokens contained within the current element have been read β€” i.e. it's impossible to read past the end of the enclosed XML. boundaries, bestie, we respect them 🚧

🌸 scope_handlers

gets you a list of all the handlers that are registered locally to the current scope. just so you know β€” the returned list won't include '*' if a wildcard handler has been registered. sneaky lil omission, don't be surprised πŸ™ˆ

🌸 all_handlers

gets you a list of all registered handlers across all scopes. and again, the returned list won't include the '*' wildcard handler. same energy, wider net πŸ•ΈοΈ


✨ SEE ALSO ✨

<http://en.wikipedia.org/wiki/Recursive_descent_parser>, XML::TokeParser, XML::Twig.


✨ BUGS AND LIMITATIONS ✨

XML::Descent uses XML::TokeParser to do the actual parsing, right. and XML::TokeParser can only return start tags, end tags, raw text and processing instructions. so as a direct result of that, calling xml at the root of an XML document will exclude any <?xml?> declaration. that's just how it is, no cap 😭

No bugs have been reported.

please report any bugs or feature requests to bug-xml-descent@rt.cpan.org, or through the web interface at <http://rt.cpan.org>. do it, they wanna hear from you πŸ“¬


✨ AUTHOR ✨

Andy Armstrong <andy@hexten.net>



okay so the legal tea β˜•

Copyright (c) 2006-2009, Andy Armstrong <andy@hexten.net>. All rights reserved.

This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See perlartistic. πŸŽ€

that's it, that's the license bestie β€” Perl's own Artistic License, iconic behaviour, no cap πŸ’…


πŸ›‘οΈ DISCLAIMER OF WARRANTY πŸ›‘οΈ

and now the part every dev scrolls past but genuinely needs to read 😭 it's in all caps because that's how legal disclaimers work, not because anyone's yelling at you. here it is, word for word:

BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENSE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

so yeah β€” no warranty, the risk is on you, and if it eats your data that's a you problem πŸ™ˆ it's free software, that's the trade. worth knowing, fr fr.


and that's the whole page, bestie 🫢 you just read the XML::Descent(3pm) manual from top to bottom, all the way down to the footer β€” which was, for the record, perl v5.40.1 dated 2025-03-07. go forth and descend βœ¨πŸŒˆπŸ’–

πŸ“¦ where this page comes from