/* * ============================================================================= * * Copyright (c) 2011-2016, The THYMELEAF team (http://www.thymeleaf.org) * * 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 org.thymeleaf.templateparser.text; /* * This class performs the processing of comments, just after they are identified by the parser. The idea * is that every comment that is identified as a 'commented element' should be converted to the adequate * 'element' events, and also that every comment that is identified as a 'commented expression' is processed by * removing the comment prefix/suffix and then also removing part of the following text event content (until any of * ';', ',' ,')', '}', ']' is found (taking care of the possible existence of literals, and of different levels of * object property hierarchy), or another non-text event is fired) * * Some examples (note the comment suffixes are written with a whitespace so that they don't close this comment): * * /*[# th:each="a : ${list}"]* / -> [# th:each="a : ${list}"] (decomposed into the corresponding events) * /*[(${someVar})]* / something; -> [(${someVar})]; (as a single TEXT event) * /*[[${someVar}]]* / something; -> [[${someVar}]]; (as a single TEXT event) * /*[whatever]* / -> /*[whatever]* / (as a single TEXT event) * /*whatever* / -> /*whatever* / (as a single TEXT event) * * NOTE: No comment event should ever be fired by this handler. "Normal" comments, i.e. those that are not commented * elements nor commented expressions, will be passed to the next handler in the chain as text events. * NOTE: The inlining mechanism is a part of the Standard Dialects, so the conversion performed by this handler * on inlined output expressions should only be applied if one of the Standard Dialects has been configured. * * @author Daniel Fernandez * @since 3.0.0 */ final class CommentProcessorTextHandler extends AbstractChainedTextHandler { private final boolean standardDialectPresent; private boolean filterTexts = false; private char[] filteredTextBuffer = null; private int filteredTextSize = 0; private int[] filteredTextLocator = null; CommentProcessorTextHandler(final boolean standardDialectPresent, final ITextHandler handler) { super(handler); this.standardDialectPresent = standardDialectPresent; } @Override public void handleDocumentEnd( final long endTimeNanos, final long totalTimeNanos, final int line, final int col) throws TextParseException { processFilteredTexts(); super.handleDocumentEnd(endTimeNanos, totalTimeNanos, line, col); } @Override public void handleComment( final char[] buffer, final int contentOffset, final int contentLen, final int outerOffset, final int outerLen, final int line, final int col) throws TextParseException { processFilteredTexts(); /* * FIRST STEP: Quickly determine if we actually need to do anything with this comment. We will process * every comment which has any of the shapes: [#...], [/...], [(...)] or [[...]]. * If we determine that a comment is not processable, we will output it as mere text. */ if (!isCommentProcessable(buffer, contentOffset, contentLen)) { super.handleText(buffer, outerOffset, outerLen, line, col); return; } /* * SECOND STEP: If these comments are here just wrapping an element, unwrap such element. */ final int maxi = contentOffset + contentLen; if (TextParsingElementUtil.isOpenElementStart(buffer, contentOffset, maxi)) { // This might be an open / standalone element, let's check how it ends if (TextParsingElementUtil.isElementEnd(buffer, maxi - 2, maxi, true)) { // It's a standalone element TextParsingElementUtil.parseStandaloneElement(buffer, contentOffset, contentLen, line, col + 2, getNext()); return; } else if (TextParsingElementUtil.isElementEnd(buffer, maxi - 1, maxi, false)) { // It's an open element TextParsingElementUtil.parseOpenElement(buffer, contentOffset, contentLen, line, col + 2, getNext()); return; } } else if (TextParsingElementUtil.isCloseElementStart(buffer, contentOffset, maxi)) { // Seems we may have an element being closed here... if (TextParsingElementUtil.isElementEnd(buffer, maxi - 1, maxi, false)) { // It's a standalone element TextParsingElementUtil.parseCloseElement(buffer, contentOffset, contentLen, line, col + 2, getNext()); return; } } /* * FINAL STEP: At this point, we know it's an expression, not an element. So we will not be modifying the * content of the comment (the expression itself, such as '[[${someVar}]]' or '[(${someVar})], but * we will be removing the rest of the line (or more correctly the rest of the structure the * comment is in). * * NOTE This will only be performed if a Standard Dialect is present, given it's the Standard * Dialects who define the output expression inlining mechanism. */ if (this.standardDialectPresent) { getNext().handleText(buffer, contentOffset, contentLen, line, col + 2); // +2 in order to count '[[' or [(' this.filterTexts = true; } else { // A Standard Dialect is not present, so this entire mechanism is disabled and the comment reported // as mere text. No need to perform any transformation getNext().handleText(buffer, outerOffset, outerLen, line, col); } } private boolean isCommentProcessable(final char[] buffer, final int contentOffset, final int contentLen) { final int maxi = contentOffset + contentLen; if (contentLen < 3 || buffer[contentOffset] != '[' || buffer[maxi - 1] != ']') { return false; } if (contentLen >= 4 && buffer[contentOffset + 1] == '(' && buffer[maxi - 2] == ')') { // That's a [(...)] commented unescaped expression return true; } if (contentLen >= 4 && buffer[contentOffset + 1] == '[' && buffer[maxi - 2] == ']') { // That's a [[...]] commented escaped expression return true; } // It is wrapped by brackets, but it is not a commented expression, so it will only be processable if it // matches the syntax of a commented element if (TextParsingElementUtil.isOpenElementStart(buffer, contentOffset, maxi)) { return TextParsingElementUtil.isElementEnd(buffer, maxi - 1, maxi, false); // we don't mind whether it is minimized or not } if (TextParsingElementUtil.isCloseElementStart(buffer, contentOffset, maxi)) { return TextParsingElementUtil.isElementEnd(buffer, maxi - 1, maxi, false); } return false; } @Override public void handleText( final char[] buffer, final int offset, final int len, final int line, final int col) throws TextParseException { if (this.filterTexts) { // We are filtering, so we will be accumulating texts until a different, non-text event comes, and // then start processing and see until which position we really have to filter out. filterText(buffer, offset, len, line, col); return; } super.handleText(buffer, offset, len, line, col); } private void filterText(final char[] buffer, final int offset, final int len, final int line, final int col) { // We need to put the filtered text into the buffer, which we might have to create or grow first if (this.filteredTextBuffer == null) { this.filteredTextBuffer = new char[Math.max(256, len)]; this.filteredTextSize = 0; this.filteredTextLocator = new int[2]; } else if (this.filteredTextSize + len > this.filteredTextBuffer.length) { final char[] newFilteredTextBuffer = new char[Math.max(this.filteredTextBuffer.length + 256, this.filteredTextSize + len)]; System.arraycopy(this.filteredTextBuffer, 0, newFilteredTextBuffer, 0, this.filteredTextSize); this.filteredTextBuffer = newFilteredTextBuffer; } System.arraycopy(buffer, offset, this.filteredTextBuffer, this.filteredTextSize, len); this.filteredTextSize += len; this.filteredTextLocator[0] = line; this.filteredTextLocator[1] = col; } private void processFilteredTexts() throws TextParseException { if (!this.filterTexts) { return; } final int filterOffset = computeFilterOffset(this.filteredTextBuffer, 0, this.filteredTextSize, this.filteredTextLocator); if (filterOffset < this.filteredTextSize) { // We filter out until filterOffset, and create a text event for the rest of the text super.handleText( this.filteredTextBuffer, filterOffset, (this.filteredTextSize - filterOffset), this.filteredTextLocator[0], this.filteredTextLocator[1]); } // else need to filter out ALL the texts until the next structure this.filteredTextSize = 0; this.filterTexts = false; } @Override public void handleStandaloneElementStart( final char[] buffer, final int nameOffset, final int nameLen, final boolean minimized, final int line, final int col) throws TextParseException { processFilteredTexts(); super.handleStandaloneElementStart(buffer, nameOffset, nameLen, minimized, line, col); } @Override public void handleOpenElementStart( final char[] buffer, final int nameOffset, final int nameLen, final int line, final int col) throws TextParseException { processFilteredTexts(); super.handleOpenElementStart(buffer, nameOffset, nameLen, line, col); } @Override public void handleCloseElementStart( final char[] buffer, final int nameOffset, final int nameLen, final int line, final int col) throws TextParseException { processFilteredTexts(); super.handleCloseElementStart(buffer, nameOffset, nameLen, line, col); } private static int computeFilterOffset(final char[] buffer, final int offset, final int maxi, final int[] locator) { if (offset == maxi) { return 0; } char literalDelimiter = 0; int arrayLevel = 0; int objectLevel = 0; int i = offset; while (i < maxi) { final char c = buffer[i++]; if (literalDelimiter != 0) { if (c == literalDelimiter && buffer[i - 2] != '\\') { literalDelimiter = 0; } ParsingLocatorUtil.countChar(locator, c); continue; } if (c == '\'' || c == '"') { literalDelimiter = c; ParsingLocatorUtil.countChar(locator, c); continue; } if (c == '{') { objectLevel++; ParsingLocatorUtil.countChar(locator, c); continue; } else if (objectLevel > 0 && c == '}') { objectLevel--; ParsingLocatorUtil.countChar(locator, c); continue; } else if (c == '[') { arrayLevel++; ParsingLocatorUtil.countChar(locator, c); continue; } else if (arrayLevel > 0 && c == ']') { arrayLevel--; ParsingLocatorUtil.countChar(locator, c); continue; } if (arrayLevel == 0 && objectLevel == 0) { if (c == '\n'){ return i - 1; // If we find a line feed after we are sure no open array/objects left, just stop } if (c == ';' || c == ',' || c == ')' || c == '}' || c == ']') { return i - 1; } if (c == '/' && i < maxi && buffer[i] == '/') { // This is a single-line comment return i - 1; } } ParsingLocatorUtil.countChar(locator, c); } return maxi; } }