#! /usr/bin/perl -w
# vim:set ts=2 sw=2 expandtab:
# xmlformat - configurable XML file formatter/pretty-printer
# Copyright (c) 2004, 2005 Kitebird, LLC. All rights reserved.
# Some portions are based on the REX shallow XML parser, which
# is Copyright (c) 1998, Robert D. Cameron. These include the
# regular expression parsing variables and the shallow_parse()
# method.
# This software is licensed as described in the file LICENSE,
# which you should have received as part of this distribution.
# Syntax: xmlformat [config-file] xml-file
# Default config file is $ENV{XMLFORMAT_CONF} or ./xmlformat.conf, in that
# order.
# Paul DuBois
# paul@kitebird.com
# 2003-12-14
# The input document first is parsed into a list of strings. Each string
# represents one of the following:
# - text node
# - processing instruction (the XML declaration is treated as a PI)
# - comment
# - CDATA section
# - DOCTYPE declaration
# - element tag (either , , or ), *including attributes*
# Entities are left untouched. They appear in their original form as part
# of the text node in which they occur.
# The list of strings then is converted to a hierarchical structure.
# The document top level is represented by a reference to a list.
# Each list element is a reference to a node -- a hash that has "type"
# and "content" key/value pairs. The "type" key indicates the node
# type and has one of the following values:
# "text" - text node
# "pi" - processing instruction node
# "comment" - comment node
# "CDATA" - CDATA section node
# "DOCTYPE" - DOCTYPE node
# "elt" - element node
# (For purposes of this program, it's really only necessary to have "text",
# "elt", and "other". The types other than "text" and "elt" currently are
# all treated the same way.)
# For all but element nodes, the "content" value is the text of the node.
# For element nodes, the "content" hash is a reference to a list of
# nodes for the element's children. In addition, an element node has
# three additional key/value pairs:
# - The "name" value is the tag name within the opening tag, minus angle
# brackets or attributes.
# - The "open_tag" value is the full opening tag, which may also be the
# closing tag.
# - The "close_tag" value depends on the opening tag. If the open tag is
# "", the close tag is "". If the open tag is "", the
# close tag is the empty string.
# If the tree structure is converted back into a string with
# tree_stringify(), the result can be compared to the input file
# as a regression test. The string should be identical to the original
# input document.
use strict;
use Getopt::Long;
$Getopt::Long::ignorecase = 0; # options are case sensitive
$Getopt::Long::bundling = 1; # allow short options to be bundled
my $PROG_NAME = "xmlformat";
my $PROG_VERSION = "1.04";
my $PROG_LANG = "Perl";
# ----------------------------------------------------------------------
package XMLFormat;
use strict;
# ----------------------------------------------------------------------
# Regular expressions for parsing document components. Based on REX.
# SPE = shallow parsing expression
# SE = scanning expression
# CE = completion expression
# RSB = right square brackets
# QM = question mark
my $TextSE = "[^<]+";
my $UntilHyphen = "[^-]*-";
my $Until2Hyphens = "$UntilHyphen(?:[^-]$UntilHyphen)*-";
my $CommentCE = "$Until2Hyphens>?";
my $UntilRSBs = "[^\\]]*\\](?:[^\\]]+\\])*\\]+";
my $CDATA_CE = "$UntilRSBs(?:[^\\]>]$UntilRSBs)*>";
my $S = "[ \\n\\t\\r]+";
my $NameStrt = "[A-Za-z_:]|[^\\x00-\\x7F]";
my $NameChar = "[A-Za-z0-9_:.-]|[^\\x00-\\x7F]";
my $Name = "(?:$NameStrt)(?:$NameChar)*";
my $QuoteSE = "\"[^\"]*\"|'[^']*'";
my $DT_IdentSE = "$S$Name(?:$S(?:$Name|$QuoteSE))*";
my $MarkupDeclCE = "(?:[^\\]\"'><]+|$QuoteSE)*>";
my $S1 = "[\\n\\r\\t ]";
my $UntilQMs = "[^?]*\\?+";
my $PI_Tail = "\\?>|$S1$UntilQMs(?:[^>?]$UntilQMs)*>";
my $DT_ItemSE =
"<(?:!(?:--$Until2Hyphens>|[^-]$MarkupDeclCE)|\\?$Name(?:$PI_Tail))|%$Name;|$S";
my $DocTypeCE = "$DT_IdentSE(?:$S)?(?:\\[(?:$DT_ItemSE)*\\](?:$S)?)?>?";
my $DeclCE =
"--(?:$CommentCE)?|\\[CDATA\\[(?:$CDATA_CE)?|DOCTYPE(?:$DocTypeCE)?";
my $PI_CE = "$Name(?:$PI_Tail)?";
my $EndTagCE = "$Name(?:$S)?>?";
my $AttValSE = "\"[^<\"]*\"|'[^<']*'";
my $ElemTagCE = "$Name(?:$S$Name(?:$S)?=(?:$S)?(?:$AttValSE))*(?:$S)?/?>?";
my $MarkupSPE =
"<(?:!(?:$DeclCE)?|\\?(?:$PI_CE)?|/(?:$EndTagCE)?|(?:$ElemTagCE)?)";
my $XML_SPE = "$TextSE|$MarkupSPE";
# ----------------------------------------------------------------------
# Allowable options and their possible values:
# - The keys of this hash are the allowable option names
# - The value for each key is list of allowable option values
# - If the value is undef, the option value must be numeric
# If any new formatting option is added to this program, it
# must be specified here, *and* a default value for it should
# be listed in the *DOCUMENT and *DEFAULT pseudo-element
# option hashes.
my %opt_list = (
"format" => [ "block", "inline", "verbatim" ],
"normalize" => [ "yes", "no" ],
"subindent" => undef,
"wrap-length" => undef,
"entry-break" => undef,
"exit-break" => undef,
"element-break" => undef
);
# Object creation: set up the default formatting configuration
# and variables for maintaining input and output document.
sub new
{
my $type = shift;
my $self = {};
# Formatting options for each element.
$self->{elt_opts} = { };
# The formatting options for the *DOCUMENT and *DEFAULT pseudo-elements can
# be overridden in the configuration file, but the options must also be
# built in to make sure they exist if not specified in the configuration
# file. Each of the structures must have a value for every option.
# Options for top-level document children.
# - Do not change entry-break: 0 ensures no extra newlines before
# first element of output.
# - Do not change exit-break: 1 ensures a newline after final element
# of output document.
# - It's probably best not to change any of the others, except perhaps
# if you want to increase the element-break.
$self->{elt_opts}->{"*DOCUMENT"} = {
"format" => "block",
"normalize" => "no",
"subindent" => 0,
"wrap-length" => 0,
"entry-break" => 0, # do not change
"exit-break" => 1, # do not change
"element-break" => 1
};
# Default options. These are used for any elements in the document
# that are not specified explicitly in the configuration file.
$self->{elt_opts}->{"*DEFAULT"} = {
"format" => "block",
"normalize" => "no",
"subindent" => 1,
"wrap-length" => 0,
"entry-break" => 1,
"exit-break" => 1,
"element-break" => 1
};
# Run the *DOCUMENT and *DEFAULT options through the option-checker
# to verify that the built-in values are legal.
my $err_count = 0;
foreach my $elt_name (keys (%{$self->{elt_opts}})) # ... for each element
{
# Check each option for element
while (my ($opt_name, $opt_val) = each (%{$self->{elt_opts}->{$elt_name}}))
{
my $err_msg;
($opt_val, $err_msg) = check_option ($opt_name, $opt_val);
if (!defined ($err_msg))
{
$self->{elt_opts}->{$elt_name}->{$opt_name} = $opt_val;
}
else
{
warn "LOGIC ERROR: $elt_name default option is invalid\n";
warn "$err_msg\n";
++$err_count;
}
}
}
# Make sure that the every option is represented in the
# *DOCUMENT and *DEFAULT structures.
foreach my $opt_name (keys (%opt_list))
{
foreach my $elt_name (keys (%{$self->{elt_opts}}))
{
if (!exists ($self->{elt_opts}->{$elt_name}->{$opt_name}))
{
warn "LOGIC ERROR: $elt_name has no default '$opt_name' option\n";
++$err_count;
}
}
}
die "Cannot continue; internal default formatting options must be fixed\n"
if $err_count > 0;
bless $self, $type; # bless object and return it
}
# Initialize the variables that are used per-document
sub init_doc_vars
{
my $self = shift;
# Elements that are used in the document but not named explicitly
# in the configuration file.
$self->{unconf_elts} = { };
# List of tokens for current document.
$self->{tokens} = [ ];
# List of line numbers for each token
$self->{line_num} = [ ];
# Document node tree (constructed from the token list).
$self->{tree} = [ ];
# Variables for formatting operations:
# out_doc = resulting output document (constructed from document tree)
# pending = array of pending tokens being held until flushed
$self->{out_doc} = "";
$self->{pending} = [ ];
# Inline elements within block elements are processed using the
# text normalization (and possible line-wrapping) values of their
# enclosing block. Blocks and inlines may be nested, so we maintain
# a stack that allows the normalize/wrap-length values of the current
# block to be determined.
$self->{block_name_stack} = [ ]; # for debugging
$self->{block_opts_stack} = [ ];
# A similar stack for maintaining each block's current break type.
$self->{block_break_type_stack} = [ ];
}
# Accessors for token list and resulting output document
sub tokens
{
my $self = shift;
return $self->{tokens};
}
sub out_doc
{
my $self = shift;
return $self->{out_doc};
}
# Methods for adding strings to output document or
# to the pending output array
sub add_to_doc
{
my ($self, $str) = @_;
$self->{out_doc} .= $str;
}
sub add_to_pending
{
my ($self, $str) = @_;
push (@{$self->{pending}}, $str);
}
# Block stack mainenance methods
# Push options onto or pop options off from the stack. When doing
# this, also push or pop an element onto the break-level stack.
sub begin_block
{
my ($self, $name, $opts) = @_;
push (@{$self->{block_name_stack}}, $name);
push (@{$self->{block_opts_stack}}, $opts);
push (@{$self->{block_break_type_stack}}, "entry-break");
}
sub end_block
{
my $self = shift;
pop (@{$self->{block_name_stack}});
pop (@{$self->{block_opts_stack}});
pop (@{$self->{block_break_type_stack}});
}
# Return the current block's normalization status or wrap length
sub block_normalize
{
my $self = shift;
my $size = @{$self->{block_opts_stack}};
my $opts = $self->{block_opts_stack}->[$size-1];
return $opts->{normalize} eq "yes";
}
sub block_wrap_length
{
my $self = shift;
my $size = @{$self->{block_opts_stack}};
my $opts = $self->{block_opts_stack}->[$size-1];
return $opts->{"wrap-length"};
}
# Set the current block's break type, or return the number of newlines
# for the block's break type
sub set_block_break_type
{
my ($self, $type) = @_;
my $size = @{$self->{block_break_type_stack}};
$self->{block_break_type_stack}->[$size-1] = $type;
}
sub block_break_value
{
my $self = shift;
my $size = @{$self->{block_opts_stack}};
my $opts = $self->{block_opts_stack}->[$size-1];
$size = @{$self->{block_break_type_stack}};
my $type = $self->{block_break_type_stack}->[$size-1];
return $opts->{$type};
}
# ----------------------------------------------------------------------
# Read configuration information. For each element, construct a hash
# containing a hash key and value for each option name and value.
# After reading the file, fill in missing option values for
# incomplete option structures using the *DEFAULT options.
sub read_config
{
my $self = shift;
my $conf_file = shift;
my @elt_names = ();
my $err_msg;
my $in_continuation = 0;
my $saved_line = "";
open (FH, $conf_file) or die "Cannot read config file $conf_file: $!\n";
while ()
{
chomp;
next if /^\s*($|#)/; # skip blank lines, comments
if ($in_continuation)
{
$_ = $saved_line . " " . $_;
$saved_line = "";
$in_continuation = 0;
}
if (!/^\s/)
{
# Line doesn't begin with whitespace, so it lists element names.
# Names are separated by whitespace or commas, possibly followed
# by a continuation character or a comment.
if (/\\$/)
{
s/\\$//; # remove continuation character
$saved_line = $_;
$in_continuation = 1;
next;
}
s/\s*#.*$//; # remove any trailing comment
@elt_names = split (/[\s,]+/, $_);
# make sure each name has an entry in the elt_opts structure
foreach my $elt_name (@elt_names)
{
$self->{elt_opts}->{$elt_name} = { }
unless exists ($self->{elt_opts}->{$elt_name});
}
}
else
{
# Line begins with whitespace, so it contains an option
# to apply to the current element list, possibly followed by
# a comment. First check that there is a current list.
# Then parse the option name/value.
die "$conf_file:$.: Option setting found before any "
. "elements were named.\n"
if !@elt_names;
s/\s*#.*$//;
my ($opt_name, $opt_val) = /^\s+(\S+)(?:\s+|\s*=\s*)(\S+)$/;
die "$conf_file:$.: Malformed line: $_\n" unless defined ($opt_val);
# Check option. If illegal, die with message. Otherwise,
# add option to each element in current element list
($opt_val, $err_msg) = check_option ($opt_name, $opt_val);
die "$conf_file:$.: $err_msg\n" if defined ($err_msg);
foreach my $elt_name (@elt_names)
{
$self->{elt_opts}->{$elt_name}->{$opt_name} = $opt_val;
}
}
}
close (FH);
# For any element that has missing option values, fill in the values
# using the options for the *DEFAULT pseudo-element. This speeds up
# element option lookups later. It also makes it unnecessary to test
# each option to see if it's defined: All element option structures
# will have every option defined.
my $def_opts = $self->{elt_opts}->{"*DEFAULT"};
foreach my $elt_name (keys (%{$self->{elt_opts}}))
{
next if $elt_name eq "*DEFAULT";
foreach my $opt_name (keys (%{$def_opts}))
{
next if exists ($self->{elt_opts}->{$elt_name}->{$opt_name}); # already set
$self->{elt_opts}->{$elt_name}->{$opt_name} = $def_opts->{$opt_name};
}
}
}
# Check option name to make sure it's legal. Check the value to make sure
# that it's legal for the name. Return a two-element array:
# (value, undef) if the option name and value are legal.
# (undef, message) if an error was found; message contains error message.
# For legal values, the returned value should be assigned to the option,
# because it may get type-converted here.
sub check_option
{
my ($opt_name, $opt_val) = @_;
# - Check option name to make sure it's a legal option
# - Then check the value. If there is a list of values
# the value must be one of them. Otherwise, the value
# must be an integer.
return (undef, "Unknown option name: $opt_name")
unless exists ($opt_list{$opt_name});
my $allowable_val = $opt_list{$opt_name};
if (defined ($allowable_val))
{
return (undef, "Unknown '$opt_name' value: $opt_val")
unless grep (/^$opt_val$/, @{$allowable_val});
}
else # other options should be numeric
{
# "$opt_val" converts $opt_val to string for pattern match
return (undef, "'$opt_name' value ($opt_val) should be an integer")
unless "$opt_val" =~ /^\d+$/;
}
return ($opt_val, undef);
}
# Return hash of option values for a given element. If no options are found:
# - Add the element name to the list of unconfigured options.
# - Assign the default options to the element. (This way the test for the
# option fails only once.)
sub get_opts
{
my $self = shift;
my $elt_name = shift;
my $opts = $self->{elt_opts}->{$elt_name};
if (!defined ($opts))
{
$self->{unconf_elts}->{$elt_name} = 1;
$opts = $self->{elt_opts}->{$elt_name} = $self->{elt_opts}->{"*DEFAULT"};
}
return $opts;
}
# Display contents of configuration options to be used to process document.
# For each element named in the elt_opts structure, display its format
# type, and those options that apply to the type.
sub display_config
{
my $self = shift;
# Format types and the additional options that apply to each type
my $format_opts = {
"block" => [
"entry-break",
"element-break",
"exit-break",
"subindent",
"normalize",
"wrap-length"
],
"inline" => [ ],
"verbatim" => [ ]
};
foreach my $elt_name (sort (keys (%{$self->{elt_opts}})))
{
print "$elt_name\n";
my %opts = %{$self->{elt_opts}->{$elt_name}};
my $format = $opts{format};
# Write out format type, then options that apply to the format type
print " format = $format\n";
foreach my $opt_name (@{$format_opts->{$format}})
{
print " $opt_name = $opts{$opt_name}\n";
}
print "\n";
}
}
# Display the list of elements that are used in the document but not
# configured in the configuration file.
# Then re-unconfigure the elements so that they won't be considered
# as configured for the next document, if there is one.
sub display_unconfigured_elements
{
my $self = shift;
my @elts = keys (%{$self->{unconf_elts}});
if (@elts == 0)
{
print "The document contains no unconfigured elements.\n";
}
else
{
print "The following document elements were assigned no formatting options:\n";
foreach my $line ($self->line_wrap ([ join (" ", sort (@elts)) ], 0, 0, 65))
{
print "$line\n";
}
}
foreach my $elt_name (@elts)
{
delete ($self->{elt_opts}->{$elt_name});
}
}
# ----------------------------------------------------------------------
# Main document processing routine.
# - Argument is a string representing an input document
# - Return value is the reformatted document, or undef. An undef return
# signifies either that an error occurred, or that some option was
# given that suppresses document output. In either case, don't write
# any output for the document. Any error messages will already have
# been printed when this returns.
sub process_doc
{
my $self = shift;
my ($doc, $verbose, $check_parser, $canonize_only, $show_unconf_elts) = @_;
my $str;
$self->init_doc_vars ();
# Perform lexical parse to split document into list of tokens
warn "Parsing document...\n" if $verbose;
$self->shallow_parse ($doc);
if ($check_parser)
{
warn "Checking parser...\n" if $verbose;
# concatentation of tokens should be identical to original document
if ($doc eq join ("", @{$self->tokens ()}))
{
print "Parser is okay\n";
}
else
{
print "PARSER ERROR: document token concatenation differs from document\n";
}
return undef;
}
# Assign input line number to each token
$self->assign_line_numbers ();
# Look for and report any error tokens returned by parser
warn "Checking document for errors...\n" if $verbose;
if ($self->report_errors () > 0)
{
warn "Cannot continue processing document.\n";
return undef;
}
# Convert the token list to a tree structure
warn "Converting document tokens to tree...\n" if $verbose;
if ($self->tokens_to_tree () > 0)
{
warn "Cannot continue processing document.\n";
return undef;
}
# Check: Stringify the tree to convert it back to a single string,
# then compare to original document string (should be identical)
# (This is an integrity check on the validity of the to-tree and stringify
# operations; if one or both do not work properly, a mismatch should occur.)
#$str = $self->tree_stringify ();
#print $str;
#warn "ERROR: mismatch between document and resulting string\n" if $doc ne $str;
# Canonize tree to remove extraneous whitespace
warn "Canonizing document tree...\n" if $verbose;
$self->tree_canonize ();
if ($canonize_only)
{
print $self->tree_stringify () . "\n";
return undef;
}
# One side-effect of canonizing the tree is that the formatting
# options are looked up for each element in the document. That
# causes the list of elements that have no explicit configuration
# to be built. Display the list and return if user requested it.
if ($show_unconf_elts)
{
$self->display_unconfigured_elements ();
return undef;
}
# Format the tree to produce formatted XML as a single string
warn "Formatting document tree...\n" if $verbose;
$self->tree_format ();
# If the document is not empty, add a newline and emit a warning if
# reformatting failed to add a trailing newline. This shouldn't
# happen if the *DOCUMENT options are set up with exit-break = 1,
# which is the reason for the warning rather than just silently
# adding the newline.
$str = $self->out_doc ();
if ($str ne "" && $str !~ /\n$/)
{
warn "LOGIC ERROR: trailing newline had to be added\n";
$str .= "\n";
}
return $str;
}
# ----------------------------------------------------------------------
# Parse XML document into array of tokens and store array
sub shallow_parse
{
my ($self, $xml_document) = @_;
$self->{tokens} = [ $xml_document =~ /$XML_SPE/g ];
}
# ----------------------------------------------------------------------
# Extract a tag name from a tag and return it.
# Dies if the tag cannot be found, because this is supposed to be
# called only with a legal tag.
sub extract_tag_name
{
my $tag = shift;
die "Cannot find tag name in tag: $tag\n" unless $tag =~ /^<\/?($Name)/;
return $1;
}
# ----------------------------------------------------------------------
# Assign an input line number to each token. The number indicates
# the line number on which the token begins.
sub assign_line_numbers
{
my $self = shift;
my $line_num = 1;
$self->{line_num} = [ ];
for (my $i = 0; $i < @{$self->{tokens}}; $i++)
{
my $token = $self->{tokens}->[$i];
push (@{$self->{line_num}}, $line_num);
# count newlines and increment line counter (tr returns no. of matches)
$line_num += ($token =~ tr/\n/\n/);
}
}
# ----------------------------------------------------------------------
# Check token list for errors and report any that are found. Error
# tokens are those that begin with "<" but do not end with ">".
# Returns the error count.
# Does not modify the original token list.
sub report_errors
{
my $self = shift;
my $err_count = 0;
for (my $i = 0; $i < @{$self->{tokens}}; $i++)
{
my $token = $self->{tokens}->[$i];
if ($token =~ /^ && $token !~ />$/)
{
my $line_num = $self->{line_num}->[$i];
warn "Malformed token at line $line_num, token " . ($i+1) . ": $token\n";
++$err_count;
}
}
warn "Number of errors found: $err_count\n" if $err_count > 0;
return $err_count;
}
# ----------------------------------------------------------------------
# Helper routine to print tag stack for tokens_to_tree
sub print_tag_stack
{
my ($label, @stack) = @_;
if (@stack < 1)
{
warn " $label: none\n";
}
else
{
warn " $label:\n";
for (my $i = 0; $i < @stack; $i++)
{
warn " ", ($i+1), ": ", $stack[$i], "\n";
}
}
}
# Convert the list of XML document tokens to a tree representation.
# The implementation uses a loop and a stack rather than recursion.
# Does not modify the original token list.
# Returns an error count.
sub tokens_to_tree
{
my $self = shift;
my @tag_stack = (); # stack for element tags
my @children_stack = (); # stack for lists of children
my $children = [ ]; # current list of children
my $err_count = 0;
for (my $i = 0; $i < @{$self->{tokens}}; $i++)
{
my $token = $self->{tokens}->[$i];
my $line_num = $self->{line_num}->[$i];
my $tok_err = "Error near line $line_num, token " . ($i+1) . " ($token)";
if ($token !~ /^) # text
{
push (@{$children}, text_node ($token));
}
elsif ($token =~ /^