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

mdoc.internal.cli.Settings.scala Maven / Gradle / Ivy

There is a newer version: 2.2.24
Show newest version
package mdoc.internal.cli

import java.io.InputStream
import java.nio.charset.Charset
import java.nio.charset.IllegalCharsetNameException
import java.nio.charset.StandardCharsets
import java.nio.charset.UnsupportedCharsetException
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.InvalidPathException
import java.nio.file.PathMatcher
import mdoc.OnLoadContext
import mdoc.PostModifier
import mdoc.PreModifier
import metaconfig.Conf
import metaconfig.ConfDecoder
import metaconfig.ConfEncoder
import metaconfig.ConfError
import metaconfig.Configured
import metaconfig.annotation._
import metaconfig.generic
import metaconfig.generic.Surface
import org.typelevel.paiges.Doc
import scala.annotation.StaticAnnotation
import scala.meta.internal.io.PathIO
import scala.meta.io.AbsolutePath
import scala.meta.io.RelativePath
import mdoc.StringModifier
import mdoc.Variable
import mdoc.Reporter
import mdoc.internal.markdown.{GitHubIdGenerator, MarkdownCompiler, ReplVariablePrinter}

class Section(val name: String) extends StaticAnnotation

case class Settings(
    @Section("Common options")
    @Description(
      "The input directory containing markdown and other documentation sources. " +
        "Markdown files will be processed by mdoc while other files will be copied " +
        "verbatim to the output directory."
    )
    @ExtraName("i")
    in: AbsolutePath,
    @Description("The output directory to generate the mdoc site.")
    @ExtraName("o")
    out: AbsolutePath,
    @Description("Start a file watcher and incrementally re-generate the site on file save.")
    @ExtraName("w")
    watch: Boolean = false,
    @Description(
      "Instead of generating a new site, report an error if generating the site would produce a diff " +
        "against an existing site. Useful for asserting in CI that a site is up-to-date."
    )
    @ExtraName("test")
    check: Boolean = false,
    @Description("Disable link hygiene analysis so that no warnings are reported for dead links.")
    noLinkHygiene: Boolean = false,
    @Description("Include additional diagnostics for debugging potential problems.")
    verbose: Boolean = false,
    @Description(
      "Key/value pairs of variables to replace through @VAR@. " +
        "For example, the flag '--site.VERSION 1.0.0' will replace appearances of '@VERSION@' in " +
        "markdown files with the string 1.0.0"
    )
    site: Map[String, String] = Map.empty,
    @Section("Compiler options")
    @Description(
      "Classpath to use when compiling Scala code examples. " +
        "Defaults to the current thread's classpath."
    )
    classpath: String = "",
    @Description(
      "Compiler flags such as compiler plugins '-Xplugin:kind-projector.jar' " +
        "or custom options '-deprecated'. Formatted as a single string with space separated values. " +
        "To pass multiple values: --scalac-options \"-Yrangepos -deprecated\". " +
        "Defaults to the value of 'scalacOptions' in the 'mdoc.properties' resource file, if any. " +
        "When using sbt-mdoc, update the `scalacOptions` sbt setting instead of passing --scalac-options to `mdocExtraArguments`."
    )
    scalacOptions: String = "",
    @Description("Remove all files in the output directory before generating a new site.")
    cleanTarget: Boolean = false,
    @Section("LiveReload options")
    @Description("Don't start a LiveReload server under --watch mode.")
    noLivereload: Boolean = false,
    @Description(
      "Which port the LiveReload server should listen to. " +
        "If the port is not free, another free port close to this number is used."
    )
    port: Int = 4000,
    @Description("Which hostname the LiveReload server should listen to")
    host: String = "localhost",
    @Section("Less common options")
    @Description("Print out a help message and exit")
    help: Boolean = false,
    @Description("Print out usage instructions and exit")
    usage: Boolean = false,
    @Description("Print out the version number and exit")
    version: Boolean = false,
    @Description("Set of file extensions to treat as markdown files.")
    markdownExtensions: List[String] = List("md", "html"),
    @Description(
      "Glob to filter which files to process. Defaults to all files. " +
        "Example: --include **/example.md will process only files with the name example.md."
    )
    @ExtraName("includePath")
    include: List[PathMatcher] = Nil,
    @Description(
      "Glob to filter which files from exclude from processing. Defaults to no files. " +
        "Example: --include users/**.md --exclude **/example.md will process all files in the users/ directory " +
        "excluding files named example.md."
    )
    @ExtraName("excludePath")
    exclude: List[PathMatcher] = Nil,
    @Description(
      "Use relative filenames when reporting error messages. " +
        "Useful for producing consistent docs on a local machine and CI. "
    )
    reportRelativePaths: Boolean = false,
    @Description("The encoding to use when reading and writing files.")
    charset: Charset = StandardCharsets.UTF_8,
    @Description("The working directory to use for making relative paths absolute.")
    cwd: AbsolutePath,
    @Hidden()
    stringModifiers: List[StringModifier] = StringModifier.default(),
    @Hidden()
    postModifiers: List[PostModifier] = PostModifier.default(),
    @Hidden()
    preModifiers: List[PreModifier] = PreModifier.default(),
    @Hidden()
    @Description("The input stream to listen for enter key during file watching.")
    inputStream: InputStream = System.in,
    @Hidden()
    @Description("The width of the screen, used to line wrap pretty-printed objects.")
    screenWidth: Int = 120,
    @Hidden()
    @Description("The height of the screen, used to truncate large pretty-printed objects.")
    screenHeight: Int = 50,
    @Hidden()
    @Description("The generator for header IDs, defaults to GitHub ID generator")
    headerIdGenerator: String => String = GitHubIdGenerator,
    @Hidden()
    @Description("The pretty printer for variables")
    variablePrinter: Variable => String = ReplVariablePrinter
) {

  val isMarkdownFileExtension = markdownExtensions.toSet

  def withProperties(props: MdocProperties): Settings =
    copy(
      scalacOptions = props.scalacOptions,
      classpath = props.classpath,
      site = site ++ props.site,
      in = props.in.getOrElse(in),
      out = props.out.getOrElse(out)
    )

  override def toString: String = ConfEncoder[Settings].write(this).toString()

  def isFileWatching: Boolean = watch && !check

  def toInputFile(infile: AbsolutePath): Option[InputFile] = {
    val relpath = infile.toRelative(in)
    if (isIncluded(relpath)) {
      val outfile = out.resolve(relpath)
      Some(InputFile(relpath, infile, outfile))
    } else {
      None
    }
  }
  def isExplicitlyExcluded(path: RelativePath): Boolean = {
    exclude.exists(_.matches(path.toNIO))
  }
  def isIncluded(path: RelativePath): Boolean = {
    (include.isEmpty || include.exists(_.matches(path.toNIO))) &&
    !isExplicitlyExcluded(path)
  }

  def onLoad(reporter: Reporter): Unit = {
    val ctx = new OnLoadContext(reporter, this)
    preModifiers.foreach(_.onLoad(ctx))
  }
  def validate(logger: Reporter): Configured[Context] = {
    if (Files.exists(in.toNIO)) {
      if (out.toNIO.startsWith(in.toNIO)) {
        Configured.error(Feedback.outSubdirectoryOfIn(in.toNIO, out.toNIO))
      } else {
        val compiler = MarkdownCompiler.fromClasspath(classpath, scalacOptions)
        onLoad(logger)
        if (logger.hasErrors) {
          Configured.error("Failed to load modifiers")
        } else {
          Configured.ok(Context(this, logger, compiler))
        }
      }
    } else {
      ConfError.fileDoesNotExist(in.toNIO).notOk
    }
  }
  def resolveIn(relpath: RelativePath): AbsolutePath = {
    in.resolve(relpath)
  }

  def resolveOut(relpath: RelativePath): AbsolutePath = {
    out.resolve(relpath)
  }
  def withWorkingDirectory(dir: AbsolutePath): Settings = {
    copy(
      in = dir.resolve("docs"),
      out = dir.resolve("out"),
      cwd = dir
    )
  }
}

object Settings extends MetaconfigScalametaImplicits {
  def baseDefault(cwd: AbsolutePath): Settings = {
    new Settings(
      in = cwd.resolve("docs"),
      out = cwd.resolve("out"),
      cwd = cwd
    )
  }
  def default(cwd: AbsolutePath): Settings = {
    val base = baseDefault(cwd)
    val props = MdocProperties.default(cwd)
    base.withProperties(props)
  }
  def fromCliArgs(args: List[String], base: Settings): Configured[Settings] = {
    Conf
      .parseCliArgs[Settings](args)
      .andThen(_.as[Settings](decoder(base)))
  }
  def version(displayVersion: String) =
    s"mdoc v$displayVersion"
  def usage: String =
    """|Usage:   mdoc [




© 2015 - 2024 Weber Informatics LLC | Privacy Policy