Change XML in Scala without mutation?

I am trying to replace XML fragments and you need a battery along the way. Let's say I have a populated question that is stored as XML:

val q = <text>The capitals of Bolivia are <blank/> and <blank/>.</text> 

At some point, I want to turn these spaces into HTML input elements, and I need to be able to distinguish between the first and second so that I can check them. (Ignore the fact that in this case, the two capitals can appear in any order - with a headache, which I will deal with later).

Thanks to the excellent answers on StackOverflow, I created the following solution:

 import scala.xml._ import scala.xml.transform._ class BlankReplacer extends BasicTransformer { var i = 0 override def transform(n: Node): NodeSeq = n match { case <blank/> => { i += 1 <input name={ "blank.%d".format(i) }/> } case elem: Elem => elem.copy(child=elem.child.flatMap(transform _)) case _ => n } } 

and it works quite well. I need to create new BlankReplacer() every time I want to start re-numbering, but this works a lot:

 scala> new BlankReplacer()(q) res6: scala.xml.Node = <text>The capitals of Bolivia are <input name="blank.1"></input> and <input name="blank.2"></input>.</text> 

That is the question. Is there an easy way to avoid the mutation that I have to do every time I replace <blank/> ? What I have does not seem terrible to me, but I think it could be cleaner if I did not create a new instance of the BlankReplacer class every time I had to convert the question to HTML. I'm sure there is a way to turn this into a drive, but I'm not sure how to do this.

Thanks! Todd

+4
source share
2 answers

Scales Xml provides path folding, allowing you to “modify" the tree and accumulate ...

 import scales.utils._ import ScalesUtils._ import scales.xml._ import ScalesXml._ // the xml example val q = <("text") /( "The capitals of Bolivia are ", <("blank")," and ",<("blank"),".") // which elems to fold on? val p = top(q) \\* "blank" val f = foldPositions(p, p.size){ // size is used as the starting point for the fold case (question, path) => (question - 1, Replace( <("input") /@ ("name" -> ("blank."+question) )) ) } // f is an either, so we assuming its working here, and ._1 is the accumalator, _.2 the Path val newTree = f.left.get._2.tree 

The only feature is that it accumulates in the reverse order of the document, there is also a non-accumulating version. This allows a combination of transformations when some of them are destructive (for example, changing a subkild and then deleting it in another transformation, everything just works).

Entering the fold itself is any Iterable of Paths, if they are in the same tree, which allows you to combine queries as you wish.

For more details see here how Fold Xml to scale

+1
source

This is exactly the problem that Anti-XML Lightning is designed to solve :

In other words, we started with an XML tree, we deployed to this tree using a selector, we got a new version of this result set with some changes (in our case, a new attribute), and now we want to return to the tree that we originally had, except for the change that we lowered in the bowels. This is what the zipper for ...

In your case, you can do something like this:

 import com.codecommit.antixml._ def replaceBlanks(el: Elem) = { var i = 0 (el \\ "blank").map { _ => i += 1 <input name={"blank.%d".format(i)}/>.convert }.unselect } 

Or you can avoid var using the trick in this answer :

 def replaceBlanks(el: Elem) = { val blanks = el \\ "blank" (0 until blanks.size).foldLeft(blanks) { case (z, i) => z.updated(i, z(i).copy( name = "input", attrs = Attributes("name" -> "blank.%d".format(i + 1))) ) }.unselect } 

Now we can apply the method to your element (after converting it to com.codecommit.antixml.Elem ):

 scala> println(replaceBlanks(q.convert)) <text>The capitals of Bolivia are <input name="blank.1"/> and <input name="blank.2"/>.</text> 

The trick is that we can use \\ to dig a tree, as in the case of scala.xml , but unlike scala.xml we can make changes to the resulting "node set" (actually with a zipper) and then return them to their original context using unselect .

+4
source

All Articles