#!/usr/bin/perl -w
#
# $Revision: 1.1.1.1 $
#
# $Date: 2003-07-27 11:07:11 $

use XML::Parser;

my $Usage = <<'End_of_Usage;';
Usage is:
    xmlfilter [-h] [-nl] [{-+}root] [{-+}el=elname] [{-+}el:elnamepat]
              [{-+}att:attname] [{-+}att:attname:attvalpat] xmlfile

Prints on standard output the result of filtering the given xmlfile
for elements according to the switches. A '-' option will drop the
element from the output; a '+' will keep it. The output should also
be a well-formed XML document.

    -h		Print this message

    -nl         Emit a newline prior to every start tag.

    [-+]root	Drop (or keep) the root element. Defaults to keep.
		If the root element were named "foo", then -root
		would be equivalent to -el=foo. Note that even if
		you're dropping the root element, it's start and
		end tag are kept in order that the output remains
		a well-formed XML document.

    [-+]el=elname
		Drop (or keep) elements of type elname.

    [-+]el:elnamepat
		Drop (or keep) element whose type name matches elnamepat.

    [-+]att:attname
		Drop (or keep) elements which have an attribute = attname.

    [-+]att:attname:attvalpat
		Drop (or keep) elements which have an attribute = attname
		and for which the attribute value matches attvalpat.
End_of_Usage;

my $pass       = 1;
my $do_newline = 0;

my $attcheck = 0;

my %drop_el;
my @drop_elpat;

my %keep_el;
my @keep_elpat;

my %drop_att;
my %keep_att;

my $always_true = sub { 1; };
my $root_element = '';

my $in_cdata = 0;

# Process options

while ( defined( $ARGV[0] ) and $ARGV[0] =~ /^[-+]/ ) {
    my $opt = shift;

    if ( $opt eq '-root' ) {
        $pass = 0;
    }
    elsif ( $opt eq '+root' ) {
        $pass = 1;
    }
    elsif ( $opt eq '-h' ) {
        print $Usage;
        exit;
    }
    elsif ( $opt eq '-nl' ) {
        $do_newline = 1;
    }
    elsif ( $opt =~ /^([-+])el([:=])(\S*)/ ) {
        my ( $disp, $kind, $pattern ) = ( $1, $2, $3 );
        my ( $hashref, $aref );

        if ( $disp eq '-' ) {
            $hashref = \%drop_el;
            $aref    = \@drop_elpat;
        }
        else {
            $hashref = \%keep_el;
            $aref    = \@keep_elpat;
        }

        if ( $kind eq '=' ) {
            $hashref->{$pattern} = 1;
        }
        else {
            push( @$aref, $pattern );
        }
    }
    elsif ( $opt =~ /^([-+])att:(\w+)(?::(\S*))?/ ) {
        my ( $disp, $id, $pattern ) = ( $1, $2, $3 );
        my $ref = ( $disp eq '-' ) ? \%drop_att : \%keep_att;

        if ( defined($pattern) ) {
            $pattern =~ s!/!\\/!g;
            my $sub;
            eval "\$sub = sub {\$_[0] =~ /$pattern/;};";

            $ref->{$id} = $sub;
        }
        else {
            $ref->{$id} = $always_true;
        }

        $attcheck = 1;
    }
    else {
        die "Unknown option: $opt\n$Usage";
    }
}

my $drop_el_pattern = join( '|', @drop_elpat );
my $keep_el_pattern = join( '|', @keep_elpat );

my $drop_sub;
if ($drop_el_pattern) {
    eval "\$drop_sub = sub {\$_[0] =~ /$drop_el_pattern/;}";
}
else {
    $drop_sub = sub { };
}

my $keep_sub;
if ($keep_el_pattern) {
    eval "\$keep_sub = sub {\$_[0] =~ /$keep_el_pattern/;}";
}
else {
    $keep_sub = sub { };
}

my $doc = shift;

die "No file specified\n$Usage" unless defined($doc);

my @togglestack = ();

my $p = new XML::Parser(
    ErrorContext => 2,
    Handlers     => {
        Start => \&start_handler,
        End   => \&end_handler
    }
);

if ($pass) {
    $p->setHandlers(
        Char       => \&char_handler,
        CdataStart => \&cdata_start,
        CdataEnd   => \&cdata_end
    );
}

$p->parsefile($doc);

print "</$root_element>\n"
  unless $pass;

################
## End of main
################

sub start_handler {
    my $xp = shift;
    my $el = shift;

    unless ($root_element) {
        $root_element = $el;
        print "<$el>\n"
          unless $pass;
    }

    my ( $elref, $attref, $sub );

    if ($pass) {
        $elref  = \%drop_el;
        $attref = \%drop_att;
        $sub    = $drop_sub;
    }
    else {
        $elref  = \%keep_el;
        $attref = \%keep_att;
        $sub    = $keep_sub;
    }

    if (   defined( $elref->{$el} )
        or &$sub($el)
        or check_atts( $attref, @_ ) ) {
        $pass = !$pass;
        if ($pass) {
            $xp->setHandlers(
                Char       => \&char_handler,
                CdataStart => \&cdata_start,
                CdataEnd   => \&cdata_end
            );
        }
        else {
            $xp->setHandlers(
                Char       => 0,
                CdataStart => 0,
                CdataEnd   => 0
            );
        }
        push( @togglestack, $xp->depth );
    }

    if ($pass) {
        print "\n" if $do_newline;
        print "<$el";
        while (@_) {
            my $id  = shift;
            my $val = shift;

            $val = $xp->xml_escape( $val, "'" );
            print " $id='$val'";
        }
        print ">";
    }
}    # End start_handler

sub end_handler {
    my $xp = shift;
    my $el = shift;

    if ($pass) {
        print "</$el>";
    }

    if ( @togglestack and $togglestack[-1] == $xp->depth ) {
        $pass = !$pass;
        if ($pass) {
            $xp->setHandlers(
                Char       => \&char_handler,
                CdataStart => \&cdata_start,
                CdataEnd   => \&cdata_end
            );
        }
        else {
            $xp->setHandlers(
                Char       => 0,
                CdataStart => 0,
                CdataEnd   => 0
            );
        }

        pop(@togglestack);
    }

}    # End end_handler

sub char_handler {
    my ( $xp, $text ) = @_;

    if ( length($text) ) {

        $text = $xp->xml_escape( $text, '>' )
          unless $in_cdata;

        print $text;
    }
}    # End char_handler

sub cdata_start {
    my $xp = shift;

    print '<![CDATA[';
    $in_cdata = 1;
}

sub cdata_end {
    my $xp = shift;

    print ']]>';
    $in_cdata = 0;
}

sub check_atts {
    return $attcheck unless $attcheck;

    my $ref = shift;

    while (@_) {
        my $id  = shift;
        my $val = shift;

        if ( defined( $ref->{$id} ) ) {
            my $ret = &{ $ref->{$id} }($val);
            return $ret if $ret;
        }
    }

    return 0;
}    # End check_atts

# Tell Emacs that this is really a perl script
# Local Variables:
# mode:perl
# End:
