Search code examples
xmlperlxml-twig

How to change attribute to a specific non-alphabetic order using XML::Twig?


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.


Solution

  • 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.