|
22 | 22 |
|
23 | 23 | import org.antlr.v4.runtime.CommonToken; |
24 | 24 | import org.antlr.v4.runtime.Token; |
| 25 | +import org.apache.commons.lang3.StringUtils; |
25 | 26 |
|
26 | 27 | import com.github.jknack.handlebars.internal.HbsParser.AmpvarContext; |
27 | 28 | import com.github.jknack.handlebars.internal.HbsParser.BlockContext; |
28 | 29 | import com.github.jknack.handlebars.internal.HbsParser.CommentContext; |
29 | 30 | import com.github.jknack.handlebars.internal.HbsParser.DelimitersContext; |
30 | | -import com.github.jknack.handlebars.internal.HbsParser.NewlineContext; |
31 | 31 | import com.github.jknack.handlebars.internal.HbsParser.PartialContext; |
32 | | -import com.github.jknack.handlebars.internal.HbsParser.SpacesContext; |
33 | 32 | import com.github.jknack.handlebars.internal.HbsParser.TemplateContext; |
34 | 33 | import com.github.jknack.handlebars.internal.HbsParser.TextContext; |
35 | 34 | import com.github.jknack.handlebars.internal.HbsParser.TvarContext; |
|
45 | 44 | public class MustacheSpec extends HbsParserBaseListener { |
46 | 45 |
|
47 | 46 | /** |
48 | | - * Track if the current line has real text (not spaces). |
| 47 | + * Tracks if the current line should be treated as stand-alone. |
49 | 48 | */ |
50 | | - private boolean nonSpace = false; |
| 49 | + private Boolean standAlone; |
51 | 50 |
|
52 | 51 | /** |
53 | | - * Track if the current line has mustache instruction. |
| 52 | + * Tracks text tokens for future whitespace removal. |
54 | 53 | */ |
55 | | - private Boolean hasTag; |
| 54 | + private List<CommonToken> textTokens = new ArrayList<>(); |
56 | 55 |
|
57 | 56 | /** |
58 | | - * Track the current line. |
| 57 | + * Channel for tokens that need their last line removed. |
59 | 58 | */ |
60 | | - protected StringBuilder line = new StringBuilder(); |
61 | | - |
62 | | - /** |
63 | | - * Track the spaces/lines that need to be excluded. |
64 | | - */ |
65 | | - private List<CommonToken> spaces = new ArrayList<>(); |
66 | | - |
67 | | - @Override |
68 | | - public void enterSpaces(final SpacesContext ctx) { |
69 | | - CommonToken space = (CommonToken) ctx.SPACE().getSymbol(); |
70 | | - line.append(space.getText()); |
71 | | - spaces.add(space); |
72 | | - } |
73 | | - |
74 | | - @Override |
75 | | - public void enterNewline(final NewlineContext ctx) { |
76 | | - CommonToken newline = (CommonToken) ctx.NL().getSymbol(); |
77 | | - spaces.add(newline); |
78 | | - stripSpaces(); |
79 | | - } |
| 59 | + public static final int REMOVE_LAST_LINE_CHANNEL = Token.MIN_USER_CHANNEL_VALUE; |
80 | 60 |
|
81 | 61 | @Override |
82 | 62 | public void exitTemplate(final TemplateContext ctx) { |
83 | | - stripSpaces(); |
| 63 | + removeWhitespace(); |
| 64 | + this.textTokens.clear(); |
| 65 | + this.standAlone = null; |
84 | 66 | } |
85 | 67 |
|
86 | | - /** |
87 | | - * Move tokens to the hidden channel if necessary. |
88 | | - */ |
89 | | - private void stripSpaces() { |
90 | | - boolean hasTag = this.hasTag == null ? false : this.hasTag.booleanValue(); |
91 | | - if (hasTag && !nonSpace) { |
92 | | - for (CommonToken space : spaces) { |
93 | | - space.setChannel(Token.HIDDEN_CHANNEL); |
| 68 | + @Override |
| 69 | + public void enterText(final TextContext ctx) { |
| 70 | + CommonToken currentToken = (CommonToken) ctx.getStart(); |
| 71 | + String currentText = currentToken.getText(); |
| 72 | + Integer secondLineIndex = MustacheStringUtils.indexOfSecondLine(currentText); |
| 73 | + if (secondLineIndex == null) { |
| 74 | + // Non-whitespace was found. Clear list and start again with the current token |
| 75 | + this.textTokens.clear(); |
| 76 | + this.standAlone = null; |
| 77 | + } else if (secondLineIndex >= 0) { |
| 78 | + // Try to remove whitespace from the previously saved text tokens |
| 79 | + boolean canRemoveWhitespace = removeWhitespace(); |
| 80 | + if (canRemoveWhitespace) { |
| 81 | + // Remove the first line of this text as well |
| 82 | + String newText = StringUtils.substring(currentText, secondLineIndex); |
| 83 | + currentToken.setText(newText); |
94 | 84 | } |
95 | | - } else { |
96 | | - spaces.clear(); |
| 85 | + |
| 86 | + // Clear list and start again with the current token |
| 87 | + this.textTokens.clear(); |
| 88 | + this.standAlone = null; |
97 | 89 | } |
98 | 90 |
|
99 | | - this.hasTag = null; |
100 | | - nonSpace = false; |
101 | | - line.setLength(0); |
102 | | - } |
103 | | - |
104 | | - @Override |
105 | | - public void enterText(final TextContext ctx) { |
106 | | - nonSpace = true; |
| 91 | + this.textTokens.add(currentToken); |
107 | 92 | } |
108 | 93 |
|
109 | 94 | @Override |
@@ -136,7 +121,8 @@ public void enterUnless(final UnlessContext ctx) { |
136 | 121 | hasTag(true); |
137 | 122 | } |
138 | 123 |
|
139 | | - @Override public void enterElseBlock(final HbsParser.ElseBlockContext ctx) { |
| 124 | + @Override |
| 125 | + public void enterElseBlock(final HbsParser.ElseBlockContext ctx) { |
140 | 126 | hasTag(true); |
141 | 127 | } |
142 | 128 |
|
@@ -166,8 +152,63 @@ public void enterVar(final VarContext ctx) { |
166 | 152 | * @param hasTag True, to indicate there is a mustache instruction. |
167 | 153 | */ |
168 | 154 | private void hasTag(final boolean hasTag) { |
169 | | - if (this.hasTag != Boolean.FALSE) { |
170 | | - this.hasTag = hasTag; |
| 155 | + if (this.standAlone != Boolean.FALSE) { |
| 156 | + this.standAlone = hasTag; |
171 | 157 | } |
172 | 158 | } |
| 159 | + |
| 160 | + /** |
| 161 | + * Remove whitespace from previously saved text tokens. |
| 162 | + * |
| 163 | + * @return True if whitespace could be removed. False otherwise. |
| 164 | + */ |
| 165 | + private boolean removeWhitespace() { |
| 166 | + boolean canRemoveWhitespace = this.standAlone == null ? false : this.standAlone.booleanValue(); |
| 167 | + if (!canRemoveWhitespace) { |
| 168 | + return false; |
| 169 | + } |
| 170 | + |
| 171 | + if (textTokens.isEmpty()) { |
| 172 | + return true; |
| 173 | + } |
| 174 | + |
| 175 | + // Try to remove whitespace from the last line |
| 176 | + CommonToken lastToken = textTokens.get(textTokens.size() - 1); |
| 177 | + String lastText = lastToken.getText(); |
| 178 | + int newlineIndex = StringUtils.lastIndexOfAny(lastText, "\r", "\n"); |
| 179 | + if (newlineIndex >= 0) { |
| 180 | + String lastLine = lastText.substring(newlineIndex + 1); |
| 181 | + if (!StringUtils.isWhitespace(lastLine)) { |
| 182 | + // Cannot remove anything since line contains non-whitespace |
| 183 | + return false; |
| 184 | + } |
| 185 | + |
| 186 | + // Mark the last line for removal |
| 187 | + lastToken.setChannel(REMOVE_LAST_LINE_CHANNEL); |
| 188 | + } else { |
| 189 | + // Check for non-whitespace |
| 190 | + int maxIndex = textTokens.size() - 1; |
| 191 | + for (int i = maxIndex; i >= 0; i--) { |
| 192 | + CommonToken loopToken = textTokens.get(i); |
| 193 | + String loopText = loopToken.getText(); |
| 194 | + if (!StringUtils.isWhitespace(loopText)) { |
| 195 | + // Cannot remove anything since line contains non-whitespace |
| 196 | + return false; |
| 197 | + } |
| 198 | + if (i == 0) { |
| 199 | + // Mark tokens already checked for removal |
| 200 | + int j = 0; |
| 201 | + while (j <= maxIndex) { |
| 202 | + CommonToken whiteSpaceToken = textTokens.get(j); |
| 203 | + whiteSpaceToken.setChannel(Token.HIDDEN_CHANNEL); |
| 204 | + j++; |
| 205 | + } |
| 206 | + |
| 207 | + break; |
| 208 | + } |
| 209 | + } |
| 210 | + } |
| 211 | + |
| 212 | + return true; |
| 213 | + } |
173 | 214 | } |
0 commit comments