diff --git a/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTLexer.g4 b/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTLexer.g4 index c3e04e9..6c4469e 100644 --- a/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTLexer.g4 +++ b/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTLexer.g4 @@ -168,6 +168,9 @@ FINAL: [fF][iI][nN][aA][lL]; ABSTRACT: [aA][Bb][sS][tT][Rr][aA][cC][tT]; STATIC: [sS][tT][aA][tT][iI][cC]; SCRIPTOPEN: '<' [cC] [fF] [sS] [cC] [rR] [iI] [pP] [tT] '>'; +// Lucee's template block: ``` drops out of cfscript into template markup, the mirror of +// dropping the other way. The body interpolates #...# like a string does. +OPEN_TEMPLATE: '```' -> pushMode(InTemplate); SCRIPTCLOSE:''; // operators DOT: '.'; @@ -456,4 +459,17 @@ HASH_SINGLE mode HashMode; HashMode_ANY: -> popMode,skip; - \ No newline at end of file + +mode InTemplate; +CLOSE_TEMPLATE + : '```' -> popMode +; +TEMPLATE_DOUBLEHASH + : '##' -> type(DOUBLEHASH) +; +TEMPLATE_LITERAL + : (~[`#]+ | '`' ~[`] | '``' ~[`])+ +; +TEMPLATE_HASH + : '#' -> type(POUND_SIGN),pushMode(HashMode),pushMode(DefaultMode) +; diff --git a/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 b/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 index cd5cbe6..f2514ac 100644 --- a/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 +++ b/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 @@ -18,6 +18,16 @@ cfscriptBlock : SCRIPTOPEN scriptBlock SCRIPTCLOSE ; +// The inverse of cfscriptBlock: markup embedded in cfscript, between ``` fences, with +// #...# interpolation as in a string literal. +templateBlock + : OPEN_TEMPLATE (templateLiteralPart | POUND_SIGN anExpression POUND_SIGN)* CLOSE_TEMPLATE + ; + +templateLiteralPart + : TEMPLATE_LITERAL | DOUBLEHASH + ; + componentDeclaration : componentModifier? COMPONENT componentAttribute* componentGuts //-> ( COMPDECL componentAttribute* componentGuts) ; @@ -154,6 +164,7 @@ statement | startExpression SEMICOLON | SEMICOLON // empty statement | functionCall // without semi + | templateBlock ; endOfStatement diff --git a/cfml.parsing/src/main/java/cfml/parsing/cfscript/script/CFTemplateBlockStatement.java b/cfml.parsing/src/main/java/cfml/parsing/cfscript/script/CFTemplateBlockStatement.java new file mode 100644 index 0000000..aa71c3f --- /dev/null +++ b/cfml.parsing/src/main/java/cfml/parsing/cfscript/script/CFTemplateBlockStatement.java @@ -0,0 +1,62 @@ +package cfml.parsing.cfscript.script; + +import java.util.ArrayList; +import java.util.List; + +import org.antlr.v4.runtime.Token; + +import cfml.parsing.cfscript.CFExpression; +import cfml.parsing.util.ArrayBuilder; + +/** + * Lucee's template block: markup embedded in cfscript between ``` fences, the mirror + * of <cfscript> dropping the other way. + * + * The body is markup rather than cfscript, so it is kept as its source text and written back + * unchanged. What a consumer needs from it is the interpolated #...# expressions, + * which are parsed and reachable through {@link #getExpressions()} and {@link + * #decomposeExpression()} -- a variable used only inside a template block is still a use. + */ +public class CFTemplateBlockStatement extends CFParsedStatement { + + private static final long serialVersionUID = 1L; + + private static final String FENCE = "```"; + + private String content; + private List expressions; + + public CFTemplateBlockStatement(Token _t, String _content, List _expressions) { + super(_t); + content = _content; + expressions = _expressions == null ? new ArrayList() : _expressions; + for (CFExpression expression : expressions) { + expression.setParent(this); + } + } + + /** The markup between the fences, exactly as written, interpolations included. */ + public String getContent() { + return content; + } + + /** The #...# expressions inside the block, in source order. */ + public List getExpressions() { + return expressions; + } + + @Override + public String Decompile(int indent) { + return FENCE + content + FENCE; + } + + @Override + public List decomposeExpression() { + return expressions; + } + + @Override + public List decomposeScript() { + return ArrayBuilder.createCFScriptStatement(); + } +} diff --git a/cfml.parsing/src/main/java/cfml/parsing/cfscript/walker/CFScriptStatementVisitor.java b/cfml.parsing/src/main/java/cfml/parsing/cfscript/walker/CFScriptStatementVisitor.java index 8e543f1..653b841 100644 --- a/cfml.parsing/src/main/java/cfml/parsing/cfscript/walker/CFScriptStatementVisitor.java +++ b/cfml.parsing/src/main/java/cfml/parsing/cfscript/walker/CFScriptStatementVisitor.java @@ -8,10 +8,13 @@ import java.util.Stack; import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.misc.Interval; import org.antlr.v4.runtime.tree.ParseTree; import cfml.CFSCRIPTParser.AbortStatementContext; import cfml.CFSCRIPTParser.AdminStatementContext; +import cfml.CFSCRIPTParser.AnExpressionContext; import cfml.CFSCRIPTParser.AnonymousFunctionDeclarationContext; import cfml.CFSCRIPTParser.AssignmentExpressionContext; import cfml.CFSCRIPTParser.BaseExpressionContext; @@ -52,6 +55,7 @@ import cfml.CFSCRIPTParser.StartExpressionContext; import cfml.CFSCRIPTParser.StatementContext; import cfml.CFSCRIPTParser.SwitchStatementContext; +import cfml.CFSCRIPTParser.TemplateBlockContext; import cfml.CFSCRIPTParser.TagFunctionStatementContext; import cfml.CFSCRIPTParser.TagStatementContext; import cfml.CFSCRIPTParser.TagThrowStatementContext; @@ -95,6 +99,7 @@ import cfml.parsing.cfscript.script.CFScriptStatement; import cfml.parsing.cfscript.script.CFSwitchStatement; import cfml.parsing.cfscript.script.CFTagThrowStatement; +import cfml.parsing.cfscript.script.CFTemplateBlockStatement; import cfml.parsing.cfscript.script.CFThreadStatement; import cfml.parsing.cfscript.script.CFThrowStatement; import cfml.parsing.cfscript.script.CFTransactionStatement; @@ -585,6 +590,25 @@ public CFScriptStatement visitExitStatement(ExitStatementContext ctx) { return exitStatement; } + @Override + public CFScriptStatement visitTemplateBlock(TemplateBlockContext ctx) { + // The body is markup, not cfscript, so it is kept verbatim rather than modelled. Only the + // interpolations are parsed -- without a visitor here the block vanished from the tree and + // its #...# expressions leaked out as bare statements. + List expressions = new ArrayList(); + for (AnExpressionContext interpolation : ctx.anExpression()) { + CFExpression expression = cfExpressionVisitor.visit(interpolation); + if (expression != null) { + expressions.add(expression); + } + } + Token open = ctx.OPEN_TEMPLATE().getSymbol(); + Token close = ctx.CLOSE_TEMPLATE().getSymbol(); + String content = open.getInputStream().getText( + Interval.of(open.getStopIndex() + 1, close.getStartIndex() - 1)); + return new CFTemplateBlockStatement(open, content, expressions); + } + @Override public CFScriptStatement visitParamStatement(ParamStatementContext ctx) { // System.out.println("visitParamStatement"); diff --git a/cfml.parsing/src/test/resources/cfml/tests/expressions/template_block_48.cfc b/cfml.parsing/src/test/resources/cfml/tests/expressions/template_block_48.cfc new file mode 100644 index 0000000..d7d0b54 --- /dev/null +++ b/cfml.parsing/src/test/resources/cfml/tests/expressions/template_block_48.cfc @@ -0,0 +1,7 @@ +name = "World"; +user = { age: 30 }; +``` +

Hello #name#

+

You are #user.age# years old

+``` +after = 1; diff --git a/cfml.parsing/src/test/resources/cfml/tests/expressions/template_block_48.expected.txt b/cfml.parsing/src/test/resources/cfml/tests/expressions/template_block_48.expected.txt new file mode 100644 index 0000000..07c701a --- /dev/null +++ b/cfml.parsing/src/test/resources/cfml/tests/expressions/template_block_48.expected.txt @@ -0,0 +1,144 @@ +/*===TOKENS===*/ +IDENTIFIER +'=' <=> +OPEN_STRING <"> +STRING_LITERAL +CLOSE_STRING <"> +';' <;> +Hidden:NEWLINE <> +IDENTIFIER +'=' <=> +'{' <{> +IDENTIFIER +':' <:> +INTEGER_LITERAL <30> +'}' <}> +';' <;> +Hidden:NEWLINE <> +OPEN_TEMPLATE <```> +TEMPLATE_LITERAL <

Hello> +'#' <#> +IDENTIFIER +'#' <#> +TEMPLATE_LITERAL <

+

You are> +'#' <#> +IDENTIFIER +'.' <.> +IDENTIFIER +'#' <#> +TEMPLATE_LITERAL > +CLOSE_TEMPLATE <```> +Hidden:NEWLINE <> +IDENTIFIER +'=' <=> +INTEGER_LITERAL <1> +';' <;> +Hidden:NEWLINE <> +/*===TREE===*/ +(scriptBlock + (element + (statement + (assignmentExpression + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier name)))) + ) + = + (startExpression + (baseExpression + (unaryExpression + (primaryExpression + (literalExpression (stringLiteral " (stringLiteralPart World) ")) + ) + ) + ) + ) + ) + (endOfStatement ;) + ) + ) + (element + (statement + (assignmentExpression + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier user)))) + ) + = + (startExpression + (baseExpression + (unaryExpression + (primaryExpression + (implicitStruct + { + (implicitStructElements + (implicitStructExpression + (implicitStructKeyExpression (multipartIdentifier (identifier age))) + : + (baseExpression (unaryExpression (primaryExpression (literalExpression 30)))) + ) + ) + } + ) + ) + ) + ) + ) + ) + (endOfStatement ;) + ) + ) + (element + (statement + (templateBlock + ``` + (templateLiteralPart '\n

Hello ') + # + (anExpression + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier name)))) + ) + ) + # + (templateLiteralPart '

\n

You are ') + # + (anExpression + (startExpression + (baseExpression + (unaryExpression (memberExpression (identifier user) . (identifier age))) + ) + ) + ) + # + (templateLiteralPart ' years old

\n') + ``` + ) + ) + ) + (element + (statement + (assignmentExpression + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier after)))) + ) + = + (startExpression + (baseExpression (unaryExpression (primaryExpression (literalExpression 1)))) + ) + ) + (endOfStatement ;) + ) + ) +) +/*======*/ +/*===DECOMPILE===*/ +{ +name = 'World'; +user = {age:30}; +``` +

Hello #name#

+

You are #user.age# years old

+```; +after = 1; + +} +/*======*/ \ No newline at end of file