XML icon indicating copy to clipboard operation
XML copied to clipboard

Iteratively using replace() on nodes gives unexpected results

Open bronco-creek opened this issue 10 years ago • 6 comments

For example:

my @elements = $mydocument.root.nodes; for @elements -> $element { my $new-element = @new-elements.pop; $mydocument.root.replace($element, $new-element); }

seems to result in only the first and last $new-element ending up in the document. Sorry, I have not golfed this down further. I see a similar result with the replaceChild() syntax.

bronco-creek avatar Mar 17 '15 00:03 bronco-creek

Also weird, when I look at the code for the other bug, I'll look into this one too.

supernovus avatar Mar 20 '15 17:03 supernovus

Confirm dropping elements (...in 2024), not sure if my syntax is valid. Still, replacing nodes with self drops 1/2 the elements:

~$ raku -MXML -e 'my $mydocument=open-xml($*ARGFILES.Str);  \
                  my @elements = $mydocument.root.nodes;       \
                  my @new-elements = $mydocument.root.nodes;      \
                  @elements.say; say "\n____\n"; @new-elements.say; say "\n____\n";  \
                  for @elements -> $element {    \
                      my $new-element = @new-elements.pop;    \
                      $mydocument.root.replace($element, $new-element);  \
                  }; .say for $mydocument;' ~/exemel_text.xml

[
   <file>text1</file>
   <file>text2</file>
   <file>text3</file>
   <file>text4</file>
]

____

[
   <file>text1</file>
   <file>text2</file>
   <file>text3</file>
   <file>text4</file>
]

____

<?xml version="1.0"?><root>
  <file>text1</file>
  <file>text2</file>
  </root>
~$

Try reversing the @new-elements replacement, now all 4 elements are replaced with blanks! (Also...weirdness with printing reversed array):

~$ raku -MXML -e 'my $mydocument=open-xml($*ARGFILES.Str);  \
                  my @elements = $mydocument.root.nodes;       \
                  my @new-elements = $mydocument.root.nodes.reverse;    \  
                  @elements.say; say "\n____\n"; @new-elements.say; say "\n____\n";  \
                  for @elements -> $element {    \
                      my $new-element = @new-elements.pop;    \
                      $mydocument.root.replace($element, $new-element);     \
                  }; .say for $mydocument;' ~/exemel_text.xml

[
   <file>text1</file>
   <file>text2</file>
   <file>text3</file>
   <file>text4</file>
]

____

[
 <file>text4</file>
   <file>text3</file>
   <file>text2</file>
   <file>text1</file>
  ]

____

<?xml version="1.0"?><root>




</root>
~$ 

Different syntax but still errors: "No such method 'replace' for invocant of type 'Array'"

 ~$ raku -MXML -e 'my $mydocument=open-xml($*ARGFILES.Str);     \
                   my @elements = $mydocument.root.nodes;          \
                   my @new-elements = $mydocument.root.nodes.reverse;     \
                   for @(@elements,@new-elements) -> $old,$new {   \
                       $mydocument.root.nodes.replace($old, $new);     \
                   };  .say for $mydocument;' ~/exemel_text.xml
No such method 'replace' for invocant of type 'Array'
  in block <unit> at -e line 1

Maybe related to #69 ?

@supernovus @bronco-creek @jonathanstowe @lizmat @dwarring @vrurg

jubilatious1 avatar Feb 28 '24 00:02 jubilatious1

Here's a working one-liner for replacing text within user identified nodes ( restricted to top-level with :RECURSE(0) and restricted to :TAG{"title"}):

~$ raku -MXML -e 'my  $xml = open-xml( $*ARGFILES.Str );
                  for $xml.elements( :RECURSE(0), :TAG{"title"} ) -> $E {
                      my $old = $E.contents[0];
                      my $new = XML::Text.new( text => $old.text.subst(/^old-text$/, "new-text" ) );
                      $E.replace( $old, $new );
                  };  .say for $xml;'   file.xml

See: https://unix.stackexchange.com/a/771574/227738

Maybe add a warning to the XML docs page about trying to replace Elements without converting to XML::Element first?

jubilatious1 avatar Mar 04 '24 16:03 jubilatious1

I added a bunch of debug output so you can see a little bit more clearly what's going on:

full execution with some unnecessary extra output

raku -MXML -e 'sub debugout($n) { $n.^name ~ " " ~ $n.gist.raku }; my $mydocument=open-xml($*ARGFILES.Str);  \
                  my @elements = $mydocument.root.nodes.eager;       \
                  my @new-elements = $mydocument.root.nodes.eager;      \
                  .&debugout.say for @elements; say "\n____\n"; .&debugout.say for @new-elements; say "\n____\n";  \ 
                  for @elements -> $element {    \
                      my $new-element = @new-elements.pop;    \
                      say "replacing", debugout($element), " -> ", debugout($new-element); say ("replacement result: " ~ debugout $mydocument.root.replace($element, $new-element)).indent(4); say "    entire document now:"; .&debugout.indent(8).say for $mydocument.nodes;  \
                  }; say "\n____\n"; .&debugout.say for $mydocument.nodes;' exemel_text.xml 
XML::Text "\n  "
XML::Element "<file>text1</file>"
XML::Text "\n  "
XML::Element "<file>text2</file>"
XML::Text "\n  "
XML::Element "<file>text3</file>"
XML::Text "\n  "
XML::Element "<file>text4</file>"
XML::Text "\n"

____

XML::Text "\n  "
XML::Element "<file>text1</file>"
XML::Text "\n  "
XML::Element "<file>text2</file>"
XML::Text "\n  "
XML::Element "<file>text3</file>"
XML::Text "\n  "
XML::Element "<file>text4</file>"
XML::Text "\n"

____

replacingXML::Text "\n  " -> XML::Text "\n"
    replacement result: Array "[\n  ]"
    entire document now:
        XML::Text "\n"
        XML::Element "<file>text1</file>"
        XML::Text "\n  "
        XML::Element "<file>text2</file>"
        XML::Text "\n  "
        XML::Element "<file>text3</file>"
        XML::Text "\n  "
        XML::Element "<file>text4</file>"
replacingXML::Element "<file>text1</file>" -> XML::Element "<file>text4</file>"
    replacement result: Array "[<file>text1</file>]"
    entire document now:
        XML::Text "\n"
        XML::Element "<file>text4</file>"
        XML::Text "\n  "
        XML::Element "<file>text2</file>"
        XML::Text "\n  "
        XML::Element "<file>text3</file>"
        XML::Text "\n  "
replacingXML::Text "\n  " -> XML::Text "\n  "
    replacement result: Array "[\n  ]"
    entire document now:
        XML::Text "\n"
        XML::Element "<file>text4</file>"
        XML::Text "\n  "
        XML::Element "<file>text2</file>"
        XML::Text "\n  "
        XML::Element "<file>text3</file>"
replacingXML::Element "<file>text2</file>" -> XML::Element "<file>text3</file>"
    replacement result: Array "[<file>text2</file>]"
    entire document now:
        XML::Text "\n"
        XML::Element "<file>text4</file>"
        XML::Text "\n  "
        XML::Element "<file>text3</file>"
        XML::Text "\n  "
replacingXML::Text "\n  " -> XML::Text "\n  "
    replacement result: Array "[]"
    entire document now:
        XML::Text "\n"
        XML::Element "<file>text4</file>"
        XML::Text "\n  "
        XML::Element "<file>text3</file>"
        XML::Text "\n  "
replacingXML::Element "<file>text3</file>" -> XML::Element "<file>text2</file>"
    replacement result: Array "[<file>text3</file>]"
    entire document now:
        XML::Text "\n"
        XML::Element "<file>text4</file>"
        XML::Text "\n  "
        XML::Element "<file>text2</file>"
        XML::Text "\n  "
replacingXML::Text "\n  " -> XML::Text "\n  "
    replacement result: Array "[\n  ]"
    entire document now:
        XML::Text "\n"
        XML::Element "<file>text4</file>"
        XML::Text "\n  "
        XML::Element "<file>text2</file>"
        XML::Text "\n  "
replacingXML::Element "<file>text4</file>" -> XML::Element "<file>text1</file>"
    replacement result: Array "[<file>text4</file>]"
    entire document now:
        XML::Text "\n"
        XML::Element "<file>text1</file>"
        XML::Text "\n  "
        XML::Element "<file>text2</file>"
        XML::Text "\n  "
replacingXML::Text "\n" -> XML::Text "\n  "
    replacement result: Array "[\n]"
    entire document now:
        XML::Text "\n  "
        XML::Element "<file>text1</file>"
        XML::Text "\n  "
        XML::Element "<file>text2</file>"
        XML::Text "\n  "

