Some time ago, I wrote an application to allow me to view the output of the "Results" option that Blackboard Lean (oops: Learn) provides us. Since most answers use HTML, the content of individual answers can be obscured at best. At worst, impossible.
So, I created a filter that takes the CSV file and turns it into an HTML file with a table (aspects of questions are columns, student submissions are rows). I recently upgraded it to allow me to specify a particular set of columns that I was interested in. But I was very dissatisfied when I looked at the HTML class which I had previously used to set up the output tags. This was how it looked:
/** * Mutable class to form an HTML string */
class HTML() { val content = new StringBuilder("") val tagStack: mutable.Stack[String] = mutable.Stack[String]() def tag(w: String): StringBuilder = { tagStack.push(w) content.append(s"<$w>") } def unTag: StringBuilder = content.append(s"</${tagStack.pop()}>") def append(w: String): StringBuilder = content.append(w) def close(): Unit = while (tagStack.nonEmpty) { unTag } override def toString: String = content.toString + "\n"
}And this was how it was used:
def preamble(w: String): String = { val result = new HTML result.tag("head") result.tag("title") result.append(w) result.close() result.toString }def parseStreamIntoHTMLTable(ws: Stream[String], title: String): String = { val result = new HTML result.tag("html") result.append(preamble(title)) result.tag("body") result.tag("table") ws match { case header #:: body => result.append(parseRowIntoHTMLRow(header, header = true)) for (w <- body) result.append(parseRowIntoHTMLRow(w)) } result.close() result.toString }The rendering of the code here doesn't actually show that the mutable Stack class is deprecated. How could I be using a deprecated class--and for mutation too! Ugh! Well, it was a utility, not an exemplar for my students. So, it was acceptable. But I decided to functionalize it. First, I needed to create a trait which had the basic behavior I needed:Together with an abstract base class:/** * Trait Tag to model an Markup Language-type document. */ trait Tag { /** * Method to yield the name of this Tag * * @return the name, that's to say what goes between < and > */ def name: String /** * Method to yield the attributes of this Tag. * * @return a sequence of attributes, each of which is a String */ def attributes: Seq[String] /** * Method to yield the content of this Tag. * * @return the content as a String. */ def content: String /** * Method to yield the child Tags of this Tag. * * @return a Seq of Tags. */ def tags: Seq[Tag] /** * Method to add a child to this Tag * * @param tag the tag to be added * @return a new version of this Tag with the additional tag added as a child */ def :+(tag: Tag): Tag /** * Method to yield the tag names depth-first in a Seq * * @return a sequence of tag names */ def \\ : Seq[String] = name +: (for (t <- tags; x <- t.\\) yield x) }Here's one of the places the tags in the document are set up:abstract class BaseTag(name: String, attributes: Seq[String], content: String, tags: Seq[Tag])(implicit rules: TagRules) extends Tag { override def toString: String = s"""\n${tagString()}$content$tagsString${tagString(true)}""" private def attributeString(close: Boolean) = if (close || attributes.isEmpty) "" else " " + attributes.mkString(" ") private def tagsString = if (tags.isEmpty) "" else tags mkString "" private def nameString(close: Boolean = false) = (if (close) "/" else "") + name private def tagString(close: Boolean = false) = s"<${nameString(close)}${attributeString(close)}>" } And a case class for HTML with its companion object:
/** * Case class to model an HTML document. * @param name the name of the tag at the root of the document. * @param attributes the attributes of the tag. * @param content the content of the tag. * @param tags the child tags. * @param rules the "rules" (currently ignored) but useful in the future to validate documents. */ case class HTML(name: String, attributes: Seq[String], content: String, tags: Seq[Tag])(implicit rules: TagRules) extends BaseTag(name, attributes, content, tags) { /** * Method to add a child to this Tag * * @param tag the tag to be added * @return a new version of this Tag with the additional tag added as a child */ override def :+(tag: Tag): Tag = HTML(name, attributes, content, tags :+ tag) } /** * Companion object to HTML */ object HTML { implicit object HtmlRules extends TagRules def apply(name: String, attributes: Seq[String], content: String): HTML = apply(name, attributes, content, Nil) def apply(name: String, attributes: Seq[String]): HTML = apply(name, attributes, "") def apply(name: String): HTML = apply(name, Nil) def apply(name: String, content: String): HTML = apply(name, Nil, content) }Now, all we have to do is to use .toString on the document to render it for the final HTML source!def parseStreamProjectionIntoHTMLTable(columns: Seq[String], wss: Stream[Seq[String]], title: String): Try[Tag] = Try { val table = HTML("table", Seq("""border="1"""")) :+ parseRowProjectionIntoHTMLRow(columns, header = true) val body = HTML("body") :+ wss.foldLeft(table)((tag, ws) => tag :+ parseRowProjectionIntoHTMLRow(ws)) HTML("html") :+ preamble(title) :+ body }