/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.xwiki.rendering.internal.macro.velocity.filter; import java.util.regex.Pattern; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; import org.apache.velocity.VelocityContext; import org.slf4j.Logger; import org.xwiki.component.annotation.Component; import org.xwiki.component.phase.Initializable; import org.xwiki.component.phase.InitializationException; import org.xwiki.rendering.macro.velocity.filter.VelocityMacroFilter; import org.xwiki.velocity.internal.util.InvalidVelocityException; import org.xwiki.velocity.internal.util.VelocityParser; import org.xwiki.velocity.internal.util.VelocityParserContext; import org.xwiki.velocity.internal.util.VelocityBlock.VelocityType; /** * Replace each white space/new lines group by a space and inject $nl and $sp bindings in {@link VelocityContext} which * are used to respectively force a new line or a space before executing the velocity script. The bindings are removed * after script execution. * * @version $Id: 023c47db2061b938d2c24cf9f94609379824faba $ * @since 2.0M1 */ @Component @Named("html") @Singleton public class HTMLVelocityMacroFilter implements VelocityMacroFilter, Initializable { /** * The name of the new line binding. */ private static final String BINDING_NEWLINE = "nl"; /** * The value of the $nl binding. */ private static final String NEWLINE = "\n"; /** * The name of the space binding. */ private static final String BINDING_SPACE = "sp"; /** * The value of the $sp binding. */ private static final String SPACE = " "; /** * Match not UNIX new lines to replace them. */ private static final Pattern MSNEWLINE_PATTERN = Pattern.compile("\\r\\n|\\r"); /** * The logger to use for logging. */ @Inject private Logger logger; /** * Used to parser content to clean and match system directives and $nl variables. */ private VelocityParser velocityParser; @Override public void initialize() throws InitializationException { this.velocityParser = new VelocityParser(); } @Override public String before(String content, VelocityContext velocityContext) { // Add bindings velocityContext.put(BINDING_NEWLINE, NEWLINE); velocityContext.put(BINDING_SPACE, SPACE); return clean(content); } /** * Clean whites spaces in the velocity macro content. * <p> * Here a the rules: * <ul> * <li>any group of white spaces is replaced by a space</li> * <li>all white spaces after a velocity directive consuming following newline (#if, #set, etc.) are removed (no * replacement)</li> * <li>all white spaces before or after $nl are removed (no replacement)</li> * <li>all white spaces at the beginning and at the end of the content are removed (no replacement)</li> * <li>all velocity comments are removed</li> * </ul> * * @param content the content to clean * @return the cleaned content */ public String clean(String content) { StringBuffer contentBuffer = new StringBuffer(); char[] array = MSNEWLINE_PATTERN.matcher(content).replaceAll(NEWLINE).toCharArray(); VelocityParserContext context = new VelocityParserContext(); FilterContext filterContext = new FilterContext(); int i = 0; while (i < array.length) { try { if (array[i] == '#') { i = cleanKeyWord(contentBuffer, array, i, context, filterContext); continue; } else if (array[i] == '$') { i = cleanVar(contentBuffer, array, i, context, filterContext); continue; } else if (Character.isWhitespace(array[i])) { if (!filterContext.removeWhiteSpaces && contentBuffer.length() > 0) { filterContext.foundWhiteSpace = true; } ++i; continue; } } catch (InvalidVelocityException e) { this.logger.debug("Not a valid velocity keyword at char [" + i + "]", e); } flushWhiteSpaces(contentBuffer, filterContext, false); contentBuffer.append(array[i]); ++i; } flushWhiteSpaces(contentBuffer, filterContext, true); return contentBuffer.toString(); } /** * Handle velocity comments and directive. * * @param contentBuffer the final result buffer * @param array the source * @param currentIndex the current index in the source * @param context the velocity parser context * @param filterContext the filter context * @return the index after the comment or directive * @throws InvalidVelocityException not velocity */ private int cleanKeyWord(StringBuffer contentBuffer, char[] array, int currentIndex, VelocityParserContext context, FilterContext filterContext) throws InvalidVelocityException { int i = this.velocityParser.getKeyWord(array, currentIndex, null, context); if (context.getType() != VelocityType.COMMENT) { if (context.getType() == VelocityType.DIRECTIVE) { if (filterContext.wsGroup.length() == 0) { flushWhiteSpaces(filterContext.wsGroup, filterContext, false); } filterContext.wsGroup.append(array, currentIndex, i - currentIndex); filterContext.removeWhiteSpaces = true; } else { flushWhiteSpaces(contentBuffer, filterContext, false); contentBuffer.append(array, currentIndex, i - currentIndex); } } return i; } /** * Handle velocity variables. * * @param contentBuffer the final result buffer * @param array the source * @param currentIndex the current index in the source * @param context the velocity parser context * @param filterContext the filter context * @return the index after the variable * @throws InvalidVelocityException not velocity */ private int cleanVar(StringBuffer contentBuffer, char[] array, int currentIndex, VelocityParserContext context, FilterContext filterContext) throws InvalidVelocityException { StringBuffer varName = new StringBuffer(); int i = this.velocityParser.getVar(array, currentIndex, varName, null, context); if (varName.toString().equals(BINDING_NEWLINE)) { flushWhiteSpaces(contentBuffer, filterContext, true); contentBuffer.append("${nl}"); filterContext.removeWhiteSpaces = true; } else { flushWhiteSpaces(contentBuffer, filterContext, false); contentBuffer.append(array, currentIndex, i - currentIndex); } return i; } /** * Flush stored velocity directive which does not produce output (like #if, set, etc.). I also append a space if * there was a space in the group source. This is not make sure the whole no output velocity content is taken into * account when cleaning white spaces to be sure to have only one space at the end between two really generated * text. * * @param contentBuffer the final result buffer * @param filterContext the filter context * @param forceNoSpace if true no space is printed (only {@link FilterContext#wsGroup}) */ private void flushWhiteSpaces(StringBuffer contentBuffer, FilterContext filterContext, boolean forceNoSpace) { if (filterContext.wsGroup.length() > 0) { boolean space = filterContext.wsGroup.charAt(0) == ' '; if (forceNoSpace && space) { contentBuffer.append(filterContext.wsGroup, 1, filterContext.wsGroup.length()); } else { contentBuffer.append(filterContext.wsGroup); } filterContext.wsGroup.setLength(0); } if (filterContext.foundWhiteSpace && !forceNoSpace) { contentBuffer.append(' '); } filterContext.foundWhiteSpace = false; filterContext.removeWhiteSpaces = false; } @Override public String after(String content, VelocityContext velocityContext) { velocityContext.remove(BINDING_NEWLINE); velocityContext.remove(BINDING_SPACE); return content; } /** * Used to return store and retrieve some information during the filtering process. * * @version $Id: 023c47db2061b938d2c24cf9f94609379824faba $ */ static class FilterContext { /** * Indicate if at least one white space has been found. */ private boolean foundWhiteSpace; /** * Indicate if whites space has to be removed instead of replaced by a unique space. */ private boolean removeWhiteSpaces; /** * @see #getWsGroup() */ private StringBuffer wsGroup = new StringBuffer(); /** * @return Indicate if at least one white space has been found. */ public boolean isFoundWhiteSpace() { return this.foundWhiteSpace; } /** * @param foundWhiteSpace Indicate if at least one white space has been found. */ public void setFoundWhiteSpace(boolean foundWhiteSpace) { this.foundWhiteSpace = foundWhiteSpace; } /** * @return Indicate if whites space has to be removed instead of replaced by a unique space. */ public boolean isRemoveWhiteSpaces() { return this.removeWhiteSpaces; } /** * @param removeWhiteSpaces Indicate if whites space has to be removed instead of replaced by a unique space. */ public void setRemoveWhiteSpaces(boolean removeWhiteSpaces) { this.removeWhiteSpaces = removeWhiteSpaces; } /** * @return Used to store velocity directive whish does not generate output until something else is found. It's * used to match a whole group of white space including no output velocity code. */ public StringBuffer getWsGroup() { return this.wsGroup; } } }