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

com.twitter.scrooge.mustache.MustacheParser.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2011 Twitter, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may obtain
 * a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.twitter.scrooge.mustache

import scala.util.parsing.combinator._
import com.twitter.scrooge.frontend.ParseException

object MustacheAST {
  case class Template(segments: Seq[Segment])
  sealed trait Segment
  case class Data(data: String) extends Segment
  case class Interpolation(name: String) extends Segment
  case class Section(name: String, document: Template, reversed: Boolean, joiner: Option[String] = None) extends Segment
  case class Partial(name: String) extends Segment
}

class ParserPool(size: Int = 10) {
  private[this] val queue = new java.util.concurrent.LinkedBlockingQueue[MustacheParser]()
  (0 until size) foreach { _ => queue.offer(new MustacheParser) }

  def apply[T](f: MustacheParser => T) = {
    val parser = queue.take()
    try {
      f(parser)
    } finally {
      queue.offer(parser)
    }
  }
}

object MustacheParser {
  private[this] val pool = new ParserPool
  def apply(in: String): MustacheAST.Template =
    pool { _(in) }
}

class MustacheParser extends RegexParsers {
  import MustacheAST._

  override def skipWhitespace = false

  def document: Parser[Template] = rep(directive | data) ^^ { x => Template(x.flatten) }

  def directive: Parser[Option[Segment]] = interpolation | section | comment | partial

  def interpolation = "{{" ~> id <~ "}}" ^^ { x => Some(Interpolation(x)) }

  def startSection = "{{" ~> """#|\^""".r ~ id <~ "}}" ^^ { case prefix ~ id =>
    (prefix == "^", id)
  }

  def endSection = "{{/" ~> id ~ opt("|" ~> """([^}]|}(?!}))+""".r) <~ "}}"

  def section = startSection ~ document ~ endSection ^^ { case (reversed, id1) ~ doc ~ (id2 ~ joiner) =>
    if (id1 != id2) err("Expected " + id1 + ", got " + id2)
    Some(Section(id1, doc, reversed, joiner))
  }

  def comment = """\{\{!(.*?)}}""".r ^^^ None

  def partial = "{{>" ~> id <~ "}}" ^^ { x => Some(Partial(x)) }

  def data: Parser[Option[Segment]] = """([^{]+|\{(?!\{)|\{(?=\{\{))+""".r ^^ { x => Some(Data(x)) }

  def id = """[A-Za-z0-9_\.]+""".r

  def apply(in: String): Template = {
    CleanupWhitespace {
      parseAll(document, in) match {
        case Success(result, _) => result
        case x @ Failure(msg, z) => throw new ParseException(x.toString)
        case x @ Error(msg, _) => throw new ParseException(x.toString)
      }
    }
  }
}

/**
 * If a section header is on its own line, remove the trailing linefeed.
 */
object CleanupWhitespace extends (MustacheAST.Template => MustacheAST.Template) {
  import MustacheAST._

  def apply(document: Template): Template = {
    var afterSectionHeader = true
    var sectionHeaderStartedLine = true
    val segments = document.segments.map {
      case Data(data) if afterSectionHeader && sectionHeaderStartedLine && (data startsWith "\n") => {
        afterSectionHeader = false
        sectionHeaderStartedLine = (data endsWith "\n")
        Data(data.substring(1))
      }
      case x @ Section(_, _, _, _) => {
        afterSectionHeader = true
        apply(x)
      }
      case x @ Data(data) if (data endsWith "\n") => {
        afterSectionHeader = false
        sectionHeaderStartedLine = true
        x
      }
      case x => {
        afterSectionHeader = false
        sectionHeaderStartedLine = false
        apply(x)
      }
    }
    Template(segments)
  }

  def apply(segment: Segment): Segment = {
    segment match {
      case Section(name, document, reversed, joiner) =>
        Section(name, apply(document), reversed, joiner)
      case x => x
    }
  }
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy