/* * 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.transformation.icon; import java.io.StringReader; import java.util.Collections; import java.util.List; import java.util.Map; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; 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.block.Block; import org.xwiki.rendering.block.SpecialSymbolBlock; import org.xwiki.rendering.block.WordBlock; import org.xwiki.rendering.block.XDOM; import org.xwiki.rendering.internal.block.ProtectedBlockFilter; import org.xwiki.rendering.parser.ParseException; import org.xwiki.rendering.parser.Parser; import org.xwiki.rendering.transformation.AbstractTransformation; import org.xwiki.rendering.transformation.TransformationContext; import org.xwiki.rendering.transformation.TransformationException; import org.xwiki.rendering.transformation.icon.IconTransformationConfiguration; import org.xwiki.rendering.util.IconProvider; import org.xwiki.rendering.util.ParserUtils; import org.xwiki.text.StringUtils; /** * Transforms some special characters representing icons into images. For example transforms {@code :)} characters into * a smiley. * * @version $Id: 855ddb248cf2a0c9e0628ace18b06b976d891993 $ * @since 2.6RC1 */ @Component @Named("icon") @Singleton public class IconTransformation extends AbstractTransformation implements Initializable { private static final String PARAMETER_KEY = "is-an-icon"; private static final Map ICON_PARAMETER = Collections.singletonMap(PARAMETER_KEY, "true"); /** * Used to get the icon mapping information (suite of characters mapped to an icon name). */ @Inject private IconTransformationConfiguration configuration; /** * Used to get the icon representation. */ @Inject private IconProvider iconProvider; /** * Used to parse the mapping suite of characters into a XDOM tree for fast matching. */ @Inject @Named("plain/1.0") private Parser plainTextParser; /** * The logger to log. */ @Inject private Logger logger; /** * Used to remove the top level paragraph since we don't currently have an inline parser. */ private ParserUtils parserUtils = new ParserUtils(); /** * The computed tree used to perform the fast mapping. */ private XDOM mappingTree; /** * Used to filter protected blocks (code macro marker block, etc). */ private ProtectedBlockFilter filter = new ProtectedBlockFilter(); @Override public void initialize() throws InitializationException { this.mappingTree = new XDOM(Collections.emptyList()); // Transform mappings into Blocks for (Map.Entry entry : this.configuration.getMappings().entrySet()) { if (!StringUtils.isEmpty((String) entry.getValue())) { try { XDOM xdom = this.plainTextParser.parse(new StringReader((String) entry.getKey())); // Remove top level paragraph this.parserUtils.removeTopLevelParagraph(xdom.getChildren()); mergeTree(this.mappingTree, convertToDeepTree(xdom, (String) entry.getValue())); } catch (ParseException e) { this.logger.warn("Failed to parse icon symbols [" + entry.getKey() + "]. Reason = [" + e.getMessage() + "]"); } } } } @Override public void transform(Block source, TransformationContext context) throws TransformationException { List filteredBlocks = this.filter.filter(source.getChildren()); if (!this.mappingTree.getChildren().isEmpty() && !filteredBlocks.isEmpty()) { parseTree(filteredBlocks); } } /** * Converts a standard XDOM tree into a deep tree: sibling are transformed into parent/child relationships and the * leaf node is an Icon node referencing the passed icon name. * * @param sourceTree the source tree to modify * @param iconName the name of the icon to display when a match is found * @return the modified tree */ private Block convertToDeepTree(Block sourceTree, String iconName) { XDOM targetTree = new XDOM(Collections.emptyList()); Block pointer = targetTree; for (Block block : sourceTree.getChildren()) { pointer.addChild(block); pointer = block; } // Add an icon block as the last block Block iconBlock = iconProvider.get(iconName); iconBlock.setParameters(ICON_PARAMETER); pointer.addChild(iconBlock); return targetTree; } /** * Merged two XDOM trees. * * @param targetTree the tree to merge into * @param sourceTree the tree to merge */ private void mergeTree(Block targetTree, Block sourceTree) { for (Block block : sourceTree.getChildren()) { // Check if the current block exists in the target tree at the same place in the tree int pos = indexOf(targetTree.getChildren(), block); if (pos > -1) { Block foundBlock = targetTree.getChildren().get(pos); mergeTree(foundBlock, block); } else { targetTree.addChild(block); } } } /** * Shallow indexOf implementation that only compares nodes based on their data and not their children data. * * @param targetBlocks the list of blocks to look into * @param block the block to look for in the list of passed blocks * @return the position of the block in the list of target blocks and -1 if not found */ private int indexOf(List targetBlocks, Block block) { int pos = 0; for (Block targetBlock : targetBlocks) { // Test a non deep equality if (blockEquals(targetBlock, block)) { return pos; } pos++; } return -1; } /** * Compares two nodes in a shallow manner (children data are not compared). * * @param target the target node to compare * @param source the source node to compare * @return true if the two blocks are equals in a shallow manner (children data are not compared) */ private boolean blockEquals(Block target, Block source) { boolean found = false; if (source instanceof SpecialSymbolBlock && target instanceof SpecialSymbolBlock) { if (((SpecialSymbolBlock) target).getSymbol() == ((SpecialSymbolBlock) source).getSymbol()) { found = true; } } else if (source instanceof WordBlock && target instanceof WordBlock) { if (((WordBlock) target).getWord().equals(((WordBlock) source).getWord())) { found = true; } } else if (source.equals(target)) { found = true; } return found; } /** * Parse a list of blocks and replace suite of Blocks matching the icon mapping definitions by image blocks. * * @param sourceBlocks the blocks to parse */ private void parseTree(List sourceBlocks) { Block matchStartBlock = null; int count = 0; Block mappingCursor = this.mappingTree.getChildren().get(0); Block sourceBlock = sourceBlocks.get(0); while (sourceBlock != null) { while (mappingCursor != null) { if (blockEquals(sourceBlock, mappingCursor)) { if (matchStartBlock == null) { matchStartBlock = sourceBlock; } count++; mappingCursor = mappingCursor.getChildren().get(0); // If we reach the Icon Block then we've found a match! if (ICON_PARAMETER.get(PARAMETER_KEY).equals(mappingCursor.getParameter(PARAMETER_KEY))) { // Replace the first source block with the icon block and remove all other blocks... for (int i = 0; i < count - 1; i++) { matchStartBlock.getParent().removeBlock(matchStartBlock.getNextSibling()); } sourceBlock = mappingCursor.clone(); matchStartBlock.getParent().replaceChild(sourceBlock, matchStartBlock); mappingCursor = null; matchStartBlock = null; count = 0; } else { // Look for next block match break; } } else { mappingCursor = mappingCursor.getNextSibling(); } } // Look for a match in children of the source block List filteredSourceBlocks = this.filter.filter(sourceBlock.getChildren()); if (!filteredSourceBlocks.isEmpty()) { parseTree(filteredSourceBlocks); } else if (mappingCursor == null) { // No match has been found, reset state variables mappingCursor = this.mappingTree.getChildren().get(0); count = 0; matchStartBlock = null; } sourceBlock = this.filter.getNextSibling(sourceBlock); } } }