I'm aware that XML doesn't care about attribute order, but a closed source application I use requires it. It also makes tracking changes with the XML using git
easier to visualize.
I've attempted to change the order using Twig's keep_atts_order
and twig_handler
options, but the change does not appear to be reflected.
I currently handle the problem using regexs after Twig has handled the XML, but I'd rather have Twig change the order internally.
My attempt at a solution is below:
use Tie::IxHash;
use XML::Twig;
sub setorder {
my $item = $_[0];
# Copy hashes, then delete.
my %attrs = %{$item->atts};
$item->del_atts;
for my $i ( 1 .. (scalar @_ - 1) ) {
my $att = $_[$i];
if (exists($attrs{$att})) {
$item->set_att($att, $attrs{$att});
delete($attrs{$att});
}
}
}
my $twig = XML::Twig -> new(
keep_atts_order => 1,
twig_handlers => { template => sub { setorder($_, 'id', 'name', 'comp') }, },
);
my $xmldata = <<'XML';
<?xml version="1.0" encoding="UTF-8"?>
<thing>
<template name="Op" id="x1" comp="Saga">
</template>
</thing>
XML
$twig->parse ( $xmldata );
my $root = $twig->root;
print $root->print() . "\n";
The only similar question I have found is How to change the order of the attributes of an XML element using Perl and XML::Twig. It is easily solved by allowing Twig to alpha sort the attributes, but I need a non-alpha sorted order.
You didn’t do anything wrong, you found a bug in XML::Twig (as of version 3.52). While the bug still exists, you can work around it by replacing your call to del_atts
…
$item->del_atts;
… with a call to del_att
to delete all attributes individually:
$item->del_att( $item->att_names );
Your code will then work.
The bug is that the del_atts
method is implemented like this:
sub del_atts { $_[0]->{att}={}; return $_[0]; }
This replaces the hash that stores the attributes, without checking keep_atts_order
to see if the attributes hash is supposed to be tied.
Either the method should include the same conditional as set_att
does:
$_[0]->{att}={}; tie %{$elt->{att}}, 'Tie::IxHash' if (keep_atts_order());
… or it should clear the existing hash instead of replacing it with a new empty one:
%{ $_[0]->{att} }=();
… or even – though I’m not sure this one is correct – just remove the hash entirely (leaving re-creation of the hash to set_att
if needed, where the code is already correct):
delete $_[0]->{att};
Given any of these changes to the method, your original unchanged code would work.