diff --git a/modules/core/src/main/scala/tut/Tut.scala b/modules/core/src/main/scala/tut/Tut.scala index 079fd3d..dd75b98 100644 --- a/modules/core/src/main/scala/tut/Tut.scala +++ b/modules/core/src/main/scala/tut/Tut.scala @@ -14,12 +14,16 @@ object Tut { def file(in: File): Tut[Unit] = for { lines <- FileIO.lines(in).liftIO[Tut] - _ <- lines.zipWithIndex.traverse { case (l, num) => line(l, num + 1) } + _ <- lines.zipWithIndexAndNext(false)(nextCloses) + .traverse { case (l, nextCloses, num) => line(l, num + 1, nextCloses) } } yield () // Private, utility methods - private def line(text: String, lineNumber: Int): Tut[Unit] = + private val nextCloses: String => Boolean = + _.trim.startsWith("```") + + private def line(text: String, lineNumber: Int, nextCloses: Boolean): Tut[Unit] = for { s <- Tut.state inv = s.mods.filter(m => m == Invisible || m == Passthrough || m.isInstanceOf[Decorate]) @@ -35,11 +39,11 @@ object Tut { s } }.liftIO[Tut] - _ <- s.isCode.fold(interp(text, lineNumber), out(fixShed(text, mods ++ inv))) + _ <- s.isCode.fold(interp(text, lineNumber, nextCloses), out(fixShed(text, mods ++ inv))) _ <- checkBoundary(text, "```tut", true, mods) } yield () - private def interp(text: String, lineNum: Int): Tut[Unit] = + private def interp(text: String, lineNum: Int, nextCloses: Boolean): Tut[Unit] = Tut.state >>= { s => (text.trim.nonEmpty || s.partial.nonEmpty || s.mods(Silent)).whenM[Tut,Unit] { for { @@ -50,6 +54,7 @@ object Tut { r <- // in 2.13 it's an error to interpret an empty line of text, evidently if (text.trim.isEmpty && s.partial.isEmpty) success else IO(s.imain.interpret(s.partial + "\n" + text)).liftIO[Tut] >>= { + case Results.Incomplete if nextCloses => error(lineNum, Some("incomplete input in code block, missing brace or paren?")) case Results.Incomplete => incomplete(text) case Results.Success => if (s.mods(Fail)) error(lineNum, Some("failure was asserted but no failure occurred")) else success case Results.Error => if (s.mods(NoFail) || s.mods(Fail)) success else error(lineNum) diff --git a/modules/core/src/main/scala/tut/felix/Syntax.scala b/modules/core/src/main/scala/tut/felix/Syntax.scala index b24d8b7..f9b5235 100644 --- a/modules/core/src/main/scala/tut/felix/Syntax.scala +++ b/modules/core/src/main/scala/tut/felix/Syntax.scala @@ -2,6 +2,8 @@ package tut.felix import java.io.OutputStream +import scala.annotation.tailrec + trait Syntax { implicit class MonadOps[M[_], A](ma: M[A])(implicit M: Monad[M]) { @@ -35,6 +37,18 @@ trait Syntax { implicit class ListOps[A](as: List[A]) { def traverse[M[_]: Monad, B](f: A => M[B]): M[List[B]] = as.foldRight(List.empty[B].point[M])((a, mlb) => f(a).flatMap(b => mlb.map(b :: _))) + + def zipWithIndexAndNext[B](z: => B)(f: A => B): List[(A, B, Int)] = { + @tailrec + def fold(xs: List[(A, Int)], acc: Vector[(A, B, Int)]): Vector[(A, B, Int)] = + xs match { + case (x, i) :: (xs @ ((y, _) :: _)) => fold(xs, acc :+ ((x, f(y), i))) + case (x, i) :: Nil => acc :+ ((x, z, i)) + case Nil => Vector.empty + } + + fold(as.zipWithIndex, Vector.empty).toList + } } implicit class BooleanOps(b: Boolean) { diff --git a/modules/tests/src/sbt-test/tut/test-14-unclosed-expr/build.sbt b/modules/tests/src/sbt-test/tut/test-14-unclosed-expr/build.sbt new file mode 100644 index 0000000..784c014 --- /dev/null +++ b/modules/tests/src/sbt-test/tut/test-14-unclosed-expr/build.sbt @@ -0,0 +1,5 @@ +enablePlugins(TutPlugin) + +scalaVersion := sys.props("scala.version") + +scalacOptions += "-language:higherKinds" diff --git a/modules/tests/src/sbt-test/tut/test-14-unclosed-expr/project/plugins.sbt b/modules/tests/src/sbt-test/tut/test-14-unclosed-expr/project/plugins.sbt new file mode 100644 index 0000000..bbe56cd --- /dev/null +++ b/modules/tests/src/sbt-test/tut/test-14-unclosed-expr/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("org.tpolecat" % "tut-plugin" % sys.props("project.version")) diff --git a/modules/tests/src/sbt-test/tut/test-14-unclosed-expr/src/main/tut/test.md b/modules/tests/src/sbt-test/tut/test-14-unclosed-expr/src/main/tut/test.md new file mode 100644 index 0000000..61353ae --- /dev/null +++ b/modules/tests/src/sbt-test/tut/test-14-unclosed-expr/src/main/tut/test.md @@ -0,0 +1,23 @@ +Ok + +```tut +1 + 2 +``` + +Open paren + +```tut +println( +``` + +Open brace + +```tut +1 match { +``` + +Incomplete class def + +```tut +class Foo { +``` diff --git a/modules/tests/src/sbt-test/tut/test-14-unclosed-expr/test b/modules/tests/src/sbt-test/tut/test-14-unclosed-expr/test new file mode 100644 index 0000000..45d43cf --- /dev/null +++ b/modules/tests/src/sbt-test/tut/test-14-unclosed-expr/test @@ -0,0 +1 @@ +-> tut