James Risner
James Risner

Reputation: 6104

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.

Upvotes: 3

Views: 133

Answers (1)

Aristotle Pagaltzis
Aristotle Pagaltzis

Reputation: 118108

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.

Upvotes: 3

Related Questions