diff --git a/compiler/src/main/scala/play/twirl/compiler/TwirlCompiler.scala b/compiler/src/main/scala/play/twirl/compiler/TwirlCompiler.scala index c5fae405..84e6b21f 100644 --- a/compiler/src/main/scala/play/twirl/compiler/TwirlCompiler.scala +++ b/compiler/src/main/scala/play/twirl/compiler/TwirlCompiler.scala @@ -406,14 +406,14 @@ object TwirlCompiler { t.params.pos ) :+ ":" :+ resultType :+ " = {_display_(" :+ templateCode(t, resultType) :+ ")};" } - case Def(name, params, block) => { + case Def(name, params, resultType, block) => { Nil :+ (if (name.str.startsWith("implicit")) "implicit def " else "def ") :+ Source( name.str, name.pos ) :+ Source( params.str, params.pos - ) :+ " = {" :+ block.code :+ "};" + ) :+ resultType.map(":" + _.str).getOrElse("") :+ " = {" :+ block.code :+ "};" } } diff --git a/compiler/src/test/resources/localDef.scala.html b/compiler/src/test/resources/localDef.scala.html new file mode 100644 index 00000000..4374d525 --- /dev/null +++ b/compiler/src/test/resources/localDef.scala.html @@ -0,0 +1,19 @@ +@**************************************************************************************************************************************************** + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. * + ****************************************************************************************************************************************************@ + +@() + +@person(firstname: String, lastname: String) = @{ + s"$firstname-$lastname" +} +@country(city: String, country: String): String = @{ + s"$city-$country" +} +@region(continent: String):String=@{ + s"$continent" +} +@slogan()=@{ s"The High Velocity Web Framework For Java and Scala" } +@year=@{ s"2023" } + +@person("Play", "Framework")-@country("Vienna", "Austria")-@region("Europe")-@slogan()-@year \ No newline at end of file diff --git a/compiler/src/test/scala/play/twirl/compiler/test/CompilerSpec.scala b/compiler/src/test/scala/play/twirl/compiler/test/CompilerSpec.scala index acfb091a..b38343cd 100644 --- a/compiler/src/test/scala/play/twirl/compiler/test/CompilerSpec.scala +++ b/compiler/src/test/scala/play/twirl/compiler/test/CompilerSpec.scala @@ -300,6 +300,15 @@ class CompilerSpec extends AnyWordSpec with Matchers { hello.static("twirl", "something-else").toString.trim must include("""
""") } + "compile successfully (local definitions)" in { + val helper = newCompilerHelper + val hello = + helper.compile[(() => Html)]("localDef.scala.html", "html.localDef") + hello.static().toString.trim must be( + "Play-Framework-Vienna-Austria-Europe-The High Velocity Web Framework For Java and Scala-2023" + ) + } + "compile successfully (block with tuple)" in { val helper = newCompilerHelper val hello = helper.compile[(Seq[(String, String)] => Html)]("blockWithTuple.scala.html", "html.blockWithTuple") diff --git a/parser/src/main/scala/play/twirl/parser/TreeNodes.scala b/parser/src/main/scala/play/twirl/parser/TreeNodes.scala index 597ea4b7..22045a7c 100644 --- a/parser/src/main/scala/play/twirl/parser/TreeNodes.scala +++ b/parser/src/main/scala/play/twirl/parser/TreeNodes.scala @@ -26,12 +26,12 @@ object TreeNodes { case class PosString(str: String) extends Positional { override def toString: String = str } - case class Def(name: PosString, params: PosString, code: Simple) extends Positional - case class Plain(text: String) extends TemplateTree with Positional - case class Display(exp: ScalaExp) extends TemplateTree with Positional - case class Comment(msg: String) extends TemplateTree with Positional - case class ScalaExp(parts: collection.Seq[ScalaExpPart]) extends TemplateTree with Positional - case class Simple(code: String) extends ScalaExpPart with Positional + case class Def(name: PosString, params: PosString, resultType: Option[PosString], code: Simple) extends Positional + case class Plain(text: String) extends TemplateTree with Positional + case class Display(exp: ScalaExp) extends TemplateTree with Positional + case class Comment(msg: String) extends TemplateTree with Positional + case class ScalaExp(parts: collection.Seq[ScalaExpPart]) extends TemplateTree with Positional + case class Simple(code: String) extends ScalaExpPart with Positional case class Block(whitespace: String, args: Option[PosString], content: collection.Seq[TemplateTree]) extends ScalaExpPart with Positional diff --git a/parser/src/main/scala/play/twirl/parser/TwirlParser.scala b/parser/src/main/scala/play/twirl/parser/TwirlParser.scala index b9570e3b..e873e0de 100644 --- a/parser/src/main/scala/play/twirl/parser/TwirlParser.scala +++ b/parser/src/main/scala/play/twirl/parser/TwirlParser.scala @@ -404,11 +404,41 @@ class TwirlParser(val shouldParseInclusiveDot: Boolean) { val templDecl = templateDeclaration() if (templDecl != null) { anyUntil(c => c != ' ' && c != '\t', inclusive = false) - if (check("=")) { - anyUntil(c => c != ' ' && c != '\t', inclusive = false) - val code = scalaBlock() - if (code != null) { - result = Def(templDecl._1, templDecl._2, code) + var next = "" + if (check(":")) { + next = ":" + } else if (check("=")) { + next = "=" + } + if (next == ":" || next == "=") { + var resultType: Option[PosString] = None + if (next == ":") { + anyUntil(c => c != ' ' && c != '\t', inclusive = false) + val resultTypePos = input.offset() + val rt = identifier() match { + case null => null + case id => id + } + if (rt != null) { + val types = Option(squareBrackets()).getOrElse("") + resultType = Some(position(PosString(rt + types), resultTypePos)) + + anyUntil(c => c != ' ' && c != '\t', inclusive = false) + if (check("=")) { + next = "=" + } else { + next = "" + } + } else { + next = "" + } + } + if (next == "=") { + anyUntil(c => c != ' ' && c != '\t', inclusive = false) + val code = scalaBlock() + if (code != null) { + result = Def(templDecl._1, templDecl._2, resultType, code) + } } } } @@ -886,9 +916,9 @@ class TwirlParser(val shouldParseInclusiveDot: Boolean) { if (name != null) { val paramspos = input.offset() - val types = Option(squareBrackets()).getOrElse(PosString("")) + val types = Option(squareBrackets()).getOrElse("") val args = several[String, ArrayBuffer[String]] { () => parentheses() } - val params = position(PosString(types.toString + args.mkString), paramspos) + val params = position(PosString(types + args.mkString), paramspos) if (params != null) return (name, params) } else input.regress(1) // don't consume @ diff --git a/parser/src/test/scala/play/twirl/parser/test/ParserSpec.scala b/parser/src/test/scala/play/twirl/parser/test/ParserSpec.scala index e1687144..7cac16d1 100644 --- a/parser/src/test/scala/play/twirl/parser/test/ParserSpec.scala +++ b/parser/src/test/scala/play/twirl/parser/test/ParserSpec.scala @@ -180,6 +180,140 @@ class ParserSpec extends AnyWordSpec with Matchers with Inside { } } + "handle local definitions" when { + "resultType is given" in { + val tmpl = parseTemplateString( + """@implicitField: FieldConstructor = @{ FieldConstructor(myFieldConstructorTemplate.f) }""" + ) + val localDef = tmpl.defs(0) + + localDef.name.str mustBe "implicitField" + localDef.name.pos.line mustBe 1 + localDef.name.pos.column mustBe 2 + + val resultType = localDef.resultType.get + resultType.str mustBe "FieldConstructor" + resultType.pos.line mustBe 1 + resultType.pos.column mustBe 17 + + localDef.params.str mustBe "" + + localDef.code.code mustBe "{ FieldConstructor(myFieldConstructorTemplate.f) }" + } + "resultType is given without implicit prefixed" in { + val tmpl = parseTemplateString( + """@field: FieldConstructor = @{ FieldConstructor(myFieldConstructorTemplate.f) }""" + ) + val localDef = tmpl.defs(0) + + localDef.name.str mustBe "field" + localDef.name.pos.line mustBe 1 + localDef.name.pos.column mustBe 2 + + val resultType = localDef.resultType.get + resultType.str mustBe "FieldConstructor" + resultType.pos.line mustBe 1 + resultType.pos.column mustBe 9 + + localDef.params.str mustBe "" + + localDef.code.code mustBe "{ FieldConstructor(myFieldConstructorTemplate.f) }" + } + "resultType with type is given" in { + val tmpl = parseTemplateString( + """@implicitField: FieldConstructor[FooType] = @{ FieldConstructor(myFieldConstructorTemplate.f) }""" + ) + val localDef = tmpl.defs(0) + + localDef.name.str mustBe "implicitField" + localDef.name.pos.line mustBe 1 + localDef.name.pos.column mustBe 2 + + val resultType = localDef.resultType.get + resultType.str mustBe "FieldConstructor[FooType]" + resultType.pos.line mustBe 1 + resultType.pos.column mustBe 17 + + localDef.params.str mustBe "" + + localDef.code.code mustBe "{ FieldConstructor(myFieldConstructorTemplate.f) }" + } + "resultType is given without spaces" in { + val tmpl = parseTemplateString( + """@implicitField:FieldConstructor=@{ FieldConstructor(myFieldConstructorTemplate.f) }""" + ) + val localDef = tmpl.defs(0) + + localDef.name.str mustBe "implicitField" + localDef.name.pos.line mustBe 1 + localDef.name.pos.column mustBe 2 + + val resultType = localDef.resultType.get + resultType.str mustBe "FieldConstructor" + resultType.pos.line mustBe 1 + resultType.pos.column mustBe 16 + + localDef.params.str mustBe "" + + localDef.code.code mustBe "{ FieldConstructor(myFieldConstructorTemplate.f) }" + } + "resultType and params are given" in { + val tmpl = parseTemplateString( + """@implicitField(foo: String, bar: Int): FieldConstructor = @{ FieldConstructor(myFieldConstructorTemplate.f) }""" + ) + val localDef = tmpl.defs(0) + + localDef.name.str mustBe "implicitField" + localDef.name.pos.line mustBe 1 + localDef.name.pos.column mustBe 2 + + val resultType = localDef.resultType.get + resultType.str mustBe "FieldConstructor" + resultType.pos.line mustBe 1 + resultType.pos.column mustBe 40 + + localDef.params.str mustBe "(foo: String, bar: Int)" + localDef.params.pos.line mustBe 1 + localDef.params.pos.column mustBe 15 + + localDef.code.code mustBe "{ FieldConstructor(myFieldConstructorTemplate.f) }" + } + "no resultType and no params are given" in { + val tmpl = parseTemplateString( + """@implicitField = @{ FieldConstructor(myFieldConstructorTemplate.f) }""" + ) + val localDef = tmpl.defs(0) + + localDef.name.str mustBe "implicitField" + localDef.name.pos.line mustBe 1 + localDef.name.pos.column mustBe 2 + + localDef.resultType mustBe None + + localDef.params.str mustBe "" + + localDef.code.code mustBe "{ FieldConstructor(myFieldConstructorTemplate.f) }" + } + "no resultType but params are given" in { + val tmpl = parseTemplateString( + """@implicitField(foo: String, bar: Int) = @{ FieldConstructor(myFieldConstructorTemplate.f) }""" + ) + val localDef = tmpl.defs(0) + + localDef.name.str mustBe "implicitField" + localDef.name.pos.line mustBe 1 + localDef.name.pos.column mustBe 2 + + localDef.resultType mustBe None + + localDef.params.str mustBe "(foo: String, bar: Int)" + localDef.params.pos.line mustBe 1 + localDef.params.pos.column mustBe 15 + + localDef.code.code mustBe "{ FieldConstructor(myFieldConstructorTemplate.f) }" + } + } + "handle string literals within parentheses" when { "with left parenthesis" in { parseStringSuccess("""@foo("(")""")