NAME
XML::LibXML::Augment - extend XML::LibXML::{Attr,Element,Document} on a per-namespace/element basis
SYNOPSIS
{
package Local::Element::Bar;
use 5.010;
use strict;
use warnings;
use XML::LibXML::Augment -names => ['{http://example.com/}bar'];
sub tellJoke
{
say q{A man walked into a bar.};
say q{"Ouch," he said.};
say q{It was an iron bar.};
}
}
{
package main;
use 5.010;
use strict;
use warnings;
use XML::LibXML::Augment;
my $doc = XML::LibXML->load_xml(string => <<'XML');
<foo xmlns="http://example.com/">
<bar baz="1" />
</foo>
XML
XML::LibXML::Augment->upgrade($doc);
$doc->findnodes('//*[@baz]')->shift->tellJoke;
}
DESCRIPTION
XML::LibXML is super-awesome. However, I don't know about you, but sometimes I wish it had some domain-specific knowledge. For example, if I have an XML::LibXML::Element which represents an HTML <form>
element, why can't it have a submit
method?
OK, so I can subclass XML::LibXML::Element, but then I call childNodes
on my subclass, and get back plain, non-subclassed objects, and I'm back where I first began.
XML::LibXML::Augment is the package I've been meaning to write for quite some time, to take care of all those issues.
The magic is in the import method. You write a package which imports XML::LibXML::Augment with particular settings. Then, once you've parsed the document, you call XML::LibXML::Augment->upgrade
on the root node.
import %args
Currently three options are supported. Each has a leading hyphen.
-names => \@list
Each item on the list is the name of an element you want to override. If the element is in a namespace, use Clark notation:
{http://www.example.com/namespace}localname
If the element is not in a namespace, then leave out the curly braces. Because of clashes (see "Conflict resolution" in CAVEATS below) it seems like a bad idea to try to augment non-namespaced elements based entirely on their localname. But, hey, you want to do it? It's your funeral.
You can use "*" as the localname to indicate that you wish to subclass all elements in a particular namespace. (Again, see "Conflict resolution".)
Yes, this is a list, because it might make sense to use the same package to cover, say, HTML <a>
, <link>
and <area>
elements.
-type => $type
$type
can be either 'Element', 'Attr' or 'Document', but defaults to 'Element'. This indicates what sort of thing you're subclassing. Only elements, attributes and documents are supported. (Document subclassing is based on the namespace and localname of its root element.)
Elements, attributes and documents are pairwise disjoint classes, so you cannot (for example) subclass elements and attributes in the same package.
-isa => \@packages
Normally the import routine will automatically establish your package as a subclass of XML::LibXML::Augment::Attr or XML::LibXML::Augment::Element by monkeying around with your @ISA
.
There are times when you want more precise control over @ISA
though, such as inheritance heirarchies:
Local::HTML::TableHeader
-> Local::HTML::TableCell
-> Local::HTML::Element
-> XML::LibXML::Augment::Element
In this case, you probably want to use the automatic subclassing for Local::HTML::Element, but not for the other two classes, which would work better with explicit subclassing:
{
package Local::HTML::TableCell;
use XML::LibXML::Augment
-names => ['{http://www.w3.org/1999/xhtml}td'],
-isa => ['Local::HTML::Element'];
}
upgrade(@things)
This is a function, not a method. It's not exported, so call it with its full name:
XML::LibXML::Augment::upgrade(@things);
Upgrades the things in-place, skipping over things that cannot be upgraded, and returns the things as a list. You can of course call it like this:
@upgraded = XML::LibXML::Augment->upgrade(@things);
But bear in mind that because of the way Perl method calls work, this is effectively the same as:
@upgraded = (
'XML::LibXML::Augment',
XML::LibXML::Augment::upgrade(@things),
);
What is upgrading? Things that are not blessed objects are not upgradable. Blessed objects that XML::LibXML::Augment can find an appropriate subclass for are reblessed into that package (e.g. XML::LibXML::Comment is reblessed into XML::LibXML::Augment::Comment). The nodes in XML::LibXML::NodeLists are reblessed.
rebless($thing)
This is basically a single-argument version upgrade
but designed to be called as a class method, and doesn't recurse into nodelists.
my $upgraded = XML::LibXML::Augment->rebless($element);
Note that $element is actually upgraded in-place.
refaddr($upgraded) == refaddr($element); # true
ideal_class_for_object($object)
Calculates the class that rebless
would bless the object into, but doesn't actually do the reblessing.
make_class(@superclasses)
Constructs a new class that is a subclass of the given classes. Call this as a class method. Returns the class name. This is a method used internally by XML::LibXML::Augment, documented in case anybody else wants to use it.
BLESS
XML::LibXML::Augment doesn't actually have a method called BLESS
, but your package can do.
Inspired by Moose's BUILD
method, your package's BLESS
method will be called (if it exists) just after an XML::LibXML node is reblessed into your package. Unlike Moose's BUILD
method, the inheritance chain isn't automatically walked. It is your package's responsibility to call SUPER::BLESS
if required.
Do bear in mind that XML::LibXML's node objects are little more than pointers to the "real" XML nodes that live on the C side of the XS divide. As such, XML::LibXML can (and frequently does) destroy and re-instantiate the pointers willy-nilly. This may limit the usefulness of BLESS
.
HOW DOES IT WORK?
Mostly just careful use of inheritance.
CAVEATS
Conflict resolution
Only one class can handle any given element. If two different modules want to subclass, say, XHTML <form>
elements, there can be only one winner. So, which one wins? Neither. XML::LibXML::Augment creates a brand, spanking new class which inherits from both, and that new class wins. This will usually work, but may trip up sometimes. The joke.pl
example bundled with the XML-LibXML-Augment release gives a demonstration of this feature.
Note that packages which use an element wildcard, for instance:
package Local::HTML::Element;
use XML::LibXML::Augment -names => ['{http://www.w3.org/1999/xhtml}*'];
are treated purely as fallbacks. If there exists a non-wildcard class to handle an element, then the wildcard class will be ignored altogether - it won't be included in the funky on-the-fly class generation described above.
For these reasons, wildcards are perhaps best avoided. It's usually better to do something like:
package Local::HTML::Element;
our @ELEMENTS;
BEGIN {
@ELEMENTS = map { "{http://www.w3.org/1999/xhtml}$_" }
qw/a area b col colgroup ... u/;
}
use XML::LibXML::Augment -names => \@ELEMENTS;
XML::LibXML::XPathContext
We don't touch XML::LibXML::XPathContext. Results from calling, e.g. findnodes
on an XPath context will return plain old XML::LibXML nodes. (You can of course upgrade them.) That doesn't render XPath completely unusable though - XML::LibXML::Node also has a findnodes
method, which will return upgraded objects.
Subclassing all elements (or all attributes)
XML::LibXML::Augment requires you to specify the namespace URI and localname of the elements/attributes you wish to subclass. If you want to provide additional methods to all XML::LibXML::Elements, then perhaps XML::LibXML::Augment is not for you. Try:
sub XML::LibXML::Element::myAwesomeMethod {
...
}
If you think adding methods to other peoples' classes is evil, then go write some Java and quit complaining.
BUGS
Please report any bugs to http://rt.cpan.org/Dist/Display.html?Queue=XML-LibXML-Augment.
SEE ALSO
AUTHOR
Toby Inkster <tobyink@cpan.org>.
COPYRIGHT AND LICENCE
This software is copyright (c) 2012, 2014 by Toby Inkster.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.
DISCLAIMER OF WARRANTIES
THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.