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

replpp.shaded.mainargs.Renderer.scala Maven / Gradle / Ivy

The newest version!
package replpp.shaded.mainargs

import java.io.{PrintWriter, StringWriter}
import scala.math

object Renderer {

  def getLeftColWidth(items: Seq[ArgSig]) = {
    if (items.isEmpty) 0
    else items.map(renderArgShort(_).length).max
  }

  val newLine = System.lineSeparator()

  def normalizeNewlines(s: String) = s.replace("\r", "").replace("\n", newLine)

  def renderArgShort(arg: ArgSig) = arg.reader match {
    case r: TokensReader.Flag =>
      val shortPrefix = arg.shortName.map(c => s"-$c")
      val nameSuffix = arg.name.map(s => s"--$s")
      (shortPrefix ++ nameSuffix).mkString(" ")

    case r: TokensReader.Simple[_] =>
      val shortPrefix = arg.shortName.map(c => s"-$c")
      val typeSuffix = s"<${r.shortName}>"
      val nameSuffix = if (arg.positional) arg.name else arg.name.map(s => s"--$s")
      (shortPrefix ++ nameSuffix ++ Seq(typeSuffix)).mkString(" ")

    case r: TokensReader.Leftover[_, _] =>
      s"${arg.name.get} <${r.shortName}>..."
  }

  /**
   * Returns a `Some[string]` with the sortable string or a `None` if it is an leftover.
   */
  private def sortableName(arg: ArgSig): Option[String] = arg match {
    case arg: ArgSig if arg.reader.isLeftover => None

    case a: ArgSig =>
      a.shortName.map(_.toString).orElse(a.name).orElse(Some(""))
    case a: ArgSig =>
      a.name.orElse(Some(""))
  }

  object ArgOrd extends math.Ordering[ArgSig] {
    override def compare(x: ArgSig, y: ArgSig): Int =
      (sortableName(x), sortableName(y)) match {
        case (None, None) => 0 // don't sort leftovers
        case (None, Some(_)) => 1 // keep left overs at the end
        case (Some(_), None) => -1 // keep left overs at the end
        case (Some(l), Some(r)) => l.compare(r)
      }
  }

  def renderArg(
      arg: ArgSig,
      leftOffset: Int,
      wrappedWidth: Int
  ): (String, String) = {
    val wrapped = softWrap(arg.doc.getOrElse(""), leftOffset, wrappedWidth - leftOffset)
    (renderArgShort(arg), wrapped)
  }

  def formatMainMethods(
      mainMethods: Seq[MainData[_, _]],
      totalWidth: Int,
      docsOnNewLine: Boolean,
      customNames: Map[String, String],
      customDocs: Map[String, String],
      sorted: Boolean
  ): String = {
    val flattenedAll: Seq[ArgSig] =
      mainMethods.map(_.flattenedArgSigs)
        .flatten
        .map(_._1)

    val leftColWidth = getLeftColWidth(flattenedAll)
    mainMethods match {
      case Seq() => ""
      case Seq(main) =>
        Renderer.formatMainMethodSignature(
          main,
          0,
          totalWidth,
          leftColWidth,
          docsOnNewLine,
          customNames.get(main.name),
          customDocs.get(main.name),
          sorted
        )
      case _ =>
        val methods =
          for (main <- mainMethods)
            yield formatMainMethodSignature(
              main,
              2,
              totalWidth,
              leftColWidth,
              docsOnNewLine,
              customNames.get(main.name),
              customDocs.get(main.name),
              sorted
            )

        normalizeNewlines(
          s"""Available subcommands:
             |
             |${methods.mkString(newLine)}""".stripMargin
        )
    }
  }

  @deprecated("Use other overload instead", "mainargs after 0.3.0")
  def formatMainMethods(
      mainMethods: Seq[MainData[_, _]],
      totalWidth: Int,
      docsOnNewLine: Boolean,
      customNames: Map[String, String],
      customDocs: Map[String, String]
  ): String = formatMainMethods(
    mainMethods,
    totalWidth,
    docsOnNewLine,
    customNames,
    customDocs,
    sorted = true
  )

  def formatMainMethodSignature(
      main: MainData[_, _],
      leftIndent: Int,
      totalWidth: Int,
      leftColWidth: Int,
      docsOnNewLine: Boolean,
      customName: Option[String],
      customDoc: Option[String],
      sorted: Boolean
  ): String = {

    val argLeftCol = if (docsOnNewLine) leftIndent + 8 else leftColWidth + leftIndent + 2 + 2

    val sortedArgs =
      if (sorted) main.renderedArgSigs.sorted(ArgOrd)
      else main.renderedArgSigs

    val args = sortedArgs.map(renderArg(_, argLeftCol, totalWidth))

    val leftIndentStr = " " * leftIndent

    def formatArg(lhs: String, rhs: String) = {
      val lhsPadded = lhs.padTo(leftColWidth, ' ')
      val rhsPadded = rhs.linesIterator.mkString(newLine)
      if (rhs.isEmpty) s"$leftIndentStr  $lhs"
      else if (docsOnNewLine) {
        s"$leftIndentStr  $lhs\n$leftIndentStr        $rhsPadded"
      } else {
        s"$leftIndentStr  $lhsPadded  $rhsPadded"
      }
    }
    val argStrings = for ((lhs, rhs) <- args) yield formatArg(lhs, rhs)

    val mainDocSuffix = customDoc.orElse(main.doc) match {
      case Some(d) => newLine + leftIndentStr + softWrap(d, leftIndent, totalWidth)
      case None => ""
    }
    s"""$leftIndentStr${customName.getOrElse(main.name)}$mainDocSuffix
       |${argStrings.map(_ + newLine).mkString}""".stripMargin
  }

  @deprecated("Use other overload instead", "mainargs after 0.3.0")
  def formatMainMethodSignature(
      main: MainData[_, _],
      leftIndent: Int,
      totalWidth: Int,
      leftColWidth: Int,
      docsOnNewLine: Boolean,
      customName: Option[String],
      customDoc: Option[String]
  ): String = formatMainMethodSignature(
    main,
    leftIndent,
    totalWidth,
    leftColWidth,
    docsOnNewLine,
    customName,
    customDoc,
    sorted = true
  )

  def softWrap(s: String, leftOffset: Int, maxWidth: Int) = {
    if (s.isEmpty) s
    else {
      val oneLine = s.linesIterator.mkString(" ").split(' ').filter(_.nonEmpty)

      lazy val indent = " " * leftOffset

      val output = new StringBuilder(oneLine.head)
      var currentLineWidth = oneLine.head.length
      for (chunk <- oneLine.tail) {
        val addedWidth = currentLineWidth + chunk.length + 1
        if (addedWidth > maxWidth) {
          output.append(newLine + indent)
          output.append(chunk)
          currentLineWidth = chunk.length
        } else {
          currentLineWidth = addedWidth
          output.append(' ')
          output.append(chunk)
        }
      }
      output.mkString
    }
  }

  def pluralize(s: String, n: Int) = if (n == 1) s else s + "s"
  def renderEarlyError(result: Result.Failure.Early) = result match {
    case Result.Failure.Early.NoMainMethodsDetected() =>
      "No @main methods declared"
    case Result.Failure.Early.SubcommandNotSpecified(options) =>
      "Need to specify a sub command: " + options.mkString(", ")
    case Result.Failure.Early.UnableToFindSubcommand(options, token) =>
      s"Unable to find subcommand: $token, available subcommands: ${options.mkString(", ")}"
    case Result.Failure.Early.SubcommandSelectionDashes(token) =>
      "To select a subcommand to run, you don't need --s." + Renderer.newLine +
        s"Did you mean `${token.drop(2)}` instead of `$token`?"
  }

  def renderResult(
      main: MainData[_, _],
      result: Result.Failure,
      totalWidth: Int,
      printHelpOnError: Boolean,
      docsOnNewLine: Boolean,
      customName: Option[String],
      customDoc: Option[String],
      sorted: Boolean
  ): String = {

    def expectedMsg() = {
      if (printHelpOnError) {
        val leftColWidth = getLeftColWidth(main.renderedArgSigs)
        "Expected Signature: " +
          Renderer.formatMainMethodSignature(
            main,
            0,
            totalWidth,
            leftColWidth,
            docsOnNewLine,
            customName,
            customDoc,
            sorted
          )
      } else ""
    }
    result match {
      case err: Result.Failure.Early => renderEarlyError(err)
      case Result.Failure.Exception(t) =>
        val s = new StringWriter()
        val ps = new PrintWriter(s)
        t.printStackTrace(ps)
        ps.close()
        s.toString
      case Result.Failure.MismatchedArguments(missing, unknown, duplicate, incomplete) =>
        val missingStr =
          if (missing.isEmpty) ""
          else {
            val chunks = missing.map(renderArgShort(_))

            val argumentsStr = pluralize("argument", chunks.length)
            s"Missing $argumentsStr: ${chunks.mkString(" ")}" + Renderer.newLine
          }

        val unknownStr =
          if (unknown.isEmpty) ""
          else {
            val argumentsStr = pluralize("argument", unknown.length)
            s"Unknown $argumentsStr: " + unknown.map(Util.literalize(_)).mkString(
              " "
            ) + Renderer.newLine
          }

        val duplicateStr =
          if (duplicate.isEmpty) ""
          else {
            val lines =
              for ((sig, options) <- duplicate)
                yield {
                  s"Duplicate arguments for ${renderArgShort(sig)}: " +
                    options.map(Util.literalize(_)).mkString(" ") + Renderer.newLine
                }

            lines.mkString

          }
        val incompleteStr = incomplete match {
          case None => ""
          case Some(sig) =>
            s"Incomplete argument ${renderArgShort(sig)} is missing a corresponding value" +
              Renderer.newLine

        }

        Renderer.normalizeNewlines(
          s"""$missingStr$unknownStr$duplicateStr$incompleteStr${expectedMsg()}
             |""".stripMargin
        )

      case Result.Failure.InvalidArguments(x) =>
        val thingies = x.map {
          case Result.ParamError.Failed(p, vs, errMsg) =>
            val literalV = vs.map(Util.literalize(_)).mkString(" ")
            s"Invalid argument ${renderArgShort(p)} failed to parse $literalV due to $errMsg"
          case Result.ParamError.Exception(p, vs, ex) =>
            val literalV = vs.map(Util.literalize(_)).mkString(" ")
            s"Invalid argument ${renderArgShort(p)} failed to parse $literalV due to $ex"
          case Result.ParamError.DefaultFailed(p, ex) =>
            s"Invalid argument ${renderArgShort(p)}'s default value failed to evaluate with $ex"
        }

        Renderer.normalizeNewlines(
          s"""${thingies.mkString(Renderer.newLine)}
             |${expectedMsg()}
          """.stripMargin
        )
    }
  }

  @deprecated("Use other overload instead", "mainargs after 0.3.0")
  def renderResult(
      main: MainData[_, _],
      result: Result.Failure,
      totalWidth: Int,
      printHelpOnError: Boolean,
      docsOnNewLine: Boolean,
      customName: Option[String],
      customDoc: Option[String]
  ): String = renderResult(
    main,
    result,
    totalWidth,
    printHelpOnError,
    docsOnNewLine,
    customName,
    customDoc,
    sorted = true
  )
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy