/******************************************************************************* * Copyright (c) 2007, 2013 David Green and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * David Green - initial API and implementation *******************************************************************************/ package org.eclipse.mylyn.internal.wikitext.ui.editor.syntax; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentPartitioner; import org.eclipse.jface.text.ITypedRegion; import org.eclipse.jface.text.TextAttribute; import org.eclipse.jface.text.rules.IToken; import org.eclipse.jface.text.rules.ITokenScanner; import org.eclipse.mylyn.internal.wikitext.ui.WikiTextUiPlugin; import org.eclipse.mylyn.internal.wikitext.ui.editor.preferences.Preferences; import org.eclipse.mylyn.internal.wikitext.ui.editor.syntax.FastMarkupPartitioner.MarkupPartition; import org.eclipse.mylyn.internal.wikitext.ui.viewer.CssStyleManager; import org.eclipse.mylyn.internal.wikitext.ui.viewer.FontState; import org.eclipse.mylyn.wikitext.parser.css.CssParser; import org.eclipse.mylyn.wikitext.parser.css.CssRule; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.graphics.Font; /** * A token scanner that uses the results of the {@link FastMarkupPartitioner} to identify tokens. * * @author David Green */ public class MarkupTokenScanner implements ITokenScanner { private Token currentToken = null; private Iterator<Token> tokenIt = null; private CssStyleManager styleManager; private FontState defaultState; private Preferences preferences; private final CssParser cssParser = new CssParser(); public MarkupTokenScanner(Font defaultFont, Font defaultMonospaceFont) { initialize(defaultFont, defaultMonospaceFont); reloadPreferences(); } /** * Reset the fonts used by this token scanner. * * @param defaultFont * the default font, must not be null. * @param defaultMonospaceFont * the default monospace font, or null if a suitable default should be selected */ public void resetFonts(Font defaultFont, Font defaultMonospaceFont) { if (defaultFont == null) { throw new IllegalArgumentException(); } initialize(defaultFont, defaultMonospaceFont); } private void initialize(Font defaultFont, Font defaultMonospaceFont) { styleManager = new CssStyleManager(defaultFont, defaultMonospaceFont); defaultState = styleManager.createDefaultFontState(); } public void reloadPreferences() { preferences = WikiTextUiPlugin.getDefault().getPreferences(); } public int getTokenLength() { return currentToken == null ? -1 : currentToken.getLength(); } public int getTokenOffset() { return currentToken == null ? -1 : currentToken.getOffset(); } public IToken nextToken() { if (tokenIt != null && tokenIt.hasNext()) { currentToken = tokenIt.next(); } else { currentToken = null; tokenIt = null; return org.eclipse.jface.text.rules.Token.EOF; } return currentToken; } public void setRange(IDocument document, int offset, int length) { IDocumentPartitioner partitioner = document.getDocumentPartitioner(); List<Token> tokens = null; if (partitioner instanceof FastMarkupPartitioner) { FastMarkupPartitioner fastMarkupPartitioner = (FastMarkupPartitioner) partitioner; ITypedRegion[] partitioning = partitioner.computePartitioning(offset, length); if (partitioning != null) { tokens = new ArrayList<>(); ITypedRegion[] partitions = ((FastMarkupPartitioner) partitioner).getScanner() .computePartitions(document, offset, length); int lastEnd = offset; Token defaultToken; { StyleRange styleRange = styleManager.createStyleRange(defaultState, 0, 1); TextAttribute textAttribute = createTextAttribute(styleRange); defaultToken = new Token(defaultState, textAttribute, offset, length); } if (partitions != null) { for (ITypedRegion region : partitions) { if (region.getOffset() >= (offset + length)) { break; } if ((region.getOffset() + region.getLength()) < offset) { continue; } if (region instanceof MarkupPartition) { MarkupPartition partition = (MarkupPartition) region; if (lastEnd < partition.getOffset()) { Token blockBridgeToken = new Token(defaultToken.fontState, defaultToken.getData(), lastEnd, partition.getOffset() - lastEnd); addToken(tokens, blockBridgeToken); } // a token that spans the whole block Token blockToken = createToken(partition); if (blockToken == null) { blockToken = defaultToken; } if (!partition.getBlock().isSpansComputed()) { fastMarkupPartitioner.reparse(document, partition.getBlock()); } List<Span> spans = partition.getSpans(); if (spans != null) { for (Span span : spans) { if (span.getOffset() < lastEnd) { continue; } Token spanToken = createToken(blockToken.getFontState(), span); if (spanToken != null) { int blockTokenStartOffset = lastEnd < offset ? offset : lastEnd; if (blockTokenStartOffset < spanToken.getOffset()) { int blockTokenLength = spanToken.getOffset() - blockTokenStartOffset; final Token blockBridgeToken = new Token(blockToken.fontState, blockToken.getData(), blockTokenStartOffset, blockTokenLength); addToken(tokens, blockBridgeToken); } Token[] spanTokens = null; if (!span.getChildren().isEmpty()) { spanTokens = splitSpan(spanToken, span, defaultToken); } if (spanTokens != null) { for (Token spanSplitToken : spanTokens) { addToken(tokens, spanSplitToken); } } else { addToken(tokens, spanToken); } lastEnd = spanToken.offset + spanToken.length; if (lastEnd > partition.getOffset() + partition.getLength()) { throw new IllegalStateException(); } } } } final int partitionEnd = partition.getOffset() + partition.getLength(); if (lastEnd < partitionEnd) { final int realLastEnd = Math.max(lastEnd, partition.getOffset()); int diff = (partitionEnd) - realLastEnd; if (diff > 0) { int blockTokenStartOffset = realLastEnd; int blockTokenLength = diff; final Token blockBridgeToken = new Token(blockToken.fontState, blockToken.getData(), blockTokenStartOffset, blockTokenLength); addToken(tokens, blockBridgeToken); lastEnd = blockTokenStartOffset + blockTokenLength; if (lastEnd > partition.getOffset() + partition.getLength()) { throw new IllegalStateException(); } } } } } } if (lastEnd < (offset + length)) { addToken(tokens, new Token(defaultToken.fontState, defaultToken.getData(), lastEnd, length - (lastEnd - offset))); } } } currentToken = null; if (tokens == null || tokens.isEmpty()) { tokenIt = null; } else { Iterator<Token> it = tokens.iterator(); while (it.hasNext()) { Token next = it.next(); if (next.getOffset() < offset) { it.remove(); } else if (next.getOffset() + next.getLength() > (offset + length)) { it.remove(); } } tokenIt = tokens.iterator(); } } protected TextAttribute createTextAttribute(StyleRange styleRange) { int fontStyle = styleRange.fontStyle; if (styleRange.strikeout) { fontStyle |= TextAttribute.STRIKETHROUGH; } if (styleRange.underline) { fontStyle |= TextAttribute.UNDERLINE; } return new TextAttribute(styleRange.foreground, styleRange.background, fontStyle, styleRange.font); } /** * handle nested spans: given a token for a specific span, split it into one or more tokens based on analyzing its * children * * @return an array of tokens that contiguously cover the region represented by the original span. */ private Token[] splitSpan(Token spanToken, Span span, Token defaultToken) { List<Token> tokens = new ArrayList<>(span.getChildren().size() + 1); int previousEnd = spanToken.offset; for (Span child : span.getChildren().asList()) { if (child.getOffset() > previousEnd) { tokens.add(new Token(spanToken.fontState, spanToken.getData(), previousEnd, child.getOffset() - previousEnd)); } Token childToken = createToken(spanToken.fontState, child); if (childToken == null) { StyleRange styleRange = styleManager.createStyleRange(spanToken.fontState, 0, 1); TextAttribute textAttribute = createTextAttribute(styleRange); childToken = new Token(spanToken.fontState, textAttribute, child.getOffset(), child.getLength()); } if (child.getChildren().isEmpty()) { tokens.add(childToken); } else { // recursively apply to children for (Token t : splitSpan(childToken, child, defaultToken)) { tokens.add(t); } } previousEnd = child.getEndOffset(); } if (previousEnd < span.getEndOffset()) { tokens.add(new Token(spanToken.fontState, spanToken.getData(), previousEnd, span.getEndOffset() - previousEnd)); } return tokens.toArray(new Token[tokens.size()]); } private void addToken(List<Token> tokens, Token newToken) { checkAddToken(tokens, newToken); tokens.add(newToken); } private void checkAddToken(List<Token> tokens, Token newToken) { if (newToken.getLength() <= 0) { throw new IllegalStateException( NLS.bind(Messages.MarkupTokenScanner_badTokenLength, new Object[] { newToken.getLength() })); } if (newToken.getOffset() < 0) { throw new IllegalStateException( NLS.bind(Messages.MarkupTokenScanner_badTokenOffset, new Object[] { newToken.getOffset() })); } if (!tokens.isEmpty()) { Token previous = tokens.get(tokens.size() - 1); if (previous.getOffset() >= newToken.getOffset()) { throw new IllegalStateException(Messages.MarkupTokenScanner_2); } else if (previous.getOffset() + previous.getLength() > newToken.getOffset()) { throw new IllegalStateException(Messages.MarkupTokenScanner_3); } } } private Token createToken(FontState parentState, Span span) { if (span.getLength() == 0) { return null; } String cssStyles = null; String key = null; switch (span.getType()) { case BOLD: key = Preferences.PHRASE_BOLD; break; case CITATION: key = Preferences.PHRASE_CITATION; break; case CODE: key = Preferences.PHRASE_CODE; break; case DELETED: key = Preferences.PHRASE_DELETED_TEXT; break; case EMPHASIS: key = Preferences.PHRASE_EMPHASIS; break; case INSERTED: key = Preferences.PHRASE_INSERTED_TEXT; break; case ITALIC: key = Preferences.PHRASE_ITALIC; break; case MONOSPACE: key = Preferences.PHRASE_MONOSPACE; break; case QUOTE: key = Preferences.PHRASE_QUOTE; break; case SPAN: key = Preferences.PHRASE_SPAN; break; case STRONG: key = Preferences.PHRASE_STRONG; break; case SUBSCRIPT: key = Preferences.PHRASE_SUBSCRIPT; break; case SUPERSCRIPT: key = Preferences.PHRASE_SUPERSCRIPT; break; case UNDERLINED: key = Preferences.PHRASE_UNDERLINED; break; } cssStyles = preferences.getCssByPhraseModifierType().get(key); if (cssStyles == null && span.getAttributes().getCssStyle() == null && span.getChildren().isEmpty()) { return null; } FontState fontState = new FontState(parentState); if (cssStyles != null) { processCssStyles(fontState, parentState, cssStyles); } if (span.getAttributes().getCssStyle() != null) { processCssStyles(fontState, parentState, span.getAttributes().getCssStyle()); } StyleRange styleRange = styleManager.createStyleRange(fontState, 0, 1); TextAttribute textAttribute = createTextAttribute(styleRange); return new Token(fontState, textAttribute, span.getOffset(), span.getLength()); } private Token createToken(MarkupPartition partition) { if (partition.getLength() == 0) { return null; } FontState fontState = new FontState(defaultState); boolean hasStyles = processStyles(partition.getBlock(), partition, fontState); if (partition.getBlock().getAttributes().getCssStyle() != null) { processCssStyles(fontState, defaultState, partition.getBlock().getAttributes().getCssStyle()); } else { if (!hasStyles) { return null; } } StyleRange styleRange = styleManager.createStyleRange(fontState, 0, 1); TextAttribute textAttribute = createTextAttribute(styleRange); return new Token(fontState, textAttribute, partition.getOffset(), partition.getLength()); } private boolean processStyles(Block block, MarkupPartition partition, FontState fontState) { boolean hasStyles = false; if (block.getParent() != null) { hasStyles = processStyles(block.getParent(), partition, fontState); } String cssStyles = computeCssStyles(block, partition); if (cssStyles != null) { hasStyles = true; processCssStyles(fontState, defaultState, cssStyles); } return hasStyles; } private String computeCssStyles(Block block, MarkupPartition partition) { String cssStyles = null; if (block.getHeadingLevel() > 0) { cssStyles = preferences.getCssByBlockModifierType() .get(Preferences.HEADING_PREFERENCES[block.getHeadingLevel()]); } else if (block.getType() != null) { String key = null; switch (block.getType()) { case CODE: key = Preferences.BLOCK_BC; break; case QUOTE: key = Preferences.BLOCK_QUOTE; break; case PREFORMATTED: key = Preferences.BLOCK_PRE; break; case DEFINITION_TERM: key = Preferences.BLOCK_DT; break; } cssStyles = preferences.getCssByBlockModifierType().get(key); } return cssStyles; } private void processCssStyles(FontState fontState, FontState parentState, String cssStyles) { Iterator<CssRule> ruleIterator = cssParser.createRuleIterator(cssStyles); while (ruleIterator.hasNext()) { styleManager.processCssStyles(fontState, parentState, ruleIterator.next()); } } /** * public for testing purposes */ public static class Token extends org.eclipse.jface.text.rules.Token { private final int offset; private final int length; private final FontState fontState; public Token(FontState fontState, TextAttribute attribute, int offset, int length) { super(attribute); this.fontState = fontState; if (offset < 0) { throw new IllegalArgumentException(); } if (length < 0) { throw new IllegalArgumentException(); } this.offset = offset; this.length = length; } public int getOffset() { return offset; } public int getLength() { return length; } public FontState getFontState() { return fontState; } @Override public TextAttribute getData() { return (TextAttribute) super.getData(); } @Override public String toString() { return "Token [offset=" + offset + ", length=" + length + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } } }