在Scala中保留字符串插值的缩进

时间:2012-07-09 22:56:18

标签: scala

我想知道在scala中进行字符串插值时是否有任何保留缩进的方法。基本上,我想知道我是否可以介入我自己的StringContext。宏将解决这个问题,但我想等到他们正式。

这就是我想要的:

val x = "line1 \nline2"
val str = s">       ${x}"

str应评估为

>       line1
        line2

4 个答案:

答案 0 :(得分:7)

回答我的问题,并将Daniel Sobral非常有用的答案转换为代码。希望它对具有相同问题的其他人有用。我没有使用隐式类,因为我仍然在2.10之前。

用法:

import Indenter._并使用字符串插值,如e" $foo "

import Indenter._

object Ex extends App {
  override def main(args: Array[String]) {
    val name = "Foo"
    val fields = "x: Int\ny:String\nz:Double"
    // fields has several lines. All of them will be indented by the same amount.
    print (e"""
        class $name {
           ${fields}
        }
        """)  
  }
}

应该打印

class Foo
   x: Int
   y: String
   z: Double

这是自定义缩进上下文。

class IndentStringContext(sc: StringContext) {
  def e(args: Any*):String = {
    val sb = new StringBuilder()
    for ((s, a) <- sc.parts zip args) {
      sb append s

      val ind = getindent(s)
      if (ind.size > 0) { 
        sb append a.toString().replaceAll("\n", "\n" + ind)
      } else {
        sb append a.toString()
      }
    }
    if (sc.parts.size > args.size)
      sb append sc.parts.last

    sb.toString()
  }

  // get white indent after the last new line, if any
  def getindent(str: String): String = {
    val lastnl = str.lastIndexOf("\n")
    if (lastnl == -1) ""
    else {
      val ind = str.substring(lastnl + 1)
      if (ind.trim.isEmpty) ind  // ind is all whitespace. Use this
      else ""
    }
  }
}

object Indenter {
  // top level implicit defs allowed only in 2.10 and above
  implicit  def toISC(sc: StringContext) = new IndentStringContext(sc)
}

答案 1 :(得分:5)

您可以编写自己的插补器,并且可以使用自己的插值器遮挡标准插值器。现在,我不知道你的例子背后的语义是什么,所以我甚至都不去尝试。

SlideshareSpeakerDeck上查看我在Scala 2.10上的演示文稿,因为它们包含了可以编写/覆盖插补器的所有方式的示例。从幻灯片40开始(现在 - 演示文稿可能会更新,直到2.10终于出来)。

答案 2 :(得分:0)

对于寻求帖子2.10的任何人:

object Interpolators {
  implicit class Regex(sc: StringContext) {
    def r = new util.matching.Regex(sc.parts.mkString, sc.parts.tail.map(_ => "x"): _*)
  }

  implicit class IndentHelper(val sc: StringContext) extends AnyVal {
    import sc._

    def process = StringContext.treatEscapes _

    def ind(args: Any*): String = {
      checkLengths(args)
      parts.zipAll(args, "", "").foldLeft("") {
        case (a, (part, arg)) =>
          val processed = process(part)

          val prefix = processed.split("\n").last match {
            case r"""([\s|]+)$d.*""" => d
            case _                   => ""
          }

          val argLn = arg.toString
            .split("\n")

          val len = argLn.length

          // Todo: Fix newline bugs
          val indented = argLn.zipWithIndex.map {
            case (s, i) =>
              val res = if (i < 1) { s } else { prefix + s }
              if (i == len - 1) { res } else { res + "\n" }
          }.mkString

          a + processed + indented
      }
    }
  }
}

答案 3 :(得分:0)

这是一个简短的解决方案。在Scastie上的完整代码和测试。那里有两个版本,一个普通的indented插值器,一个稍微复杂些的indentedWithStripMargin插值器,使它更具可读性:

assert(indentedWithStripMargin"""abc               
                                |123456${"foo\nbar"}-${"Line1\nLine2"}""" == s"""|abc
                                                                                 |123456foo
                                                                                 |      bar-Line1
                                                                                 |          Line2""".stripMargin)

这是核心功能:

  def indentedHelper(parts: List[String], args: List[String]): String = {
    // In string interpolation, there is always one more string than argument
    assert(parts.size == 1+args.size)

    (parts, args) match {
      // The simple case is where there is one part (and therefore zero args). In that case,
      // we just return the string as-is:
      case (part0 :: Nil, Nil) => part0
      // If there is more than one part, we can simply take the first two parts and the first arg,
      // merge them together into one part, and then use recursion. In other words, we rewrite
      //    indented"A ${10/10} B ${2} C ${3} D ${4} E"
      // as
      //           indented"A 1 B ${2} C ${3} D ${4} E"
      // and then we can rely on recursion to rewrite that further as:
      //              indented"A 1 B 2 C ${3} D ${4} E"
      // then:
      //                 indented"A 1 B 2 C 3 D ${4} E"
      // then:
      //                    indented"A 1 B 2 C 3 D 4 E"
      case (part0 :: part1 :: tailparts, arg0 :: tailargs) => {
        // If 'arg0' has newlines in it, we will need to insert spaces. To decide how many spaces,
        // we count many characters after after the last newline in 'part0'. If there is no
        // newline, then we just take the length of 'part0':
        val i = part0.reverse.indexOf('\n')
        val n = if (i == -1)
                  part0.size // if no newlines in part0, we just take its length
                else
                  i // the number of characters after the last newline
        // After every newline in arg0, we must insert 'n' spaces:
        val arg0WithPadding = arg0.replaceAll("\n", "\n" + " "*n)
        val mergeTwoPartsAndOneArg = part0 + arg0WithPadding + part1
        // recurse:
        indentedHelper(mergeTwoPartsAndOneArg :: tailparts, tailargs)
      }
      // The two cases above are exhaustive, but the compiler thinks otherwise, hence we need
      // to add this dummy.
      case _ => ???
    }
  }