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

mdoc.internal.markdown.MarkdownCompiler.scala Maven / Gradle / Ivy

The newest version!
package mdoc.internal.markdown

import java.io.File
import java.net.URL
import java.net.URLClassLoader
import java.nio.file.Path
import java.nio.file.Paths
import scala.meta._
import scala.meta.inputs.Input
import scala.meta.inputs.Position
import scala.reflect.internal.util.AbstractFileClassLoader
import scala.reflect.internal.util.BatchSourceFile
import scala.tools.nsc.Global
import scala.tools.nsc.Settings
import scala.tools.nsc.io.AbstractFile
import scala.tools.nsc.io.VirtualDirectory
import scala.tools.nsc.reporters.StoreReporter
import mdoc.Reporter
import mdoc.document.Document
import mdoc.document._
import mdoc.internal.document.DocumentBuilder
import mdoc.internal.pos.PositionSyntax._
import mdoc.internal.pos.TokenEditDistance

object MarkdownCompiler {

  def default(): MarkdownCompiler = fromClasspath("")

  def buildDocument(
      compiler: MarkdownCompiler,
      reporter: Reporter,
      sectionInputs: List[SectionInput],
      instrumented: String,
      filename: String
  ): EvaluatedDocument = {
    // Use string builder to avoid accidental stripMargin processing
    val instrumentedInput = InstrumentedInput(filename, instrumented)
    val compileInput = Input.VirtualFile(filename, instrumented)
    val edit = TokenEditDistance.toTokenEdit(sectionInputs.map(_.source), compileInput)
    val doc = compiler.compile(compileInput, reporter, edit) match {
      case Some(loader) =>
        val cls = loader.loadClass("repl.Session")
        val doc = cls.newInstance().asInstanceOf[DocumentBuilder].$doc
        try {
          doc.build(instrumentedInput)
        } catch {
          case e: PositionedException =>
            val input = sectionInputs(e.section).input
            val pos =
              if (e.pos.isEmpty) {
                Position.Range(input, 0, 0)
              } else {
                val slice = Position.Range(
                  input,
                  e.pos.startLine,
                  e.pos.startColumn,
                  e.pos.endLine,
                  e.pos.endColumn
                )
                slice.toUnslicedPosition
              }
            reporter.error(pos, e.getCause)
            Document.empty(instrumentedInput)
        }
      case None =>
        // An empty document will render as the original markdown
        Document.empty(instrumentedInput)
    }
    EvaluatedDocument(doc, sectionInputs)
  }

  def fromClasspath(classpath: String): MarkdownCompiler = {
    val fullClasspath =
      if (classpath.isEmpty) defaultClasspath(_ => true)
      else {
        val base = defaultClasspath(_ => true)
        val runtime = defaultClasspath(path => path.toString.contains("mdoc-runtime"))
        base ++ runtime
      }
    new MarkdownCompiler(fullClasspath.syntax)
  }

  private def defaultClasspath(fn: Path => Boolean): Classpath = {
    val paths =
      getClass.getClassLoader
        .asInstanceOf[URLClassLoader]
        .getURLs
        .iterator
        .map(url => AbsolutePath(Paths.get(url.toURI)))
    Classpath(paths.toList)
  }

}

class MarkdownCompiler(
    classpath: String,
    target: AbstractFile = new VirtualDirectory("(memory)", None)
) {
  private val settings = new Settings()
  settings.deprecation.value = true // enable detailed deprecation warnings
  settings.unchecked.value = true // enable detailed unchecked warnings
  settings.outputDirs.setSingleOutput(target)
  settings.classpath.value = classpath
  lazy val sreporter = new StoreReporter
  private val global = new Global(settings, sreporter)
  private val appClasspath: Array[URL] = classpath
    .split(File.pathSeparator)
    .map(path => new File(path).toURI.toURL)
  private val appClassLoader = new URLClassLoader(
    appClasspath,
    this.getClass.getClassLoader
  )

  private def clearTarget(): Unit = target match {
    case vdir: VirtualDirectory => vdir.clear()
    case _ =>
  }

  def compile(input: Input, vreporter: Reporter, edit: TokenEditDistance): Option[ClassLoader] = {
    clearTarget()
    sreporter.reset()
    val run = new global.Run
    val label = input match {
      case Input.File(path, _) => path.toString()
      case Input.VirtualFile(path, _) => path
      case _ => "(input)"
    }
    run.compileSources(List(new BatchSourceFile(label, new String(input.chars))))
    if (!sreporter.hasErrors) {
      Some(new AbstractFileClassLoader(target, appClassLoader))
    } else {
      sreporter.infos.foreach {
        case sreporter.Info(pos, msg, severity) =>
          val mpos = edit.toOriginal(pos.point) match {
            case Left(_) =>
              Position.None
            case Right(p) => p.toUnslicedPosition
          }
          severity match {
            case sreporter.ERROR => vreporter.error(mpos, msg)
            case sreporter.INFO => vreporter.info(mpos, msg)
            case sreporter.WARNING => vreporter.warning(mpos, msg)
          }
        case _ =>
      }
      None
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy