blob: 9864cfb5181e77440a275476c72925bd339b3bbb [file] [log] [blame]
/*******************************************************************************
* 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;
case MARK:
key = Preferences.PHRASE_MARK;
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$
}
}
}