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

caliban.parsing.parsers.StringParsers.scala Maven / Gradle / Ivy

The newest version!
package caliban.parsing.parsers

import caliban.Value.StringValue
import fastparse._

import scala.annotation.{ switch, tailrec }

private[caliban] trait StringParsers {
  implicit object whitespace extends Whitespace {

    type State = Int

    // statuses
    final val Normal                  = 0
    final val InsideLineComment       = 1
    final val DetermineLineBreakStart = 2

    final val UnicodeBOM   = '\uFEFF'
    final val Tab          = '\u0009'
    final val Space        = '\u0020'
    final val LF           = '\u000A'
    final val CR           = '\u000D'
    final val Comma        = ','
    final val CommentStart = '#'

    override def apply(p: P[_]): P[Unit] =
      loop(p.index, state = Normal)(p, p.input)

    @tailrec def loop(index: Int, state: State)(implicit ctx: P[_], input: ParserInput): ParsingRun[Unit] =
      if (input.isReachable(index)) {
        val currentChar = input(index)
        (state: @switch) match {
          case Normal                  =>
            (currentChar: @switch) match {
              case CR                                    => loop(index + 1, state = DetermineLineBreakStart)
              case Space | LF | Comma | Tab | UnicodeBOM => loop(index + 1, state = Normal)
              case CommentStart                          => loop(index + 1, state = InsideLineComment)
              case _                                     => ctx.freshSuccessUnit(index)
            }
          case InsideLineComment       =>
            loop(
              index + 1,
              state = (currentChar: @switch) match {
                case CR => DetermineLineBreakStart
                case LF => Normal
                case _  => InsideLineComment
              }
            )
          case DetermineLineBreakStart =>
            (currentChar: @switch) match {
              case LF => loop(index + 1, state = Normal)
              case _  => loop(index, state = Normal)
            }
        }
      } else ctx.freshSuccessUnit(index)
  }

  def sourceCharacter(implicit ev: P[Any]): P[Unit]                      = CharIn("\u0009\u000A\u000D\u0020-\uFFFF")
  def sourceCharacterWithoutLineTerminator(implicit ev: P[Any]): P[Unit] = CharIn("\u0009\u0020-\uFFFF")
  def name(implicit ev: P[Any]): P[String]                               =
    CharsWhileIn("_0-9A-Za-z", 1).!.flatMap { s =>
      // Less efficient in case of an error, but more efficient in case of success (happy path)
      if (s.charAt(0) <= '9') ev.freshFailure()
      else ev.freshSuccess(s)
    }
  def nameOnly(implicit ev: P[Any]): P[String]                           = Start ~ name ~ End

  def hexDigit(implicit ev: P[Any]): P[Unit]         = CharIn("0-9a-fA-F")
  def escapedUnicode(implicit ev: P[Any]): P[String] =
    (hexDigit ~~ hexDigit ~~ hexDigit ~~ hexDigit).!.map(Integer.parseInt(_, 16).toChar.toString)

  def escapedCharacter(implicit ev: P[Any]): P[String] = CharIn("\"\\\\/bfnrt").!.map {
    case "b"   => "\b"
    case "n"   => "\n"
    case "f"   => "\f"
    case "r"   => "\r"
    case "t"   => "\t"
    case other => other
  }

  def stringCharacter(implicit ev: P[Any]): P[String] =
    sourceCharacterWithoutLineTerminator.!.filter(c =>
      c != "\"" && c != "\\"
    ) | "\\u" ~~ escapedUnicode | "\\" ~~ escapedCharacter

  def blockStringCharacter(implicit ev: P[Any]): P[String] = "\\\"\"\"".!.map(_ => "\"\"\"") | sourceCharacter.!

  def stringValue(implicit ev: P[Any]): P[StringValue] =
    (
      ("\"\"\"" ~~ ((!"\"\"\"") ~~ blockStringCharacter).repX.map(s => blockStringValue(s.mkString)) ~~ "\"\"\"") |
        ("\"" ~~ stringCharacter.repX.map(_.mkString) ~~ "\"")
    ).map(v => StringValue(v))

  def blockStringValue(rawValue: String): String = {
    val l1           = rawValue.split("\r?\n").toList
    val commonIndent = l1 match {
      case Nil       => None
      case _ :: tail =>
        tail.foldLeft(Option.empty[Int]) { case (commonIndent, line) =>
          val indent = "[ \t]*".r.findPrefixOf(line).fold(0)(_.length)
          if (indent < line.length && commonIndent.fold(true)(_ > indent)) Some(indent) else commonIndent
        }
    }
    // remove indentation
    val l2           = (commonIndent, l1) match {
      case (Some(value), head :: tail) => head :: tail.map(_.drop(value))
      case _                           => l1
    }
    // NOTE: @noinline annotation required as Scala 2.12 inlines `.dropWhile` which results in compilation error since these methods are used within fastparse's macros
    // remove start lines that are only whitespaces
    val l3           = l2.dropWhile("[ \t]*".r.replaceAllIn(_, "").isEmpty): @noinline
    // remove end lines that are only whitespaces
    val l4           = (l3.reverse.dropWhile("[ \t]*".r.replaceAllIn(_, "").isEmpty): @noinline).reverse
    l4.mkString("\n")
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy