From c679796e2f19d3ccce17b7893a7f08cd0264f247 Mon Sep 17 00:00:00 2001 From: Shashank Shailabh Date: Sat, 4 Oct 2025 07:58:38 +0530 Subject: [PATCH 1/3] add source text and whitespace on new channel --- .../jknack/handlebars/internal/HbsLexer.g4 | 4 +- .../github/jknack/handlebars/Template.java | 14 + .../handlebars/internal/BaseTemplate.java | 65 +++++ .../handlebars/internal/HbsParserFactory.java | 2 +- .../handlebars/internal/TemplateBuilder.java | 68 ++++- .../handlebars/internal/TemplateList.java | 9 + .../handlebars/WhitespaceChannelTest.java | 241 ++++++++++++++++++ 7 files changed, 395 insertions(+), 8 deletions(-) create mode 100644 handlebars/src/test/java/com/github/jknack/handlebars/WhitespaceChannelTest.java diff --git a/handlebars/src/main/antlr4/com/github/jknack/handlebars/internal/HbsLexer.g4 b/handlebars/src/main/antlr4/com/github/jknack/handlebars/internal/HbsLexer.g4 index bc6727a7b..e09c5937a 100644 --- a/handlebars/src/main/antlr4/com/github/jknack/handlebars/internal/HbsLexer.g4 +++ b/handlebars/src/main/antlr4/com/github/jknack/handlebars/internal/HbsLexer.g4 @@ -9,6 +9,8 @@ lexer grammar HbsLexer; boolean whiteSpaceControl; + public static final int WHITESPACE_CHANNEL = 1; + public HbsLexer(CharStream input, String start, String end) { this(input); this.start = start; @@ -374,5 +376,5 @@ RP ; WS - : [ \t\r\n] -> skip + : [ \t\r\n] -> channel(1) ; diff --git a/handlebars/src/main/java/com/github/jknack/handlebars/Template.java b/handlebars/src/main/java/com/github/jknack/handlebars/Template.java index dd87aa1eb..96337c87e 100644 --- a/handlebars/src/main/java/com/github/jknack/handlebars/Template.java +++ b/handlebars/src/main/java/com/github/jknack/handlebars/Template.java @@ -134,6 +134,20 @@ public List collectReferenceParameters() { */ String text(); + /** + * Provide the exact original source text including all whitespace. This method provides lossless + * source reconstruction by using whitespace preserved on hidden channels during parsing. + * + *

If token information is not available (for templates created without token streams), this + * method falls back to {@link #text()}. + * + * @return The exact original source text, or reconstructed text if token info unavailable. + * @since 4.6.0 + */ + default String getSourceText() { + return text(); + } + /** * Convert this template to JavaScript template (a.k.a precompiled template). Compilation is done * by handlebars.js and a JS Engine (usually Rhino). diff --git a/handlebars/src/main/java/com/github/jknack/handlebars/internal/BaseTemplate.java b/handlebars/src/main/java/com/github/jknack/handlebars/internal/BaseTemplate.java index 2909e0666..3165cdd7b 100644 --- a/handlebars/src/main/java/com/github/jknack/handlebars/internal/BaseTemplate.java +++ b/handlebars/src/main/java/com/github/jknack/handlebars/internal/BaseTemplate.java @@ -26,6 +26,8 @@ import java.util.Map; import java.util.Set; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.Token; import org.apache.commons.lang3.StringUtils; import com.github.jknack.handlebars.Context; @@ -59,6 +61,18 @@ abstract class BaseTemplate implements Template { /** A pre-compiled JavaScript function. */ private String javaScript; + /** + * Token stream for lossless source reconstruction. Optional - if null, falls back to text() + * reconstruction. + */ + protected CommonTokenStream tokenStream; + + /** Start token index in the token stream (inclusive). */ + protected int startTokenIndex = -1; + + /** End token index in the token stream (inclusive). */ + protected int endTokenIndex = -1; + /** * Creates a new {@link BaseTemplate}. * @@ -194,6 +208,57 @@ public BaseTemplate position(final int line, final int column) { return this; } + /** + * Set the token stream for lossless source reconstruction. + * + * @param tokenStream The token stream. + * @return This template. + */ + public BaseTemplate tokenStream(final CommonTokenStream tokenStream) { + this.tokenStream = tokenStream; + return this; + } + + /** + * Set the token span (start and end indices) for lossless source reconstruction. + * + * @param startTokenIndex The start token index (inclusive). + * @param endTokenIndex The end token index (inclusive). + * @return This template. + */ + public BaseTemplate tokenSpan(final int startTokenIndex, final int endTokenIndex) { + this.startTokenIndex = startTokenIndex; + this.endTokenIndex = endTokenIndex; + return this; + } + + /** + * Get the exact original source text for this template, including all whitespace. This provides + * lossless source reconstruction using the whitespace preserved on channel 1. + * + *

If token information is not available (for templates created without token spans), this + * falls back to the reconstructed text() method. + * + * @return The exact original source text, or reconstructed text if token info unavailable. + * @since 4.6.0 + */ + public String getSourceText() { + if (tokenStream != null && startTokenIndex >= 0 && endTokenIndex >= 0) { + // Extract text from ALL tokens (all channels) to get exact original source + // This includes whitespace tokens on channel 1 + StringBuilder text = new StringBuilder(); + for (int i = startTokenIndex; i <= endTokenIndex; i++) { + Token token = tokenStream.get(i); + if (token != null) { + text.append(token.getText()); + } + } + return text.toString(); + } + // Fallback to reconstructed text if token information not available + return text(); + } + @Override public > S as(final Class rootType) { notNull(rootType, "The rootType can't be null."); diff --git a/handlebars/src/main/java/com/github/jknack/handlebars/internal/HbsParserFactory.java b/handlebars/src/main/java/com/github/jknack/handlebars/internal/HbsParserFactory.java index ce32afb56..4a59d3aa1 100644 --- a/handlebars/src/main/java/com/github/jknack/handlebars/internal/HbsParserFactory.java +++ b/handlebars/src/main/java/com/github/jknack/handlebars/internal/HbsParserFactory.java @@ -86,7 +86,7 @@ public Template parse(final TemplateSource source) throws IOException { /** Build the AST. */ TemplateBuilder builder = - new TemplateBuilder(handlebars, source) { + new TemplateBuilder(handlebars, source, (CommonTokenStream) parser.getTokenStream()) { @Override protected void reportError( final CommonToken offendingToken, diff --git a/handlebars/src/main/java/com/github/jknack/handlebars/internal/TemplateBuilder.java b/handlebars/src/main/java/com/github/jknack/handlebars/internal/TemplateBuilder.java index 291ded550..d4b2349ea 100644 --- a/handlebars/src/main/java/com/github/jknack/handlebars/internal/TemplateBuilder.java +++ b/handlebars/src/main/java/com/github/jknack/handlebars/internal/TemplateBuilder.java @@ -98,6 +98,9 @@ private static class PartialInfo { /** The template source. Required. */ private TemplateSource source; + /** The token stream for lossless source reconstruction. Optional. */ + private org.antlr.v4.runtime.CommonTokenStream tokenStream; + /** Flag to track dead spaces and lines. */ private Boolean hasTag; @@ -124,6 +127,22 @@ private static class PartialInfo { this.source = notNull(source, "The template source is required."); } + /** + * Creates a new {@link TemplateBuilder} with token stream for lossless source reconstruction. + * + * @param handlebars A handlebars object. required. + * @param source The template source. required. + * @param tokenStream The token stream. optional. + */ + TemplateBuilder( + final Handlebars handlebars, + final TemplateSource source, + final org.antlr.v4.runtime.CommonTokenStream tokenStream) { + this.handlebars = notNull(handlebars, "The handlebars can't be null."); + this.source = notNull(source, "The template source is required."); + this.tokenStream = tokenStream; + } + @Override public Template visit(final ParseTree tree) { return (Template) super.visit(tree); @@ -342,14 +361,16 @@ public Template visitUnless(final UnlessContext ctx) { public Template visitVar(final VarContext ctx) { hasTag(false); SexprContext sexpr = ctx.sexpr(); - return newVar( + return newVarWithTokens( sexpr.QID().getSymbol(), TagType.VAR, params(sexpr.param()), hash(sexpr.hash()), ctx.start.getText(), ctx.stop.getText(), - ctx.DECORATOR() != null); + ctx.DECORATOR() != null, + ctx.start, + ctx.stop); } @Override @@ -365,28 +386,32 @@ public Object visitEscape(final EscapeContext ctx) { public Template visitTvar(final TvarContext ctx) { hasTag(false); SexprContext sexpr = ctx.sexpr(); - return newVar( + return newVarWithTokens( sexpr.QID().getSymbol(), TagType.TRIPLE_VAR, params(sexpr.param()), hash(sexpr.hash()), ctx.start.getText(), ctx.stop.getText(), - false); + false, + ctx.start, + ctx.stop); } @Override public Template visitAmpvar(final AmpvarContext ctx) { hasTag(false); SexprContext sexpr = ctx.sexpr(); - return newVar( + return newVarWithTokens( sexpr.QID().getSymbol(), TagType.AMP_VAR, params(sexpr.param()), hash(sexpr.hash()), ctx.start.getText(), ctx.stop.getText(), - false); + false, + ctx.start, + ctx.stop); } /** @@ -476,6 +501,37 @@ private Variable newVar( return var; } + /** + * Build a new {@link Variable} with token span for lossless source reconstruction. + * + * @param name The var's name. + * @param varType The var's type. + * @param params The var params. + * @param hash The var hash. + * @param startDelimiter The current start delimiter. + * @param endDelimiter The current end delimiter. + * @param decorator True, for var decorators. + * @param startToken The start token. + * @param stopToken The stop token. + * @return A new {@link Variable}. + */ + private Variable newVarWithTokens( + final Token name, + final TagType varType, + final List params, + final Map hash, + final String startDelimiter, + final String endDelimiter, + final boolean decorator, + final Token startToken, + final Token stopToken) { + Variable var = newVar(name, varType, params, hash, startDelimiter, endDelimiter, decorator); + if (tokenStream != null && startToken != null && stopToken != null) { + var.tokenStream(tokenStream).tokenSpan(startToken.getTokenIndex(), stopToken.getTokenIndex()); + } + return var; + } + /** * Build a hash. * diff --git a/handlebars/src/main/java/com/github/jknack/handlebars/internal/TemplateList.java b/handlebars/src/main/java/com/github/jknack/handlebars/internal/TemplateList.java index 6c106e3d1..a334f71d5 100644 --- a/handlebars/src/main/java/com/github/jknack/handlebars/internal/TemplateList.java +++ b/handlebars/src/main/java/com/github/jknack/handlebars/internal/TemplateList.java @@ -96,6 +96,15 @@ public String text() { return buffer.toString(); } + @Override + public String getSourceText() { + StringBuilder buffer = new StringBuilder(); + for (Template node : nodes) { + buffer.append(node.getSourceText()); + } + return buffer.toString(); + } + @Override public Iterator