| XML-Filter-Essex documentation | Contained in the XML-Filter-Essex distribution. |
XML::Essex::Model - Essex objects representing SAX events and DOM trees
Used internally by Essex, see below for external API examples.
A description of all of the events explicitly supported so far. Unsupported events are still handled as anonymous events, see XML::Essex::Event for details.
A goal of essex is to allow code to be as terse or verbose as is
appropriate for the job at hand. So almost every object may be
abbreviated. So start_element may be abbreviated as start_elt for
both the isa() method/function and for class creation.
All objects are actually blessed in to classes using the long name, like
XML::Essex::start_element even if you use an abbreviation like
XML::Essex::start_elt-new> to create them.
All events are stringifiable for debugging purposes and so that
attribute values, character data, comments, and processing instructions
may be matched with Perl string operations like regular expressions and
index(). It is usually more effective to use EventPath, but you can
use stringified values for things like complex regexp matching.
It is unwise to match other events with string operators because no XML
escaping of data is done, so "<" in an attribute value or character data
is stringified as "<", so print()ing out the three events associated
with the sequence "<foo><bar/></foo>" will look like
"<foo><bar/></foo>", obviously not what the document intended. Given
the rarity of such constructs in real life XML, though, this is
sufficient for debugging purposes, and does make it easy to match
against strings.
Ordinarily, you tell get() what kind of object you want using an
EventPath expression:
my $start_element = get "start_element::*";
You can also just get() whatever's next in the document or use a
union expression. In this case, you may need to see what you've gotten.
The isa() method (see below) and the isa() functions (see
XML::Essex) should be used to figure out what type of
object is being used before relying on the stringification:
get until isa "chars" and /Bond, James Bond/;
get until type eq "characters" and /Bond, James Bond/;
get until isa( "chars" ) && /Bond, James Bond/;
get "text()" until /Bond, James Bond/;
This makes it easier to match characters data, but other methods should be used to select things like start tags and elements:
get "start_element::*" until $_->name eq "address" && $_->{id} eq $id;
get "start_element::address" until $_->{id} eq $id;
The lack of escaping only affects stringification of objects, for instance:
warn $_; ## See what event is being dealt with right now
/Bond, James Bond/ ## Match current event
. Things are escaped properly when the put operator is used, using
put() emits properly escaped XML.
Some observervations:
Stringifying an event does not produce a well formed chunk of XML. Events must be emitted through a downstream filter.
Events with no natural XML representation--like start_document--stringify as their name: "start_document()". If it's not listed on this page, it stringifies this way.
Whitespace is inserted only where manditory inside XML constructs, and
is a single space. It is left unmolested in character data, comments,
processing instructions (other than <?xml ...?>, which is parsed
by all XML parsers).
Attributes in start_element events are stringified in alphabetical order
according to Perl's sort() function.
Processing instructions, including the <?xml...?> declaration,
often have things that look like attributes but are not, so the items
above about whitespace and attribute sort order do not apply. Actually,
the <?xml ... ?> declaration is well defined and there will
be only a single whitespace character, though the pseudo-attributes
version, encoding and standalone will not be sorted.
No escapes are used. See above.
Character data is catenated, including mixed data and CDATA, in to single strings. CDATA sections are tracked and may be analyzed.
Namespaces are stringified according to any prefixes that have been
registered, otherwise they stringify in james clark notation
("{}foo"), except for the empty namespace URI, which alway
stringifies as "" (ie no prefix). See XML::Essex's Namspaces section for details.
All of the objects in the model provide the following methods. These methods are exported as functions from the XML::Essex module for convenience (those functions are wrappers around these methods).
Returns TRUE if the object is of the type, abbreviated type, or class passed. So, for an object encapsulating a characters event, returns TRUE for any of:
XML::Essex::Event ## The base class for all events
XML::Essex::start_document ## The actuall class name
start_document ## The event type
start_doc ## The event type, abbreviated
Returns the class name, such as XML::Essex::start_document.
Returns the class name, such as start_document.
Returns the class name, the type name and any abbreviations. The abbreviations are sorted from longest to shortest.
aka: start_doc
my $e = start_doc \%values; ## %values is not defined in SAX1/SAX2
Stringifies as: start_document($reserved)
where $reserved is a character string that may sometime include info passed in the start_document event, probably formatted as attributes.
aka: (no abbreviations)
my $e = xml_decl;
my $e = xml_decl
Version => "1",
Encoding => "UTF-8",
Standalone => "yes";
my $e = xml_decl {
Version => "1",
Encoding => "UTF-8",
Standalone => "yes"
};
Stringifies as: <?xml version="$version" encoding="$enc"
standalone="$yes_or_no"?>
Note that this does not follow the sorted attribute order behavior of start_element, as the seeming attributes here are not attributes, like processing instructions that have pretend attributes.
aka: end_doc
my $e = end_doc \%values; ## %values is not defined in SAX1/SAX2
Stringifies as: end_document($reserved)
where $reserved is a character string that may sometime include info passed in the end_document event, probably formatted as attributes.
aka: start_elt
my $e = start_elt foo => { attr => "val" };
my $e = start_elt $start_elt; ## Copy constructor
my $e = start_elt $end_elt; ## end_elt deconstructor
my $e = start_elt $elt; ## elt deconstructor
Stringifies as: <foo attr1="$val1" attr2="val2">
The element name and any attribute names are prefixed according to
namespace mappings registered in the Essex processor, the prefixes they
had in the source document are ignored. If no prefix has been mapped,
jclark notation ({http:...}foo) is used. Then they are sorted
according to Perl's sort() function, so jclarked attribute names come
last, as it happens.
TODO: Support attribute ordering via consecutive {...} sets.
Attributes may be accessed using hash dereferences:
get "start_element::*" until $_->{id} eq "10"; ## No namespace prefix
get "start_element::*" until $_->{"{}id"} eq "10";
get "start_element::*" until $_->{"{http://foo/}id"} eq "10";
get "start_element::*" until $_->{"foo:id"} eq "10";
and the attribute names may be obtained by:
keys %$_;
. Keys are returned in no predictable order, see Namespaces for details on the three formats keys may be returned in.
Returns the name of the node according to the namespace stringification rules.
Returns the name of the node in James Clark notation.
my @keys = $e->jclark_keys
Returns a list of attribute names in jclark notation ("{...}name").
aka: attr
my $name_attr = $start_elt->{name};
my $attr = attr $name;
my $attr = attr $name => $value;
my $attr = attr {
LocalName => $local_name,
NamespaceURI => $ns_uri,
Value => $value,
};
Stringifies as its value: harvey
This is not a SAX event, but an object returned from within element or
start_element objects that gives you access to the NamespaceUri,
LocalName, and Value fields of the attribute. Does not give
access to the Name or Prefix fields present in SAX events.
If you create an attribute with an undefined value, it will stringify
as the undefined value. Attributes that are created without an
explicit undefined Value field will be given the defaul value
of "", including attributes that are autovivified. This allows
get "*" until $_->{id} eq "10";
to work. This has the side effect of addingan id="" attribute to all
elements without an id attribute. To avoid the side effect, use the
exists function to detect nonexistant attributes:
get "*" until exists $_->{id} and $_->{id} eq "10";
aka: end_elt
my $e = end_element "foo";
my $e = end_element $start_elt;
my $e = end_element $end_elt;
my $e = end_element $elt;
Stringifies as: </foo>
See start_element for details on namespace handling.
aka: elt
my $e = elt foo => "content", $other_elt, "more content", $pi, ...;
my $e = elt foo => { attr1 => "val1" }, "content", ...;
Stringifies as: <foo attr1="val1">content</foo>
Never stringifies as an empty element tag (<foo/>), although
downstream filters and handlers may choose to do that.
Constructs an element. An element is a sequence of events between a matching start_element and end_element, inclusive.
Attributes may be accessed using Perl hash dereferencing, as with start_element events, see start_element for details.
Content may be accessed using Perl array dereferencing:
my @content = @$_;
unshift @$_, "prefixed content";
push @$_, "appended content";
Note that
my $elt2 = elt $elt1; ## doesn't copy content, just name+attra
only copies the name and attributes, it does not copy the content. To copy content do either of:
my $elt2 = elt $elt1, @$elt1;
my $elt2 = $elt1->clone;
This is because the first parameter is converted to a start/end_element pair and any content is ignored. This is so that:
my $elt2 = elt $elt1, "new content";
creates an element with the indicated content.
Returns the names of attributes as a list of JamesClarkified
keys, just like start_element's jclark_keys().
Returns the name of the node according to the namespace stringification rules.
Returns the name of the node in James Clark notation.
aka: chars
my $e = chars "A stitch", " in time", " saves nine";
my $e = chars {
Data => "A stitch in time saves nine",
};
Stringifies like a string: A stitch in time saves nine.
Character events are aggregated.
TODO: make that aggregation happen.
aka: (no abbreviation)
my $e = comment "A stitch in time saves nine";
my $e = comment {
Data => "A stitch in time saves nine",
};
Stringifies like a string: A stitch in time saves nine.
Instances of the Essex object model classes carry a reference to the
original data (SAX events), rather than copying it. This means that
there are fewer copies (a good thing; though there is an increased cost
of getting at any data in the events) and that upstream filters may send
blessed, tied, or overloaded objects to us and they will not be molested
unless the Essex filter messes with them. There is also an
implementation reason for this, it makes overloading hash accesses
like $_-{}> easier to implement.
Passing an Essex event to a constructor for a new Essex event does
result in a deep copy of the referenced data (via
XML::Essex::Event::clone()).
The objects in the Essex object model are not available independantly
as class files. You must use XML::Essex::Model to get at them. This
is because there is a set of event types used in almost all SAX filters
and it is cheaper to compile one file containing these than to open
multiple files.
This does not mean that all classes are loaded when the
XML::Essex::Model is use()ed or require()ed, rare events are
likely to be autoloaded.
In order to allow
my $e = XML::Essex::start_elt( ... );
to work as expected--in case the calling package prefers not to import
start_elt(), for instance--the objects in the model are all in the
XML::Essex::Event::... namespace, like
XML::Essex::Event::start_element.
Copyright 2002, R. Barrie Slaymaker, Jr., All Rights Reserved
You may use this module under the terms of the BSD, Artistic, oir GPL licenses, any version.
Barrie Slaymaker <barries@slaysys.com>
| XML-Filter-Essex documentation | Contained in the XML-Filter-Essex distribution. |
package XML::Essex::Model; $VERSION = 0.000_1;
use XML::Essex::Event; { use strict; use Carp (); sub _jclarkify { my ( $name ) = @_; return $name if substr( $name, 0, 1 ) eq "{"; if ( $name =~ /(.*):(.*)/ ) { ## prefix notation ## "TODO: Namespace prefix access for attrs"; return "{foo}$name"; } ## TODO: default to default ns instead of empty ns return "{}$name"; } sub _split_name { my ( $name ) = @_; return ( $1, $2 ) if /^\{(.*)\}(.*)\z/; ## TODO: prefix => URI Namespace mapping return ( "http://foo/", $2 ) if $name =~ /(.*):(.*)/; ## TODO: default to default ns instead of empty ns return ( "", $name ); } sub _render_name { my ( $ns, $local_name ) = @_; $local_name = "*UNDEFINED NAME*" unless defined $local_name; return $local_name unless defined $ns && length $ns; ## TODO: ns => prefix mapping return "foo:$local_name" if $ns =~ /foo/; return "{$ns}$local_name"; } sub _render_event_name { _render_name @{$_[0]}{qw( NamespaceURI LocalName )}; } } { package XML::Essex::Event::start_document; @ISA = qw( XML::Essex::Event ); use strict; sub type { "start_document" } sub types { ( __PACKAGE__, "start_document", "start_doc" ) } sub isa { my $self = shift; return $_[0] eq "start_document" || $_[0] eq "start_doc" || $self->SUPER::isa( @_ ); } @XML::Essex::Event::start_doc::ISA = qw( XML::Essex::Event::start_document ); sub XML::Essex::Event::start_doc::new { my $proto = shift; $proto = __PACKAGE__ if ! ref $proto && $proto eq "XML:Essex::start_doc"; $proto->XML::Essex::Event::start_document::new( @_ ) } }
{ package XML::Essex::Event::xml_decl; @ISA = qw( XML::Essex::Event ); use strict; use overload '""' => \&_stringify; sub type { "xml_decl" } sub types { ( __PACKAGE__, "xml_decl" ) } sub _stringify { my $self = shift; return join "", qq[<?xml version="$$self->{Version}"], exists $$self->{Encoding} && $$self->{Encoding} ? qq[ encoding="$$self->{Encoding}"] : (), exists $$self->{Standalone} && defined $$self->{Standalone} ? qq[ standalone="$$self->{Standalone}"] : (), qq[?>]; } sub isa { my $self = shift; return $_[0] eq "xml_decl" || $self->SUPER::isa( @_ ); } }
{ package XML::Essex::Event::end_document; @ISA = qw( XML::Essex::Event ); use strict; sub type { "end_document" } sub types { ( __PACKAGE__, "end_document", "end_doc" ) } sub isa { my $self = shift; return $_[0] eq "end_document" || $_[0] eq "end_doc" || $self->SUPER::isa( @_ ); } @XML::Essex::Event::end_doc::ISA = qw( XML::Essex::Event::end_document ); sub XML::Essex::Event::end_doc::new { my $proto = shift; $proto = __PACKAGE__ if ! ref $proto && $proto eq "XML:Essex::end_doc"; $proto->XML::Essex::Event::end_document::new( @_ ) } }
{ package XML::Essex::Event::start_element; @ISA = qw( XML::Essex::Event ); use strict; use overload( '""' => \&_stringify, '%{}' => \&_hash_deref, ); sub new { my $self = shift->SUPER::new( ! ref $_[0] ? do { my $elt_name = shift; my $attrs = shift; ( Name => $elt_name, LocalName => $elt_name, $attrs ? ( Attributes => { map { ( "{}$_" => { Name => $_, LocalName => $_, Value => $attrs->{$_}, } ) } keys %$attrs } ) : (), ) } : @_ ); delete $$self->{StartElement}; ## In case an ::element was passed in delete $$self->{EndElement}; ## In case an ::element was passed in delete $$self->{Content}; ## In case an ::element was passed in return $self; } sub type { "start_element" } sub types { ( __PACKAGE__, "start_element", "start_elt" ) } sub _stringify { my $self = shift; my $name = $$self->{LocalName}; if ( defined $$self->{NamespaceURI} && length $$self->{NamespaceURI} ) { ## TODO namespace -> prefix translation $name = "foo:$name"; } ## Work around some odd thread safety thing. ## TODO: See if this can be removed with perl5.8.1 my $s = $$self; my $a = $s->{Attributes}; my $foo = join "", qq[<], $name, keys %$a ? sort map { my $name = $_->{LocalName}; if ( defined $_->{NamespaceURI} && length $_->{NamespaceURI} ) { ## TODO namespace -> prefix translation $name = "foo:$name"; } join "", qq[ ], $name, qq[="], $_->{Value}, qq["]; } values %$a : (), qq[>]; return $foo; } sub _hash_deref { my $self = shift; $$self->{_TiedAttributes} ||= do { my %h; tie %h, "XML::Essex::Event::_tied_attributes", $$self->{Attributes}; \%h; }; return $$self->{_TiedAttributes}; } sub isa { my $self = shift; return $_[0] eq "start_element" || $_[0] eq "start_elt" || $self->SUPER::isa( @_ ); } sub generate_SAX { my $self = shift; return $self->SUPER::generate_SAX( @_ ) unless exists $$self->{_TiedAttributes}; my $ta = delete $$self->{_TiedAttributes}; my $r; my $ok = eval { $r = $self->SUPER::generate_SAX( @_ ); 1; }; $$self->{_TiedAttributes} = $ta; die $@ unless $ok; return $r; } @XML::Essex::Event::start_elt::ISA = qw( XML::Essex::Event::start_element ); sub XML::Essex::Event::start_elt::new { my $proto = shift; $proto = __PACKAGE__ if ! ref $proto && $proto eq "XML:Essex::start_elt"; $proto->XML::Essex::Event::start_element::new( @_ ) }
sub name {
my $self = shift;
return XML::Essex::Model::_render_event_name( $$self );
}
sub jclark_name {
my $self = shift;
return join( "",
"{",
defined $$self->{NamespaceURI}
? $$self->{NamespaceURI} : "",
"}",
$$self->{LocalName}
);
}
sub jclark_keys { keys %{${shift()}->{Attributes}} }
package XML::Essex::Event::_tied_attributes;
## tie %h, "XML...", $event;
sub TIEHASH {
my $proto = shift;
return bless {
Attributes => shift,
Wrappers => {},
}, $proto;
}
sub EXISTS {
return
exists shift->{Attributes}->{XML::Essex::Model::_jclarkify shift};
}
sub FETCH {
my $self = shift;
my $name = XML::Essex::Model::_jclarkify shift;
return $self->{Wrappers}->{$name}
||= XML::Essex::Event::attribute->new(
$self->{Attributes}->{$name} ||= do {
my ( $ns, $name ) = XML::Essex::Model::_split_name $name;
{
LocalName => $name,
NamespaceURI => $ns,
Value => "",
}
}
);
}
sub STORE {
my $self = shift;
my $name = XML::Essex::Model::_jclarkify shift;
my $value = shift;
$self->{Attributes}->{$name} ||= do {
my ( $ns, $name ) = XML::Essex::Model::_split_name $name;
{
LocalName => $name,
NamespaceURI => $ns,
}
};
$self->{Attributes}->{$name}->{Value} = $value;
}
sub DELETE {
my $self = shift;
my $name = XML::Essex::Model::_jclarkify shift;
delete $self->{Attributes}->{$name};
delete $self->{Wrappers}->{$name};
}
sub FIRSTKEY {
my $self = shift;
keys %{$self->{Attributes}}; ## reset each()'s state
## TODO: apply ns=>prefix mappings
my $r = each %{$self->{Attributes}};
$r =~ s/^\{\}//;
return $r;
}
sub NEXTKEY {
my $self = shift;
## TODO: apply ns=>prefix mappings
my $r = each %{$self->{Attributes}};
$r =~ s/^\{\}//;
return $r;
}
}
{ package XML::Essex::Event::attribute; @ISA = qw( XML::Essex::Event ); use strict; use overload '""' => \&_stringify; sub new { my $self = shift->SUPER::new( ! ref $_[0] ? do { my ( $ns, $name ) = XML::Essex::Model::_split_name shift; my $value = @_ ? shift : ""; ( NamespaceURI => $ns, LocalName => $name, Value => $value, Name => undef, Prefix => undef, ) } : @_ ); delete $$self->{StartElement}; ## In case an ::element was passed in delete $$self->{EndElement}; ## In case an ::element was passed in delete $$self->{Content}; ## In case an ::element was passed in return $self; } sub type { "attribute" } sub types { ( __PACKAGE__, "attribute", "attr" ) } sub _stringify { ${shift()}->{Value} } sub isa { my $self = shift; return $_[0] eq "attribute" || $_[0] eq "attr" || $self->SUPER::isa( @_ ); } @XML::Essex::Event::attr::ISA = qw( XML::Essex::Event::attribute ); sub XML::Essex::Event::attr::new { my $proto = shift; $proto = __PACKAGE__ if ! ref $proto && $proto eq "XML:Essex::attr"; $proto->XML::Essex::Event::attribute::new( @_ ) } }
{ package XML::Essex::Event::end_element; @ISA = qw( XML::Essex::Event ); use strict; use overload '""' => \&_stringify; sub new { my $self = shift->SUPER::new( ! ref $_[0] ? do { my $elt_name = shift; ( Name => $elt_name, LocalName => $elt_name, ) } : shift ); delete $$self->{StartElement}; ## In case an ::element was passed in delete $$self->{Content}; ## In case an ::element was passed in delete $$self->{EndElement}; ## In case an ::element was passed in delete $$self->{Attributes}; ## ::element or ::start_element return $self; } sub type { "end_element" } sub types { ( __PACKAGE__, "end_element", "end_elt" ) } sub _stringify { my $self = shift; my $name = $$self->{LocalName}; if ( defined $$self->{NamespaceURI} && length $$self->{NamespaceURI} ) { ## TODO namespace -> prefix translation $name = "foo:$name"; } return join $name, qq[</], qq[>]; } sub isa { my $self = shift; return $_[0] eq "end_element" || $_[0] eq "end_elt" || $self->SUPER::isa( @_ ); } @XML::Essex::Event::end_elt::ISA = qw( XML::Essex::Event::end_element ); sub XML::Essex::Event::end_elt::new { my $proto = shift; $proto = __PACKAGE__ if ! ref $proto && $proto eq "XML:Essex::end_elt"; $proto->XML::Essex::Event::end_element::new( @_ ) } }
{ package XML::Essex::Event::element; @ISA = qw( XML::Essex::Event ); use strict; use overload( '""' => \&_stringify, '%{}' => sub { ${shift()}->{StartElement}->_hash_deref }, '@{}' => sub { ${shift()}->{Content} }, ); sub new { my $proto = shift; my $self = $proto->SUPER::new; if ( @_ ) { my $arg1 = shift; $arg1 = $$arg1->{StartElement} if UNIVERSAL::isa( $arg1, __PACKAGE__ ); $$self->{StartElement} = do { my @args; push @args, shift while @_ && ref $_[0] eq "HASH"; XML::Essex::Event::start_element->new( $arg1, @args ); }; if ( @_ && UNIVERSAL::isa( $_[-1], "XML::Essex::Model::end_element" ) ) { $$self->{EndElement} = pop; } @{$$self->{Content}} = map ! ref $_ ? XML::Essex::Event::characters->new( $_ ) : ref eq "ARRAY" ? XML::Essex::Event::element->new( @$_ ) : Carp::croak( "$_ is not a content for $self" ), @_; } return $self; } sub _set_default_end_element { my $self = shift; return if $$self->{EndElement}; $$self->{EndElement} = XML::Essex::Event::end_element->new( $$self->{StartElement} ); } ## Utility functions used in XML::Handler::Essex to build elements ## from incoming events. sub _start_element { my $self = shift; $$self->{StartElement} = shift if @_; $$self->{StartElement}; } sub _add_content { my $self = shift; push @{$$self->{Content}}, shift; } sub _end_element { my $self = shift; $$self->{EndElement} = shift if @_; $$self->{EndElement}; } sub clone { my $clone = shift->SUPER::clone; $_ = $_->clone for $clone->{StartElement}, @{$clone->{Content}}, $clone->{EndElement}; return $clone; } sub type { "element" } sub types { ( __PACKAGE__, "element", "elt" ) } sub _stringify { my $self = shift; Carp::croak "Can't stringify element with no start_element event" unless $$self->{StartElement}; $self->_set_default_end_element; return join "", $$self->{StartElement}, @{$$self->{Content}}, $$self->{EndElement}; } sub isa { my $self = shift; return $_[0] eq "element" || $_[0] eq "elt" || $self->SUPER::isa( @_ ); } sub generate_SAX { my $self = shift; Carp::croak "Can't generate SAX events for element with no start_element event" unless $$self->{StartElement}; $self->_set_default_end_element; $_->generate_SAX( @_ ) for $$self->{StartElement}, @{$$self->{Content}}, $$self->{EndElement}; } @XML::Essex::Event::elt::ISA = qw( XML::Essex::Event::element ); sub XML::Essex::Event::elt::new { my $proto = shift; $proto = __PACKAGE__ if ! ref $proto && $proto eq "XML:Essex::elt"; $proto->XML::Essex::Event::element::new( @_ ) }
sub jclark_keys { ${shift()}->{StartElement}->jclark_keys }
sub name { ${shift()}->{StartElement}->name }
sub jclark_name { ${shift()}->{StartElement}->jclark_name }
}
{ package XML::Essex::Event::characters; @ISA = qw( XML::Essex::Event ); use strict; use overload '""' => \&_stringify; sub new { my $self = shift->SUPER::new( ! ref $_[0] ? ( Data => join( "", @_ ), ) : shift ); return $self; } sub type { "characters" } sub types { ( __PACKAGE__, "characters", "chars" ) } sub _stringify { ${shift()}->{Data} }; sub isa { my $self = shift; return $_[0] eq "characters" || $_[0] eq "chars" || $self->SUPER::isa( @_ ); } @XML::Essex::Event::chars::ISA = qw( XML::Essex::Event::characters ); sub XML::Essex::Event::chars::new { my $proto = shift; $proto = __PACKAGE__ if ! ref $proto && $proto eq "XML:Essex::chars"; $proto->XML::Essex::Event::characters::new( @_ ) } }
{ package XML::Essex::Event::comment; @ISA = qw( XML::Essex::Event ); use strict; use overload '""' => \&_stringify; sub new { my $self = shift->SUPER::new( ! ref $_[0] ? ( Data => shift, ) : shift ); return $self; } sub type { "comment" } sub types { ( __PACKAGE__, "comment" ) } sub _stringify { ${shift()}->{Data} }; sub isa { my $self = shift; return $_[0] eq "comment" || $self->SUPER::isa( @_ ); } }
1;