/** * Aptana Studio * Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the GNU Public License (GPL) v3 (with exceptions). * Please see the license.html included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ package com.aptana.editor.sass; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.StringTokenizer; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.rules.ICharacterScanner; import org.eclipse.jface.text.rules.IRule; import org.eclipse.jface.text.rules.IToken; import org.eclipse.jface.text.rules.IWordDetector; import org.eclipse.jface.text.rules.Token; import org.eclipse.jface.text.rules.WordRule; import com.aptana.core.util.StringUtil; import com.aptana.editor.common.text.rules.CharacterMapRule; import com.aptana.editor.common.text.rules.ExtendedWordRule; import static com.aptana.editor.css.CSSCodeScannerFlex.*; import com.aptana.editor.css.CSSCodeScannerFlex; import com.aptana.editor.css.CSSCodeScannerRuleBased; import com.aptana.editor.css.parsing.lexer.CSSTokenType; /** * @author Chris Williams */ @SuppressWarnings("deprecation") public class SassCodeScanner extends CSSCodeScannerRuleBased { private IToken lastToken; private IDocument _document; @Override protected List<IRule> createRules() { List<IRule> rules = super.createRules(); // Stick in a rule that recognizes mixins and variables ExtendedWordRule variableRule = new VariableWordRule(new VariableWordDetector(), createToken(ISassConstants.VARIABLE_SCOPE), true); ExtendedWordRule wordRule = new ExtendedWordRule(new IWordDetector() { public boolean isWordStart(char c) { return Character.isLetter(c)||c=='-'; } public boolean isWordPart(char c) { return Character.isLetterOrDigit(c) || c == '-' || c == '_'; } },createToken(IDocument.DEFAULT_CONTENT_TYPE),true) { @Override protected IToken getWordToken(String word) { if (HTML_TAGS.contains(word)) { return new Token(CSSTokenType.ELEMENT.getScope()); } else if (CSSCodeScannerFlex.FUNCTIONS.contains(word)) { return new Token(CSSTokenType.FUNCTION.getScope()); } else if (PROPERTY_NAMES.contains(word)) { return new Token(CSSTokenType.PROPERTY.getScope()); } else if (PROPERTY_VALUES.contains(word)) { return new Token(CSSTokenType.VALUE.getScope()); } else if (PSEUDOS.contains(word)) { return new Token(CSSTokenType.CLASS.getScope()); } else if (STANDARD_COLORS.contains(word)) { return new Token(CSSTokenType.COLOR.getScope()); } // slower ones as last else if (FONT_NAMES.contains(word)) { return new Token(CSSTokenType.FONT.getScope()); } else if (HTML_ATTRIBUTE.contains(word)) { return new Token(CSSTokenType.PROPERTY.getScope()); } else if (CSSCodeScannerFlex.MEDIA_KEYWORDS.contains(word) || KEYFRAME_KEYWORDS.contains(word)) { return new Token(CSSTokenType.PROPERTY.getScope()); } return super.getWordToken(word); } }; rules.add(1, variableRule); rules.add(0,wordRule); return rules; } @SuppressWarnings("nls") @Override protected WordRule createAtWordsRule() { WordRule wordRule = super.createAtWordsRule(); wordRule.addWord("@mixin", createToken(ISassConstants.KEYWORD_CONTROL_AT_RULE_MIXIN_SCOPE)); wordRule.addWord("@include", createToken(ISassConstants.KEYWORD_CONTROL_AT_RULE_INCLUDE_SCOPE)); wordRule.addWord("@function", createToken(ISassConstants.KEYWORD_CONTROL_AT_RULE_FUNCTION_SCOPE)); wordRule.addWord("@while", createToken(ISassConstants.KEYWORD_CONTROL_AT_RULE_WHILE_SCOPE)); wordRule.addWord("@each", createToken(ISassConstants.KEYWORD_CONTROL_AT_RULE_EACH_SCOPE)); wordRule.addWord("@for", createToken(ISassConstants.KEYWORD_CONTROL_AT_RULE_FOR_SCOPE)); wordRule.addWord("@if", createToken(ISassConstants.KEYWORD_CONTROL_AT_RULE_IF_SCOPE)); wordRule.addWord("@warn", createToken(ISassConstants.KEYWORD_CONTROL_AT_RULE_WARN_SCOPE)); wordRule.addWord("@debug", createToken(ISassConstants.KEYWORD_CONTROL_AT_RULE_DEBUG_SCOPE)); wordRule.addWord("@extend", createToken(ISassConstants.KEYWORD_CONTROL_AT_RULE_EXTEND_SCOPE)); return wordRule; } @Override public IToken nextToken() { IToken token = super.nextToken(); if (token.isWhitespace()) { return token; } if (token.getData() instanceof String && ((String) token.getData()).contains(".css")) //$NON-NLS-1$ { String cssScopeName = ((String) token.getData()); String sassScopeName = cssScopeName.replaceAll("\\.css", "\\.sass"); //$NON-NLS-1$ //$NON-NLS-2$ token = new Token(sassScopeName); } // FIXME If token is "meta.selector.sass", then it's whitespace. Check if it contains a newline. If so, make it // Token.WHITESPACE if not preceded by a comma! if (lastToken != null && !ISassConstants.PUNCTUATION_SEPARATOR_SCOPE.equals(lastToken.getData()) && (ISassConstants.META_SELECTOR_SCOPE.equals(token.getData()) || ISassConstants.META_PROPERTY_VALUE_SCOPE .equals(token.getData()))) { String src = getSource(getTokenOffset(), getTokenLength()); if (src.contains("\n") || src.contains("\r")) //$NON-NLS-1$ //$NON-NLS-2$ // $codepro.audit.disable platformSpecificLineSeparator { if (ISassConstants.META_SELECTOR_SCOPE.equals(token.getData())) { fInSelector = false; } else if (ISassConstants.META_PROPERTY_VALUE_SCOPE.equals(token.getData())) { fInPropertyValue = false; } token = Token.WHITESPACE; } } else if (lastToken != null && (ISassConstants.KEYWORD_CONTROL_AT_RULE_MIXIN_SCOPE.equals(lastToken.getData()) || ISassConstants.KEYWORD_CONTROL_AT_RULE_INCLUDE_SCOPE .equals(lastToken.getData()))) { token = new Token(ISassConstants.ENTITY_NAME_FUNCTION_SCOPE); } if (token.isOther()) { lastToken = token; } return token; } private String getSource(int tokenOffset, int tokenLength) { try { return _document.get(tokenOffset, tokenLength); } catch (BadLocationException e) { return StringUtil.EMPTY; } } @Override protected CharacterMapRule createPunctuatorsRule() { CharacterMapRule rule = super.createPunctuatorsRule(); // Override equals rule.add('=', createToken(ISassConstants.PUNCTUATION_DEFINITION_ENTITY_SCOPE)); return rule; } /** * Here we override the array of static property names from CSS and make ones that have "namespaces" (as Sass calls * them) also get split up so we recognize the second half (i.e. we recognize both "font-family" as well as "font" * and "family" individually). */ @Override protected String[] getPropertyNames() { String[] origCSS = super.getPropertyNames(); Set<String> namespaced = new HashSet<String>(); for (String name : origCSS) { StringTokenizer tokenizer = new StringTokenizer(name, "-"); //$NON-NLS-1$ while (tokenizer.hasMoreTokens()) namespaced.add(tokenizer.nextToken()); namespaced.add(name); } List<String> list = new ArrayList<String>(namespaced); Collections.sort(list, new Comparator<String>() { public int compare(String o1, String o2) { return o2.length() - o1.length(); } }); return list.toArray(new String[list.size()]); } @Override public void setRange(IDocument document, int offset, int length) { this.lastToken = null; this._document = document; super.setRange(document, offset, length); } // FIXME This rule doesn't properly set the first char (!, =, or +) to it's own different punctuation token type private static final class VariableWordRule extends ExtendedWordRule { private VariableWordRule(IWordDetector detector, IToken defaultToken, boolean ignoreCase) { super(detector, defaultToken, ignoreCase); } @Override protected boolean wordOK(String word, ICharacterScanner scanner) { return word.length() >= 2; } } private static class VariableWordDetector implements IWordDetector { public boolean isWordPart(char c) { return Character.isLetterOrDigit(c) || c == '-' || c == '_'; } public boolean isWordStart(char c) { // Old SASS used !, = and + as prefixes for variables and mixins, keep them in for now return c == '!' || c == '$' || c == '=' || c == '+'; } } }