Skip to content
This repository has been archived by the owner on Apr 13, 2021. It is now read-only.

[WIP] Support embedded images #233

Open
wants to merge 1 commit into
base: series/0.6.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion modules/core/src/main/scala/tut/AnsiFilterStream.scala
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,3 @@ class AnsiFilterStream(os: OutputStream) extends FilterOutputStream(os) {
}

}

11 changes: 6 additions & 5 deletions modules/core/src/main/scala/tut/FileIO.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,19 @@ object FileIO extends IMainPlatform /* scala version-specific */ {
* @return Ending state of tut after processing
*/
def tut(in: File, out: File, opts: List[String]): IO[TutState] =
IO(new FileOutputStream(out)).using { outputStream =>
IO(new AnsiFilterStream(outputStream)).using { filterStream =>
IO(new Spigot(filterStream)).using { filterSpigot =>
IO(new FileOutputStream(out)).using { outputstream =>
IO(new ImageFilterStream(outputstream)).using { imageFilterStream =>
IO(new AnsiFilterStream(imageFilterStream)).using { ansiFilterStream =>
IO(new Spigot(ansiFilterStream)).using { filterSpigot =>
IO(new PrintStream(filterSpigot, true, Encoding)).using { printStream =>
IO(new OutputStreamWriter(printStream, Encoding)).using { streamWriter =>
IO(new PrintWriter(streamWriter)).using { printWriter =>
(for {
interp <- newInterpreter(printWriter, iMainSettings(opts))
state = TutState(false, Set(), false, interp, printWriter, filterSpigot, "", false, in, opts)
state = TutState(false, Set(), false, interp, imageFilterStream, printWriter, filterSpigot, "", false, in, opts)
endSt <- Tut.file(in).exec(state)
} yield endSt).withOut(printStream)
}}}}}}
}}}}}}}

private[tut] def ls(dir: File): IO[List[File]] =
IO(Option(dir.listFiles).fold(List.empty[File])(_.toList))
Expand Down
146 changes: 146 additions & 0 deletions modules/core/src/main/scala/tut/ImageFilterStream.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package tut

import java.io.{OutputStream, FilterOutputStream}
import java.nio.charset.StandardCharsets
import java.util.Base64

class ImageFilterStream(os: OutputStream) extends FilterOutputStream(os) {
import ImageFilterStream._

val F: State = State(_ => F)

val T_B64: State = State(_ => T_B64)
val T_URL: State = State(_ => T_URL)

val S: State = State {
case 27 => I0
case _ => F
}

val I0: State = State {
case ']' => I1
case _ => F
}

val I1: State = State {
case '1' => I2
case _ => F
}

val I2: State = State {
case '3' => I3
case _ => F
}

val I3: State = State {
case '3' => I4
case _ => F
}

val I4: State = State {
case '7' => I_B64
case '8' => I_URL
case _ => F
}

val I_B64: State = State {
case '\u0007' => T_B64
case _ => I_B64
}

val I_URL: State = State {
case '\u0007' => T_URL
case _ => I_URL
}

def get(n: Int): Option[Entry] =
unsafeEntries.drop(n).headOption

def length: Int = unsafeEntries.length

private var unsafeEntries: List[Entry] = Nil
private var stack: List[Int] = Nil
private var state: State = S // Start

private def decodeDataString(input: List[Int]): String =
new String(
stack.reverse.drop(6).map(_.toByte).toArray,
StandardCharsets.UTF_8)

private def parseKeyValuePairs(input: String): (Map[String, String], Option[String]) = {
val index = input.lastIndexOf(':')
val (input0, extra) = if (index > 0) {
val (prefix, suffix) = input.splitAt(index)
prefix -> Some(suffix.drop(1))
} else input -> None

input0.split(";")
.flatMap(_.split("=", 2).toList match {
case head :: tail :: Nil => Some(head -> tail)
case _ => None
})
.toMap -> extra
}

override def write(n: Int): Unit =
state.apply(n) match {

case F =>
stack.foldRight(())((c, _) => super.write(c))
super.write(n)
stack = Nil
state = S

case T_URL =>
val (map, extra) = parseKeyValuePairs(decodeDataString(stack))
val maybeEntry = map.get("url").map(url =>
Entry.URL(url, map.get("alt")))
unsafeEntries = maybeEntry.toList ::: unsafeEntries
stack.foldRight(())((c, _) => super.write(c))
super.write(n)
stack = Nil
state = S

case T_B64 =>
val (map, extra) = parseKeyValuePairs(decodeDataString(stack))
val maybeEntry =
for {
_ <- map.get("File")
data <- extra
} yield Entry.Base64(data, map.get("alt"))
unsafeEntries = maybeEntry.toList ::: unsafeEntries
stack.foldRight(())((c, _) => super.write(c))
super.write(n)
stack = Nil
state = S

case s =>
stack = n :: stack
state = s

}

}

object ImageFilterStream {

private lazy val decoder: Base64.Decoder = Base64.getDecoder

final case class State(apply: Int => State)

sealed trait Entry
object Entry {
final case class URL(url: String, alt: Option[String]) extends Entry
final case class Base64(data: String, alt: Option[String]) extends Entry {
lazy val decoded: Array[Byte] = decoder.decode(data)

lazy val fileExtension: String = decoded.take(8).toList match {
case List(0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A) => "png"
case List(0xFF, 0xD8, 0xFF, 0xDB | 0xE0 | 0xE1, _, _, _, _) => "jpg"
case List(0x47, 0x49, 0x46, 0x38, 0x37 | 0x39, 0x61, _, _) => "gif"
case List(0x42, 0x4D, _, _, _, _, _, _) => "bmp"
case _ => "img" // ¯\_(ツ)_/¯
}
}
}
}
19 changes: 18 additions & 1 deletion modules/core/src/main/scala/tut/Tut.scala
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,26 @@ object Tut {
private def out(text: String): Tut[Unit] =
for {
s <- Tut.state
_ <- (s.mods(Invisible) || s.mods(Evaluated) || s.mods(Passthrough)).unlessM(IO { s.pw.println(text); s.pw.flush() }.liftIO[Tut])
_ <- (s.mods(Invisible) || s.mods(Evaluated) || s.mods(Passthrough)).unlessM(IO { s.pw.println(unsafeImagery(text, s)); s.pw.flush() }.liftIO[Tut])
} yield ()

val TutImageTag = raw"!\[([^\]]+)\]\(tut\:(\d+)\)".r
import ImageFilterStream.Entry

private def unsafeImagery(text: String, s: TutState): String = {
TutImageTag.replaceAllIn(text, _ match {
case TutImageTag(alt, indexStr) =>
val res = s.ifs.get(indexStr.toInt).map(_ match {
case Entry.URL(urlStr, maybeAlt) =>
s"![${maybeAlt getOrElse alt}]($urlStr)"
case entry @ Entry.Base64(dataStr, maybeAlt) =>
val lol = "it's a " + entry.fileExtension
s"![${maybeAlt getOrElse alt}]($lol)"
})
res getOrElse s"<<tut error: image $indexStr does not exist on the image stack>>"
})
}

private def success: Tut[Unit] =
for {
s <- Tut.state
Expand Down
1 change: 1 addition & 0 deletions modules/core/src/main/scala/tut/TutState.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ final case class TutState(
mods: Set[Modifier],
needsNL: Boolean,
imain: IMain,
ifs: ImageFilterStream,
pw: PrintWriter,
spigot: Spigot,
partial: String,
Expand Down
15 changes: 15 additions & 0 deletions modules/tests/src/sbt-test/tut/test-15-images/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
enablePlugins(TutPlugin)

scalaVersion := sys.props("scala.version")

resolvers += Resolver.bintrayRepo("cibotech", "public")
libraryDependencies += "com.cibo" %% "evilplot" % "0.4.0"

lazy val check = TaskKey[Unit]("check")

check := {
val expected = IO.readLines(file("expect.md"))
val actual = IO.readLines(crossTarget.value / "tut"/ "test.md")
if (expected != actual)
sys.error("Output doesn't match expected: \n" + actual.mkString("\n"))
}
1 change: 1 addition & 0 deletions modules/tests/src/sbt-test/tut/test-15-images/expect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Images
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("org.tpolecat" % "tut-plugin" % sys.props("project.version"))
28 changes: 28 additions & 0 deletions modules/tests/src/sbt-test/tut/test-15-images/src/main/tut/test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Images

```tut
println("\033]1338;url=https://media.giphy.com/media/10RhccNxPSaglW/giphy.gif;alt=keycat\u0007")
```

```tut
import com.cibo.evilplot.plot._
import com.cibo.evilplot.plot.aesthetics.DefaultTheme._
import com.cibo.evilplot.numeric.Point
import java.io.File

val data = Seq.tabulate(100) { i =>
Point(i.toDouble, scala.util.Random.nextDouble())
}
val bufferedImage = ScatterPlot(data).render().asBufferedImage
```

```tut:invisible
val out = java.util.Base64.getEncoder().wrap(Console.out)
print("\033]1337;File=blah;foo=whatever:")
javax.imageio.ImageIO.write(bufferedImage, "png", out)
print("\u0007")
```

wahoo ![wahoo](tut:1)

plot goes here ![lol](tut:0), hopefully
2 changes: 2 additions & 0 deletions modules/tests/src/sbt-test/tut/test-15-images/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
> tut
> check