/******************************************************************************* * Copyright (c) 2007-2016 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 implementation in Mylyn * Stephan Wahlbrink - API and implementation for DocMLET *******************************************************************************/ package de.walware.docmlet.wikitext.ui.sourceediting; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.TextAttribute; import org.eclipse.jface.text.rules.IToken; import org.eclipse.jface.text.rules.ITokenScanner; import org.eclipse.jface.text.rules.Token; import org.eclipse.mylyn.internal.wikitext.core.util.css.CssParser; import org.eclipse.mylyn.internal.wikitext.core.util.css.CssRule; 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.viewer.FontState; import org.eclipse.mylyn.wikitext.core.parser.Attributes; import org.eclipse.mylyn.wikitext.core.parser.DocumentBuilder; import org.eclipse.mylyn.wikitext.core.parser.DocumentBuilder.BlockType; import org.eclipse.mylyn.wikitext.core.parser.DocumentBuilder.SpanType; import org.eclipse.mylyn.wikitext.core.parser.Locator; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.graphics.RGB; import de.walware.ecommons.ltk.core.SourceContent; import de.walware.ecommons.preferences.PreferencesUtil; import de.walware.ecommons.text.core.treepartitioner.ITreePartitionNode; import de.walware.ecommons.text.core.treepartitioner.TreePartitionUtil; import de.walware.docmlet.wikitext.core.markup.IMarkupLanguage; import de.walware.docmlet.wikitext.core.markup.MarkupParser2; import de.walware.docmlet.wikitext.core.source.EmbeddingAttributes; import de.walware.docmlet.wikitext.core.source.MarkupLanguageDocumentSetupParticipant; import de.walware.docmlet.wikitext.core.source.extdoc.IExtdocMarkupLanguage; import de.walware.docmlet.wikitext.internal.ui.sourceediting.EmbeddedHtml; import de.walware.docmlet.wikitext.internal.ui.sourceediting.MarkupCssStyleManager; @SuppressWarnings("restriction") public class MarkupTokenScanner implements ITokenScanner { private static class PositionToken extends Token { private final int offset; private final int length; public PositionToken(final TextAttribute attribute, final int offset, final int length) { super(attribute); this.offset= offset; this.length= length; } public int getOffset() { return this.offset; } public int getLength() { return this.length; } @Override public String toString() { return "Token [offset=" + this.offset + ", length=" + this.length + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } } private static class BreakException extends RuntimeException { private static final long serialVersionUID= 1L; public BreakException() { super("BreakScan", null, true, false); } } private class Builder extends DocumentBuilder { private int beginOffset; private int endOffset; private int scanRestartOffset; private int scanCurrentOffset; private final ArrayDeque<FontState> scanFontStateStack= new ArrayDeque<>(); private FontState nestedCodeFontState; public Builder() { } private void clearBuilder() { this.scanFontStateStack.clear(); this.nestedCodeFontState= null; } public void scan(final IDocument document, final int offset, final int length) throws BadLocationException, BreakException { this.beginOffset= offset; this.endOffset= offset + length; this.scanRestartOffset= this.beginOffset; final ITreePartitionNode rootNode= TreePartitionUtil.getRootNode(document, MarkupTokenScanner.this.partitioning); ITreePartitionNode node= TreePartitionUtil.getNode(document, MarkupTokenScanner.this.partitioning, offset, false); if (node != rootNode) { while (node.getParent() != rootNode) { node= node.getParent(); } this.scanRestartOffset= node.getOffset(); } final IMarkupLanguage markupLanguage= MarkupLanguageDocumentSetupParticipant.getMarkupLanguage(document, MarkupTokenScanner.this.partitioning); final MarkupParser2 markupParser= new MarkupParser2(markupLanguage, this); markupParser.disable(MarkupParser2.GENERATIVE_CONTENT); markupParser.enable(MarkupParser2.SOURCE_STRUCT); markupParser.enable(MarkupParser2.INLINE_ALL); final SourceContent content= new SourceContent(0, document.get(this.scanRestartOffset, Math.min(this.endOffset + 100, document.getLength()) - this.scanRestartOffset), this.scanRestartOffset ); markupParser.parse(content, true); } private void updateOffset() { updateOffset(getLocator().getDocumentOffset()); } private void updateOffset(final int locatorOffset) { final int offset= this.scanRestartOffset + locatorOffset; if (offset > this.scanCurrentOffset) { addToken(this.scanFontStateStack.getLast(), this.scanCurrentOffset, Math.min(this.endOffset, offset) ); this.scanCurrentOffset= offset; } if (offset >= this.endOffset) { throw new BreakException(); } } @Override public void beginDocument() { this.scanCurrentOffset= this.beginOffset; this.scanFontStateStack.addLast(MarkupTokenScanner.this.styleManager.createDefaultFontState()); } @Override public void endDocument() { updateOffset(); } @Override public void beginBlock(final BlockType type, final Attributes attributes) { final FontState fontState; if (type == BlockType.CODE && attributes instanceof EmbeddingAttributes && ((EmbeddingAttributes) attributes).getForeignType() == IExtdocMarkupLanguage.EMBEDDED_HTML) { fontState= createHtmlFontState( this.scanFontStateStack.getLast(), (EmbeddingAttributes) attributes ); } else { fontState= createFontState( this.scanFontStateStack.getLast(), getPrefCssStyles(type), (attributes != null) ? attributes.getCssStyle() : null ); } if (type == BlockType.CODE && this.scanFontStateStack.size() > 1) { this.nestedCodeFontState= fontState; return; } updateOffset(); this.scanFontStateStack.addLast(fontState); } @Override public void endBlock() { if (this.nestedCodeFontState != null) { this.nestedCodeFontState= null; return; } updateOffset(); this.scanFontStateStack.removeLast(); } @Override public void beginSpan(final SpanType type, final Attributes attributes) { final FontState fontState; if (type == SpanType.CODE && attributes instanceof EmbeddingAttributes && ((EmbeddingAttributes) attributes).getForeignType() == IExtdocMarkupLanguage.EMBEDDED_HTML) { fontState= createHtmlFontState( this.scanFontStateStack.getLast(), (EmbeddingAttributes) attributes ); } else { fontState= createFontState( this.scanFontStateStack.getLast(), getPrefCssStyles(type), (attributes != null) ? attributes.getCssStyle() : null ); } updateOffset(); this.scanFontStateStack.addLast(fontState); } @Override public void endSpan() { updateOffset(); this.scanFontStateStack.removeLast(); } @Override public void beginHeading(final int level, final Attributes attributes) { final FontState fontState= createFontState( this.scanFontStateStack.getLast(), getPrefCssStyles(level), (attributes != null) ? attributes.getCssStyle() : null ); updateOffset(); this.scanFontStateStack.addLast(fontState); } @Override public void endHeading() { updateOffset(); this.scanFontStateStack.removeLast(); } @Override public void characters(final String text) { if (this.nestedCodeFontState != null) { updateOffset(); this.scanFontStateStack.addLast(this.nestedCodeFontState); final Locator locator= getLocator(); updateOffset(locator.getLineDocumentOffset() + locator.getLineLength()); this.scanFontStateStack.removeLast(); } } @Override public void charactersUnescaped(final String literal) { characters(literal); } @Override public void entityReference(final String entity) { characters(entity); } @Override public void image(final Attributes attributes, final String url) { } @Override public void link(final Attributes attributes, final String hrefOrHashName, final String text) { } @Override public void imageLink(final Attributes linkAttributes, final Attributes imageAttributes, final String href, final String imageUrl) { } @Override public void acronym(final String text, final String definition) { } @Override public void lineBreak() { } } private final String partitioning; private final List<PositionToken> tokens= new ArrayList<>(); private Iterator<PositionToken> tokenIter= null; private PositionToken currentToken= null; private MarkupCssStyleManager styleManager; private Preferences preferences; private final CssParser cssParser= new CssParser(); private final Builder builder= new Builder(); public MarkupTokenScanner(final String partitioning, final StyleConfig config) { this.partitioning= partitioning; setStyleConfig(config); reloadPreferences(); } /** * Sets the fonts used by this token scanner. */ public void setStyleConfig(final StyleConfig config) { this.styleManager= new MarkupCssStyleManager(config); } public void reloadPreferences() { this.preferences= WikiTextUiPlugin.getDefault().getPreferences(); } @Override public IToken nextToken() { if (this.tokenIter != null && this.tokenIter.hasNext()) { this.currentToken= this.tokenIter.next(); } else { this.currentToken= null; this.tokenIter= null; return Token.EOF; } return this.currentToken; } @Override public int getTokenOffset() { return this.currentToken.getOffset(); } @Override public int getTokenLength() { return this.currentToken.getLength(); } @Override public void setRange(final IDocument document, final int offset, final int length) { this.tokens.clear(); this.tokenIter= null; this.currentToken= null; try { this.builder.scan(document, offset, length); } catch (final BreakException e) {} catch (final BadLocationException e) { throw new RuntimeException(e); } finally { this.builder.clearBuilder(); } this.tokenIter= this.tokens.iterator(); } protected TextAttribute createTextAttribute(final 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); } protected final String getPrefCssStyles(final SpanType spanType) { final String key; switch (spanType) { 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; default: key= null; break; } return this.preferences.getCssByPhraseModifierType().get(key); } protected final String getPrefCssStyles(final BlockType blockType) { final String key; switch (blockType) { 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; default: key= null; break; } return this.preferences.getCssByBlockModifierType().get(key); } protected final String getPrefCssStyles(final int headingLevel) { final String key= Preferences.HEADING_PREFERENCES[headingLevel]; return this.preferences.getCssByBlockModifierType().get(key); } private FontState createFontState(final FontState parentState, final String prefCssStyles, final String explCssStyle) { if (prefCssStyles == null && explCssStyle == null) { return parentState; } final FontState fontState= new FontState(parentState); if (prefCssStyles != null) { processCssStyles(fontState, parentState, prefCssStyles); } if (explCssStyle != null) { processCssStyles(fontState, parentState, explCssStyle); } return fontState; } private final RGB htmlCommentColor= PreferencesUtil.getInstancePrefs().getPreferenceValue(EmbeddedHtml.HTML_COMMENT_COLOR_PREF); private final RGB htmlBackgroundColor= PreferencesUtil.getInstancePrefs().getPreferenceValue(EmbeddedHtml.HTML_BACKGROUND_COLOR_PREF); private FontState createHtmlFontState(final FontState parentState, final EmbeddingAttributes attributes) { final FontState fontState= new FontState(parentState); if ((attributes.getEmbedDescr() & IExtdocMarkupLanguage.EMBEDDED_HTML_COMMENT_FLAG) != 0) { if (this.htmlCommentColor != null) { fontState.setForeground(this.htmlCommentColor); } } else { if (this.htmlBackgroundColor != null) { fontState.setBackground(this.htmlBackgroundColor); } } return fontState; } private void processCssStyles(final FontState fontState, final FontState parentState, final String cssStyles) { final Iterator<CssRule> ruleIterator= this.cssParser.createRuleIterator(cssStyles); while (ruleIterator.hasNext()) { this.styleManager.processCssStyles(fontState, parentState, ruleIterator.next()); } } private void addToken(final FontState fontState, final int beginOffset, final int endOffset) { final StyleRange styleRange= this.styleManager.createStyleRange(fontState, beginOffset, endOffset - beginOffset); final TextAttribute textAttribute= createTextAttribute(styleRange); this.tokens.add(new PositionToken(textAttribute, beginOffset, endOffset - beginOffset)); } }