Skip to content

Commit

Permalink
Add scala-xml implementation for eventifier
Browse files Browse the repository at this point in the history
  • Loading branch information
satabin committed Jun 5, 2022
1 parent dd3f3eb commit 19b3ba9
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 10 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ lazy val scalaXml = crossProject(JVMPlatform, JSPlatform)
.jsSettings(
scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule))
)
.dependsOn(xml)
.dependsOn(xml % "compile->compile;test->test")

lazy val cbor = crossProject(JVMPlatform, JSPlatform)
.crossType(CrossType.Full)
Expand Down
47 changes: 46 additions & 1 deletion xml/scala-xml/src/main/scala/fs2/data/xml/scalaXml/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ package fs2
package data
package xml

import dom.Builder
import dom.{Builder, Eventifier}

import cats.syntax.all._

import scala.xml._
import scala.collection.mutable.ListBuffer

package object scalaXml {

implicit object ScalaXmlBuilder extends Builder[NodeSeq] {

def makeDocument(version: Option[String],
Expand Down Expand Up @@ -67,4 +69,47 @@ package object scalaXml {
ProcInstr(target, content)

}

implicit object ScalaXmlEventifier extends Eventifier[NodeSeq] {
def eventify(node: NodeSeq): List[XmlEvent] =
node match {
case Comment(comment) => List(XmlEvent.Comment(comment))
case PCData(content) => List(XmlEvent.XmlString(content, true))
case Text(content) => List(XmlEvent.XmlString(content, false))
case EntityRef(name) => List(XmlEvent.XmlEntityRef(name))
case ProcInstr(target, content) => List(XmlEvent.XmlPI(target, content))
case e: Elem =>
val isEmpty = e.minimizeEmpty && e.child.isEmpty
val result = new ListBuffer[XmlEvent]
val name = QName(Option(e.prefix), e.label)
result += XmlEvent.StartTag(name, makeAttributes(e.attributes, Nil), isEmpty)
result ++= e.child.flatMap(eventify(_))
result += XmlEvent.EndTag(name)
result.result()
case doc: Document =>
val result = new ListBuffer[XmlEvent]
result += XmlEvent.StartDocument
result ++= doc.version.map(version => XmlEvent.XmlDecl(version, doc.encoding, doc.standAlone))
result ++= doc.children.flatMap(eventify(_))
result += XmlEvent.EndDocument
result.result()
case Group(children) => children.flatMap(eventify(_)).toList
case other => Nil
}

private def makeTexty(nodes: List[Node]): List[XmlEvent.XmlTexty] =
nodes.collect {
case Text(s) => XmlEvent.XmlString(s, false)
case EntityRef(name) => XmlEvent.XmlEntityRef(name)
}

private def makeAttributes(md: MetaData, acc: List[Attr]): List[Attr] =
md match {
case Null => acc.reverse
case PrefixedAttribute(prefix, key, value, next) =>
makeAttributes(next, Attr(QName(Option(prefix), key), makeTexty(value.toList)) :: acc)
case UnprefixedAttribute(key, value, next) =>
makeAttributes(next, Attr(QName(key), makeTexty(value.toList)) :: acc)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2022 Lucas Satabin
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fs2
package data
package xml
package scalaXml

import dom._

import scala.xml.NodeSeq

object ScalaXmlEventifierSpec extends EventifierSpec[NodeSeq]
4 changes: 1 addition & 3 deletions xml/src/main/scala/fs2/data/xml/dom/Eventifier.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ package data
package xml
package dom

import cats.data.NonEmptyList

trait Eventifier[Node] {

def eventify(node: Node): NonEmptyList[XmlEvent]
def eventify(node: Node): List[XmlEvent]

}
8 changes: 4 additions & 4 deletions xml/src/main/scala/fs2/data/xml/dom/TreeParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,9 @@ class TreeParser[F[_], Node](implicit F: RaiseThrowable[F], builder: Builder[Nod
Pull.raiseError(new XmlTreeException(s"unexpected event '$evt'"))
}

private def parseDocument(chunk: Chunk[XmlEvent],
idx: Int,
rest: Stream[F, XmlEvent]): Pull[F, Node, (Chunk[XmlEvent], Int, Stream[F, XmlEvent])] =
private def document(chunk: Chunk[XmlEvent],
idx: Int,
rest: Stream[F, XmlEvent]): Pull[F, Node, (Chunk[XmlEvent], Int, Stream[F, XmlEvent])] =
next(chunk, idx, rest).flatMap {
case (XmlEvent.StartDocument, chunk, idx, rest) =>
for {
Expand All @@ -140,7 +140,7 @@ class TreeParser[F[_], Node](implicit F: RaiseThrowable[F], builder: Builder[Nod
case None => Pull.done
}
} else {
parseDocument(chunk, idx, rest).flatMap { case (chunk, idx, rest) => go(chunk, idx, rest) }
document(chunk, idx, rest).flatMap { case (chunk, idx, rest) => go(chunk, idx, rest) }
}
s => go(Chunk.empty, 0, s).stream
}
Expand Down
2 changes: 1 addition & 1 deletion xml/src/main/scala/fs2/data/xml/dom/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ package object dom {
/** Transforms a stream of XML nodes into a stream of XML events.
*/
def eventify[F[_], Node](implicit eventifier: Eventifier[Node]): Pipe[F, Node, XmlEvent] =
_.flatMap(node => Stream.emits(eventifier.eventify(node).toList))
_.flatMap(node => Stream.emits(eventifier.eventify(node)))

}
43 changes: 43 additions & 0 deletions xml/src/test/scala/fs2/data/xml/dom/EventifierSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2022 Lucas Satabin
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fs2
package data
package xml
package dom

import weaver._

abstract class EventifierSpec[Node](implicit builder: Builder[Node], eventifier: Eventifier[Node])
extends SimpleIOSuite {

pureTest("`eventifier` and `documents` should work well together") {

val input = Stream.emits("""<?xml version="1.1" encoding="utf-8"?>
|<a att1="value1" att2="&amp; another one">
| <!-- a comment -->
| <b>Test</b>
| <b/>
|</a>""".stripMargin)

val evts = input.through(events[Fallible, Char]())

val roundtrip = evts.through(documents).through(eventify)

expect(evts.compile.toList == roundtrip.compile.toList)

}

}

0 comments on commit 19b3ba9

Please sign in to comment.