############################################################################## # tago : Arbitrary Tag Object Perl Module # Copyright 2009 David A. Gershman, gershman@dagertech.net # # This program is distributed under the terms of the GNU General # Public License v3. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ############################################################################## # Arbitrary Tag Object package tago; $VERSION = '1.0.1'; sub new { my ( $pkg, %rest ) = @_; my @list = (); # Items that go between the open/close tags my @parents = (); # Objects that point to this one my $obj = bless { '__list' => \@list, '__close' => -1, # Whether to use a closing tag '__oid' => '', # Allegedly unique Object ID '__string' => '', # Generated string '__tag' => '', # Tag name used in and '__parents' => \@parents }, $pkg; if ( defined $rest{ '-tag' } ) { $obj -> tag( $rest{ '-tag' } ); } else { $obj -> tag( 'none' ); } $obj -> oid( $rest{ '-oid' } ) if ( $rest{ '-oid' } ); $obj -> close( $rest{ '-close' } ); $obj -> add2end( $rest{ '-add' } ) if ( $rest{ '-add' } ); $obj -> setrest( %rest ); return $obj; } # When created, all attributes of a tag are arbitrary. sub setrest { my ( $obj, %r ) = @_; my $attr; # chuck tags we no longer need, should have been set via 'new' delete $r{ '-tag' }; delete $r{ '-oid' }; delete $r{ '-close' }; delete $r{ '-insert' }; delete $r{ '-add' }; foreach ( keys %r ) { $_ =~ s/^\-//; # chuck the dashes $obj -> { $_ } = $r{ "-$_" }; # Add to the attributes } } # Set/Get objects Unique ID sub oid { my ( $obj, $n ) = @_; $obj -> { '__oid' } = $n if ( $n ); return $obj -> { '__oid' }; } # Set/Get objects tag name sub tag { my ( $obj, $n ) = @_; $obj -> { '__tag' } = $n if ( $n ); return $obj -> { '__tag' }; } # Set/Get whether tag has a closing tag # $n==1 => yes; $n==0 => no; $n==-1 => guess sub close { my ( $obj, $n ) = @_; $n = $obj -> { '__close' } if ( ! defined $n ); $obj -> { '__close' } = ( $n == 1 || $n == 0 ) ? $n : guess_close( $obj -> tag() ); return $obj -> { '__close' }; } # Return number of items in objects containing list sub count { my ( $obj ) = @_; return scalar @{ $obj -> { '__list' } }; } # Duh. sub clear { my ( $obj ) = @_; $obj -> { '__list' } = (); } # Generates the tag and all its contents, returns resulting string sub string { my ( $obj ) = @_; $obj -> generate(); return $obj -> { '__string' }; } # Traverses the tags list structures and generates the string sub generate { my ( $obj ) = @_; my $key; my $string = ( $obj -> tag() eq 'none' ) ? '' : ( '<' . $obj -> tag() ); # If there is no tag name, then don't bother with attributes. if ( $obj -> tag() ne 'none' ) { # Add all the attributes foreach ( keys %$obj ) { # Skip reserved attributes next if ( /^__/ ); next if ( $obj -> { $_ } =~ /^\-$/ ); if ( $obj -> { $_ } =~ /^\+$/i ) { $string .= " $_"; } else { $string .= " $_=\"" . $obj -> { $_ } . "\""; } } $string .= ">"; } # Add all the elements of the list foreach $key( @{ $obj -> { '__list' } } ) { $string .= ( ref $key ) ? ( $key -> string() ) : $key; } # Close the tag if needed. $string .= " tag() . ">" if ( $obj -> close() ); $obj -> { '__string' } = $string; } # Given a list of attributes, will return a hash with the attributes # as keys and the values as values. In the unique case where the # list of attributes requested is in fact only one, we'll return # the value itself, not a hash. sub get_attr { my ( $obj, @attrs ) = @_; my %values = (); # Allow a special case of getting a single attribute return $obj -> { $_ } if ( $#attrs == 0 ); # Else return a hash foreach ( @attrs ) { $values{ $_ } = $obj -> { $_ }; } return %values; } # Takes a collection of attr/value, adds them to the object. sub set_attr { my ( $obj, %rest ) = @_; foreach ( keys %rest ) { $obj -> { $_ } = $rest{ $_ }; } return 1; } # Tries to guess whether the tag should have a closing tag or not. sub guess_close { my ( $t ) = @_; $t = lc $t; return 0 if ( $t eq 'br' || $t eq 'frame' || $t eq 'hr' || $t eq 'img' || $t eq 'none' ); return 1; } # Return the list of objects with this in their lists. sub parents { my ( $obj ) = @_; return @{ $obj -> { '__parents' } }; } # Find 1st occurance of $oid sub find { my ( $obj, $oid ) = @_; my $found = ''; # Are we it? return $obj if ( $obj -> oid() eq $oid ); # No, then go through my list, recursing on each object. foreach ( @{ $obj -> { '__list' } } ) { $found = $_ -> find( $oid ) if ( ref( $_ ) ); return $found if ( $found ); } return ''; } # Same as find, but returns a list of all $oids in the structure sub find_all { my ( $obj, $oid ) = @_; my @found = (); push @found, $obj if ( $obj -> oid() eq $oid ); foreach ( @{ $obj -> { '__list' } } ) { push @found, $_ -> find_all( $oid ) if ( ref( $_ ) ); } return @found; } # Takes an array of objects/items and adds them to the end of this # objects list. sub add2end { my ( $obj, @stuff_to_add ) = @_; my $save; # Normally I'd just append the @stuff_to_add array to the '__list', # but I want to track the parents, so I need to go through and look # for more objects. foreach ( @stuff_to_add ) { # Add the new item to our list. push @{ $obj -> { '__list' } }, $_; # Now add this obj to the items parents list. push @{ $_ -> { '__parents' } }, $obj if ( ref( $_ ) ); # Save the item just added so the last one will be # return later $save = $_; } # return the last object added. return $save; } # Take an array of items to add and put them on the front. # Similar to 'add2end', but opposite end of this object's list sub add2front { my ( $obj, @stuff_to_add ) = @_; my $save; foreach ( @stuff_to_add ) { unshift @{ $obj -> { '__list' } }, $_; push @{ $_ -> { '__parents' } }, $obj if ( ref( $_ ) ); $save = $_; } return $save; # return the last object added. } # Finds an item in our current list with OID $oid and adds provided array # items before $oid. sub add_before { my ( $obj, $oid, @stuff_to_add ) = @_; my $index, $position; my @list = @{ $obj -> { '__list' } }; # want to work with array, not ref my @first_part, @second_part; # Go through our list and find $oid $position = -1; for ( $index = 0; $index <= $#list; $index++ ) { if ( ref( $list[ $index ] ) && ( $list[ $index ] -> oid() eq $oid ) ) { $position = $index; last; } } # If $oid was not found, just add2end return $obj -> add2end( @stuff_to_add ) if ( $position < 0 ); # If $oid was found at front, just add2front return $obj -> add2front( @stuff_to_add ) if ( $position == 0 ); # Else, we split our list in 2 @first_part = @list[ 0..$position-1 ]; @second_part = @list[ $position..$#list ]; # Set our current list to the first part so we can... $obj -> { '__list' } = \@first_part; # Just add2end. This will handle the parent tracking as well. $save = $obj -> add2end( @stuff_to_add ); # Now combine the two lists... @list = ( @{ $obj -> { '__list' } }, @second_part ); # ...and finally set our current list to the result. $obj -> { '__list' } = \@list; return $save; # Just return the last item added. } # Similar to add_after, but the $oid is in the first part of the # list and not the second. sub add_after { my ( $obj, $oid, @stuff_to_add ) = @_; my $index, $position; my @list = @{ $obj -> { '__list' } }; my @first_part, @second_part; # Find $oid $position = -1; for ( $index = 0; $index <= $#list; $index++ ) { if ( ref( $list[ $index ] ) && ( $list[ $index ] -> oid() eq $oid ) ) { $position = $index; last; } } # Handle end cases return $obj -> add2end( @stuff_to_add ) if ( $position < 0 ); return $obj -> add2front( @stuff_to_add ) if ( $position == $#list ); # Now insert where desired. @first_part = @list[ 0..$position ]; @second_part = @list[ $position+1..$#list ]; $obj -> { '__list' } = \@first_part; $save = $obj -> add2end( @stuff_to_add ); @list = ( @{ $obj -> { '__list' } }, @second_part ); $obj -> { '__list' } = \@list; return $save; } # Deleting a tag has two different frames of thought. # 1) Delete the current tag, but leave everything it contains. # i.e. Given "NOW", remove the tag, but keep "NOW" # 2) Remove a tag and everything it contains. # Only removes the tag information, leave data structure in tact. # May waste some space, but it keeps from having to process # parent lists of all items in the objects lists. sub delete_tag_only { my ( $obj ) = @_; # Clear all attributes foreach ( keys %$obj ) { next if ( /^__/ ); # I'll deal with the private ones delete $obj -> { $_ }; } # Now deal with private ones. I need to deal with them separately # because I want to maintain the data structure. So in reality # they're not deleted, just initialized. $obj -> { '__close' } = 0; $obj -> { '__oid' } = ''; $obj -> { '__tag' } = 'none'; return $obj; } # Deletes the object and all its contents sub delete_all { my ( $obj ) = @_; my @parents = $obj -> parents(); my $oid = $obj -> oid(); my ( $index, $parent ); # We really just need to delete all references to the object from any # parents. foreach $parent ( @parents ) { # For each parent, go find the reference to this object # and remove it. Actually, the array position in its parents # list is set to ''. Again, this prevents extra processing # of shifting the array. for ( $index = 0; $index < $parent -> count(); $index++ ) { next if ( ! ref( ${ $parent -> { '__list' } }[ $index ] ) ); ${ $parent -> { '__list' } }[ $index ] = '' if ( ${ $parent -> { '__list' } }[ $index ] -> oid() eq $oid ); } } return 1; } # Not really part of a tag, but helpful for parsing template HTML files # syntax: # #tag description# # Tag description: -attr=val;;{-attr=val;;...;;-attr=val} # sub phadd { my ( $obj, $file ) = @_; my @static = split( /\#[^\#]+\#/, $file ); my @tags = $file =~ /\#([^\#]+)\#/g; my $i; my @pairs = (); my %rest = (); my $attr, $val; ( $first, $second ) = ( ! $static[0] ) ? ( \@tags, \@static ) : ( \@static, \@tags ); # Change all tag descriptions to objects for ( $i = 0; $i <= $#tags; $i++ ) { @pairs = split ';;', $tags[ $i ]; $tags[ $i ] = new tago(); foreach ( @pairs ) { ( $attr, $val ) = split /\s*\=\s*/; if ( $attr =~ /^\-add$/i ) { $tags[ $i ] -> add( $val ); } elsif ( $attr =~ /^\-oid$/i ) { $tags[ $i ] -> oid( $val ); } elsif ( $attr =~ /^\-close$/i ) { $tags[ $i ] -> close( $val ); } elsif ( $attr =~ /^\-tag$/i ) { $tags[ $i ] -> tag( $val ); } else { $rest{ $attr } = $val; } } $tags[ $i ] -> setrest( %rest ); } $obj -> add( shift @$first ) if (( scalar @$first ) > ( scalar @$second )); $i = scalar @$first; while( $i > 0 ) { $obj -> add( shift @$second ); $obj -> add( shift @$first ); $i--; } return 1; } sub phadd_file { my ( $obj, $filename ) = @_; my @lines; open( F, $filename ) or do { return 0; }; @lines = ; close( F ); $obj -> phadd( join( '', @lines ) ); return 1; } # For convenience, have some aliases for common subs sub prune { $_[0] -> delete_all(); } sub delete { $_[0] -> delete_tag_only(); } sub add { my ( $obj, @rest ) = @_; return $obj -> add2end( @rest ); } 1;