____

XML::Text "\n  "
XML::Element "<file>text1</file>"
XML::Text "\n  "
XML::Element "<file>text2</file>"
XML::Text "\n  "

here's a bit shorter version that you get when you remove spaces and newlines from the exemel_text.xml since the text nodes are a bit distracting:

raku -MXML -e 'sub debugout($n) { $n.^name ~ " " ~ $n.gist.raku }; my $mydocument=open-xml($*ARGFILES.Str);  \
                  my @elements = $mydocument.root.nodes.eager;       \
                  my @new-elements = $mydocument.root.nodes.eager;      \
                  .&debugout.say for @elements; say "\n____\n"; .&debugout.say for @new-elements; say "\n____\n";  \ 
                  for @elements -> $element {    \
                      my $new-element = @new-elements.pop;    \
                      say "replacing ", debugout($element), " -> ", debugout($new-element); say ("replacement result: " ~ debugout $mydocument.root.replace($element, $new-element)).indent(4); say "    entire document now:"; .&debugout.indent(8).say for $mydocument.nodes;  \
                  }; say "\n____\n"; .&debugout.say for $mydocument.nodes;' exemel_text_nospace.xml 
XML::Element "<file>text1</file>"
XML::Element "<file>text2</file>"
XML::Element "<file>text3</file>"
XML::Element "<file>text4</file>"

____

XML::Element "<file>text1</file>"
XML::Element "<file>text2</file>"
XML::Element "<file>text3</file>"
XML::Element "<file>text4</file>"

____

replacing XML::Element "<file>text1</file>" -> XML::Element "<file>text4</file>"
    replacement result: Array "[<file>text1</file>]"
    entire document now:
        XML::Element "<file>text4</file>"
        XML::Element "<file>text2</file>"
        XML::Element "<file>text3</file>"
replacing XML::Element "<file>text2</file>" -> XML::Element "<file>text3</file>"
    replacement result: Array "[<file>text2</file>]"
    entire document now:
        XML::Element "<file>text4</file>"
        XML::Element "<file>text3</file>"
replacing XML::Element "<file>text3</file>" -> XML::Element "<file>text2</file>"
    replacement result: Array "[<file>text3</file>]"
    entire document now:
        XML::Element "<file>text4</file>"
        XML::Element "<file>text2</file>"
replacing XML::Element "<file>text4</file>" -> XML::Element "<file>text1</file>"
    replacement result: Array "[<file>text4</file>]"
    entire document now:
        XML::Element "<file>text1</file>"
        XML::Element "<file>text2</file>"

____

XML::Element "<file>text1</file>"
XML::Element "<file>text2</file>"

As you can see, we are:

starting state:             1 2 3 4
replacing 1 with 4, result: 4 2 3
replacing 2 with 3, result: 4 3
replacing 3 with 2, result: 4 2
replacing 4 with 1, result: 1 2

The code does pretty much exactly what you would expect, but you need to be aware that one node cannot be in more than one place at a time, which necessitates that the first two steps reduce the number of nodes, leaving us with 2 at the end.

timo avatar Feb 19 '25 22:02 timo

@timo thanks for all your hard work!

So "using replace() on nodes" essentially creates a pointer from an existing node "X" to existing node "Y", effectively eliminating node "X"?

Thx.

jubilatious1 avatar Feb 20 '25 19:02 jubilatious1

I would not call it a pointer. replace goes through the children on the node you call .replace on, finds the spot where the node you pass as first argument sits, puts the node you pass as the second argument in that same place, also reparenting it so that the tree stays correct (nodes under a node are supposed to have their $.parent attribute set to the node that directly contains it)

now that i look more closely at this:

https://github.com/raku-community-modules/XML/blob/83e00879f2e8aa1018faa0a22a50be17138c61f4/lib/XML/Element.rakumod#L71-L76

which calls this:

https://github.com/raku-community-modules/XML/blob/83e00879f2e8aa1018faa0a22a50be17138c61f4/lib/XML/Node.rakumod#L36-L40

I don't see any mention of the original node getting .remove called on it. This would result in a Node that things $foo is its parent, but $foo doesn't have it inside its @.nodes.

I haven't looked closely enough at the code to know whether this is a problem or not.

timo avatar Feb 20 '25 19:02 timo