From 7d9cfd9fea6d376e001b311de4667346d7f145f8 Mon Sep 17 00:00:00 2001 From: "Yang, Bo" Date: Wed, 16 Feb 2022 22:45:04 -0800 Subject: [PATCH] Handle the missing Searching.Found(`endIndex`) case (fix #439) --- .../binding/html/InterpolationParser.scala | 125 +++++++++++------- .../binding/html/Issue439Spec.scala | 55 ++++++++ .../thoughtwors/binding/html/htmlSpec.scala | 16 --- 3 files changed, 133 insertions(+), 63 deletions(-) create mode 100644 html/src/test/scala/com/thoughtwors/binding/html/Issue439Spec.scala diff --git a/html-InterpolationParser/src/main/scala/com/thoughworks/binding/html/InterpolationParser.scala b/html-InterpolationParser/src/main/scala/com/thoughworks/binding/html/InterpolationParser.scala index 34f10751..ba6d67f4 100644 --- a/html-InterpolationParser/src/main/scala/com/thoughworks/binding/html/InterpolationParser.scala +++ b/html-InterpolationParser/src/main/scala/com/thoughworks/binding/html/InterpolationParser.scala @@ -9,6 +9,8 @@ import org.apache.xerces.xni.QName import org.apache.xerces.xni.XMLAttributes import org.apache.xerces.xni.XMLString import org.apache.xerces.xni.parser.XMLInputSource +import org.w3c.dom.Document +import org.w3c.dom.DocumentFragment import org.w3c.dom.Node import org.xml.sax.InputSource @@ -41,12 +43,56 @@ object InterpolationParser: args.values.to(BitSet) ) - def parseHtmlParts( + /** Concatenate `parts` and parse it as HTML, assuming there are arguments + * between each parts. + * + * @example + * Given two HTML elements, and an argument as the content of the first + * element, + * {{{ + * val parts = IndexedSeq("
", "

") + * }}} + * when parsing it, + * {{{ + * val htmlFragment = InterpolationParser.parseHtmlParts( + * parts, + * { (message, _) => fail(message) }, + * + * ) + * }}} + * then the result should be an HTML fragment including the two elements, + * {{{ + * import javax.xml.transform.stream.StreamResult + * import javax.xml.transform.TransformerFactory + * import javax.xml.transform.OutputKeys + * import javax.xml.transform.dom.DOMSource + * import java.io.StringWriter + * import scala.util.chaining.given + * val writer = new StringWriter() + * TransformerFactory + * .newInstance() + * .newTransformer() + * .tap(_.setOutputProperty(OutputKeys.METHOD, "html")) + * .tap(_.setOutputProperty(OutputKeys.INDENT, "no")) + * .transform(new DOMSource(htmlFragment), new StreamResult(writer)) + * writer.toString() should be("

") + * }}} + * and there should be a placeholder in the first element. + * {{{ + * val placeholder = htmlFragment.getFirstChild().getFirstChild() + * val argIndex = placeholder.getUserData( + * InterpolationParser.ElementArgumentUserDataKey + * ) + * argIndex should be(0) + * }}} + */ + def parseHtmlParts[Fragment <: DocumentFragment]( parts: IndexedSeq[String], - argumentErrorHandler: (message: String, argumentIndex: Int) => Unit - ) = + argumentErrorHandler: (message: String, argumentIndex: Int) => Unit, + document: Document { def createDocumentFragment(): Fragment } = + org.apache.html.dom.HTMLDocumentImpl() + ): Fragment = val parser = new InterpolationParser(parts, argumentErrorHandler) - val document = org.apache.html.dom.HTMLDocumentImpl() val fragment = document.createDocumentFragment() val html = parts.mkString(Placeholder) parser.parse(InputSource(StringReader(html)), fragment) @@ -156,41 +202,36 @@ private class InterpolationParser( if beginIndex == endIndex || (beginIndex % 2 == 0 && endIndex == beginIndex + 1) then super.characters(text, augs) else + def parseTextData(encodedText: String): Unit = + if encodedText.nonEmpty then + fParserConfiguration + .asInstanceOf[HTMLConfiguration] + .evaluateInputSource( + XMLInputSource( + null, + null, + null, + StringReader(encodedText), + null + ) + ) + end if def partLoop(index: Int): Unit = assert(index % 2 == 0) endSearchResult match case Searching.InsertionPoint(`endIndex`) if endIndex == index + 1 => - fParserConfiguration - .asInstanceOf[HTMLConfiguration] - .evaluateInputSource( - XMLInputSource( - null, - null, - null, - StringReader( - html.substring( - partOffsets(index), - endCharacterOffset - ) - ), - null - ) + parseTextData( + html.substring( + partOffsets(index), + endCharacterOffset ) + ) + case Searching.Found(`endIndex`) if endIndex == index => + // break case _ => - fParserConfiguration - .asInstanceOf[HTMLConfiguration] - .evaluateInputSource( - XMLInputSource( - null, - null, - null, - StringReader( - parts(index / 2) - ), - null - ) - ) + assert(index < endIndex) + parseTextData(parts(index / 2)) argLoop(index + 1) end match end partLoop @@ -212,22 +253,12 @@ private class InterpolationParser( beginSearchResult match case Searching.InsertionPoint(`beginIndex`) if beginIndex % 2 == 1 => - fParserConfiguration - .asInstanceOf[HTMLConfiguration] - .evaluateInputSource( - XMLInputSource( - null, - null, - null, - StringReader( - html.substring( - beginCharacterOffset, - partOffsets(beginIndex) - ) - ), - null - ) + parseTextData( + html.substring( + beginCharacterOffset, + partOffsets(beginIndex) ) + ) argLoop(beginIndex) case Searching.Found(`beginIndex`) if beginIndex % 2 == 1 => argLoop(beginIndex) diff --git a/html/src/test/scala/com/thoughtwors/binding/html/Issue439Spec.scala b/html/src/test/scala/com/thoughtwors/binding/html/Issue439Spec.scala new file mode 100644 index 00000000..d360e9b6 --- /dev/null +++ b/html/src/test/scala/com/thoughtwors/binding/html/Issue439Spec.scala @@ -0,0 +1,55 @@ +package com.thoughtworks.binding +package html +import org.scalatest.freespec.AsyncFreeSpec +import org.scalatest.matchers.should.Matchers +import com.thoughtworks.dsl.keywords.Await +import com.thoughtworks.dsl.Dsl +import com.thoughtworks.dsl.macros.Reset.Default.`*` +import Binding.BindingSeq +import bindable.BindableSeq +import org.scalajs.dom.Node +import scala.concurrent.Future +import org.scalajs.dom.Element +import org.scalajs.dom.document + +final class Issue439Spec extends AsyncFreeSpec with Matchers { + + "The content of element 1 is an argument; element 2 is empty" in { + val children = html"
${"argument"}
" + *[Future] { + ( + for snapshot <- !Await(children.snapshots.toLazyList) + yield { + for node <- snapshot.toList + yield node.asInstanceOf[Element].outerHTML + }.mkString + ) should be( + LazyList( + "", + "
argument
", + "
argument
" + ) + ) + } + } + + "The content of element 1 is an argument and some more text; element 2 is empty" in { + val children = html"
${"text"} and some more text
" + *[Future] { + ( + for snapshot <- !Await(children.snapshots.toLazyList) + yield { + for node <- snapshot.toList + yield node.asInstanceOf[Element].outerHTML + }.mkString + ) should be( + LazyList( + "", + "
argument and some more text
", + "
argument and some more text
" + ) + ) + } + } + +} diff --git a/html/src/test/scala/com/thoughtwors/binding/html/htmlSpec.scala b/html/src/test/scala/com/thoughtwors/binding/html/htmlSpec.scala index 62ae3bc0..70996583 100644 --- a/html/src/test/scala/com/thoughtwors/binding/html/htmlSpec.scala +++ b/html/src/test/scala/com/thoughtwors/binding/html/htmlSpec.scala @@ -36,20 +36,4 @@ final class htmlSpec extends AsyncFreeSpec with Matchers { } } - "bug in todoapp" ignore { - val children = html"
${"text"}
" - *[Future] { - ( - for snapshot <- !Await(children.snapshots.toLazyList) - yield { - for node <- snapshot.toList - yield node.asInstanceOf[Element].outerHTML - }.mkString - ) should be( - LazyList( - "", "", "
text
" - ) - ) - } - } }