All Downloads are FREE. Search and download functionalities are using the official Maven repository.

kantan.repl.md.markdown.scala Maven / Gradle / Ivy

There is a newer version: 1.2.0
Show newest version
/*
 * Copyright 2021 Nicolas Rinaudo
 *
 * 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 kantan.repl.md.markdown

import java.io.{File, FileOutputStream, OutputStreamWriter, StringWriter, Writer}
import scala.io.Source

/** One block in a markdown file.
  *
  * This is hugely simplified, because we really only care whether a block needs to go through the REPL or not. As a
  * result, we only have two kinds of blocks: `Repl` (must be processed) or `Other` (must not be processed).
  */
enum Block {
  case Repl(value: String, modifier: Modifier, startAt: Int)
  case Other(value: String)
}

enum Modifier {
  case Fail
  case Reset
  case Invisible
  case Print
  case Silent
}

private def isReplBlockStart(line: String) = line.startsWith("```scala repl")

private def isReplBlockEnd(line: String) = line.trim() == "```"

private def extractModifier(line: String, lineNumber: Int) = {
  val index = line.indexOf(':')

  // No modifier, simple print
  if index < 0 then Modifier.Print
  else
    line.splitAt(index + 1)(1) match {
      case "reset"      => Modifier.Reset
      case "print" | "" => Modifier.Print
      case "invisible"  => Modifier.Invisible
      case "silent"     => Modifier.Silent
      case "fail"       => Modifier.Fail
      case modifier =>
        println(s"Unexpected modifier line $lineNumber: $modifier. Defaulting to normal printing.")
        Modifier.Print
    }
}

def load(source: Source): List[Block] =
  load(source.getLines)

/** Parses the specified markdown file as a list of blocks.
  *
  * Note that saying this supports markdown is a bit of a stretch. We're simply looking for triple-backtick enclosed
  * blocks with the expected label and ignoring everything else.
  *
  * This is not meant to be pretty or even very stable: the point is to have something that replaces tut and works with
  * Scala 3 up and running quickly. All of this code will be deprecated in a hurry.
  */
def load(lines: Iterator[String]): List[Block] = {

  // Current parser state: either in a REPL block, or not.
  enum State {
    case Repl(modifier: Modifier, line: Int)
    case Other
  }

  val blocks = List.newBuilder[Block]
  val block  = new StringBuilder()
  var state  = State.Other

  // Returns the content of the current block.
  // Note that we're trying to generate a canonical AST, so this removes all unnecessary whitespace.
  def currentBlock() = {
    val res = block.result()
    block.setLength(0)
    res.trim()
  }

  def appendRepl(content: String, modifier: Modifier, startAt: Int) =
    blocks += Block.Repl(content, modifier, startAt)

  def appendOther(content: String) =
    if content.nonEmpty then blocks += Block.Other(content)

  lines.zipWithIndex.foreach { case (line, number) =>
    state match {
      case State.Repl(modifier, startAt) if isReplBlockEnd(line) =>
        state = State.Other
        appendRepl(currentBlock(), modifier, startAt)

      case State.Other if isReplBlockStart(line) =>
        state = State.Repl(extractModifier(line, number), number)
        appendOther(currentBlock())

      case _ =>
        block ++= line
        block  += '\n'
    }
  }

  // Empties whatever might remain from the last block. Note that this is a bit lenient: if the last block is
  // a REPL block, and it's not properly closed, we'll ignore it silently.
  if block.nonEmpty then
    state match {
      case State.Repl(modifier, startAt) => appendRepl(currentBlock(), modifier, startAt)
      case State.Other                   => appendOther(currentBlock())
    }

  blocks.result()
}

def toString(blocks: List[Block]): String = {
  val out = new StringWriter

  print(blocks, out)

  out.toString
}

def print(blocks: List[Block], file: File): Unit = {
  val out = new OutputStreamWriter(new FileOutputStream(file), "UTF-8")
  print(blocks, out)
  out.close()
}

def print(blocks: List[Block], out: Writer): Unit = {
  def nonEmpty(block: Block) = block match {
    case Block.Other(content) => content.trim.nonEmpty
    case _                    => true
  }

  def printBlock(block: Block) = block match {
    case Block.Other(value) =>
      out.write(value.trim())
      out.write("\n")

    case Block.Repl(value, modifier, _) =>
      out.write("```scala repl")
      modifier match {
        case Modifier.Reset     => out.write(":reset")
        case Modifier.Silent    => out.write(":silent")
        case Modifier.Fail      => out.write(":fail")
        case Modifier.Invisible => out.write(":invisible")
        case Modifier.Print     =>
      }
      out.write('\n')
      out.write(value.trim())
      out.write("\n```\n")
  }

  var isFirst = true
  blocks.filter(nonEmpty).foreach { block =>
    if isFirst then isFirst = false
    else out.write("\n")
    printBlock(block)
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy