virtualeyes
virtualeyes

Reputation: 11245

Implicitly Appending Scala XML Literals

Scala's support for XML literals is fantastic for generating type safe XHTML markup; however, there's an annoying gotcha in that you must append literals together with the ++ operator when non-xml-literal code is included in a given block.

For example, this blows up:

import scala.xml._

def getNode() = <div>foo</div>
val node: NodeSeq = 
  <div>bar</div>
  <div>baz</div>{
    getNode()
  }

 error: type mismatch;
 found   : scala.xml.Elem
 required: Int
  getNode()

and while the below compiles, notice how you have to ++ after the non-xml-literal getNode() call, otherwise only the tail Elem is generated.

import scala.xml._

def getNode() = <div>foo</div>
val node: NodeSeq = 
  <div>bar</div>
  <div>baz</div> ++ {
    getNode()
  }
  <div>lonely node</div>

node: scala.xml.NodeSeq = NodeSeq(<div>bar</div>, <div>baz</div>, <div>foo</div>)
res6: scala.xml.Elem = <div>lonely node</div>

So, is there a way to implicitly chain XML literals interspersed with XML returning Scala code by default? As it stands you get a false sense of security when working with XML literals since the above example compiles but is effectively broken.

One way to explicitly force appending of literals would be to do something like:

import scala.xml._
def getNode() = <div>foo</div>
def nodify(elems: Elem*): NodeSeq = elems
nodify(
  <div>bar</div>,
  <div>baz</div>,
  getNode(),
  <div>not lonely node</div>
)

res8: scala.xml.NodeSeq = NodeSeq(<div>bar</div>, <div>baz</div>, <div>foo</div>, <div>not lonely node</div>)

but that's not so nice in that you have to wrap the XML literal blocks in a method call. At least it force appends, won't compile without.

Would love to have a clean XHTML markup DSL, please provide the secret sauce if you've got it! (or clue me in if I'm missing something blindingly obvious).

Thanks

Upvotes: 2

Views: 1056

Answers (3)

virtualeyes
virtualeyes

Reputation: 11245

So, after hacking around a bit I've discovered that the explicit approach using varargs is the way to go.

Check it out, for generating html markup you're usually doing so within a layout or partial, something like:

object Crud extends Layout{
  def apply[T <: Seq[Node]](title: String, args: (Symbol,String)*)(body: T*) {
    header(...)
    <body>
      {body}
    </body>
    footer(...)
  }
}

and then you want to implicitly chain the markup that comprises the body of above layout. As per above answers there's no elegant way to pull off implicit chaining. With var args however, you can have your cake and eat it too. Before implementing the varargs approach a form to create Foos looked like:

Crud(title, ...)(
  <h1>{title}</h1>
  <div>&nbsp;</div> ++
  withForm(routes.Foo.save)(
    fields(form) ++
    crud.submit("Create")
  ) ++
  css("foo") ++ 
  css("bar")    
)

with zero compile time guarantee that a ++ wasn't forgotten, totally error prone. With varargs as per above Crud apply signature foo form create looks like this:

Crud(title, ...)(
  <h1>{title}</h1>
  <div>&nbsp;</div>,
  withForm(routes.Foo.save)(
    fields(form),
    crud.submit("Create")
  ),
  css("foo"),
  css("bar")    
)

and if you omit a comma you get a compile time error, exactly what we want. On the "plus" side you can still use ++ if desired since Seq[Node] lies at the top of Scala XML type hierarchy, so Elem, NodeSeq, etc. doesn't matter, whatever it is it just has to derive from Seq[Node] and you're good to go, woohoo ;-)

Upvotes: 0

som-snytt
som-snytt

Reputation: 39587

As a refresher (for me), your braces are not an embedded Scala expression, which has to be content inside a tag.

Your error message says you're calling NodeBuffer.apply{f}, so that's the enhancement below.

When you have two Elements in a row, you get a NodeBuffer, and the compiler builds them with &+.

scala> import xml._
import xml._

scala> def f = <div>foo</div>
f: scala.xml.Elem

scala> implicit class `autoappend Elem`(b: NodeBuffer) { def apply(e: Elem) = b &+ e }
defined class autoappend$u0020Elem

scala> :pa
// Entering paste mode (ctrl-D to finish)

val n =
<div>bar</div>
<div>baz</div>{
f
}

// Exiting paste mode, now interpreting.

n: scala.xml.NodeBuffer = ArrayBuffer(<div>bar</div>, <div>baz</div>, <div>foo</div>)

scala> val nn: NodeSeq = n
nn: scala.xml.NodeSeq = NodeSeq(<div>bar</div>, <div>baz</div>, <div>foo</div>)

You can still:

scala> n(1)
res0: scala.xml.Node = <div>baz</div>

scala> n(2)
res1: scala.xml.Node = <div>foo</div>

Doesn't everyone use interpolators these days? Is that a fad or a trend?

Upvotes: 1

Alex Cruise
Alex Cruise

Reputation: 7979

The best way I know of to handle this type of situation is to wrap the expressions in the no-op <xml:group/> element:

import scala.xml._

def getNode() = <div>foo</div>

val node: NodeSeq = <xml:group>
  <div>bar</div>
  <div>baz</div>{
    getNode()
  }
</xml:group>

yields:

scala> node
res0: scala.xml.NodeSeq = 

<div>bar</div>
<div>baz</div><div>foo</div>

Upvotes: 1

Related Questions