/** * Copyright (c) 2006-2012 Cloudsmith Inc. and other contributors, as listed below. * 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: * Cloudsmith * */ package org.cloudsmith.xtext.dommodel.formatter.css; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.Set; import org.cloudsmith.xtext.dommodel.DomModelUtils; import org.cloudsmith.xtext.dommodel.IDomNode; import org.cloudsmith.xtext.dommodel.IDomNode.NodeType; import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.EObject; import com.google.common.base.Joiner; import com.google.common.collect.Iterators; import com.google.common.collect.Sets; /** * Style Select is used to produce a configured Selector. * */ public class Select { /** * A compound rule, where all rules must be satisfied. * */ public static class And extends Selector { private int specificity = 0; private Selector[] selectors; public And(Selector... selectors) { this.selectors = selectors; } /** * Important - two And selectors are considered equal only if they have the rules in the same order. * The correctness of this can be discussed. */ @Override public boolean equalMatch(Selector selector) { if(!(selector instanceof And)) return false; And a = (And) selector; if(selectors.length != a.selectors.length) return false; for(int i = selectors.length; i >= 0; i--) if(!selectors[i].equalMatch(a.selectors[i])) return false; return true; } @Override public int getSpecificity() { if(specificity != 0) return specificity; for(int i = 0; i < selectors.length; i++) specificity += selectors[i].getSpecificity(); return specificity; } @Override public boolean matches(IDomNode node) { if(node == null) return false; for(int i = 0; i < selectors.length; i++) if(!selectors[i].matches(node)) return false; return true; } @Override public String toString() { StringBuilder builder = new StringBuilder(); Joiner joiner = Joiner.on(", "); builder.append("and("); joiner.appendTo(builder, selectors); builder.append(")"); return builder.toString(); } } /** * Matches containment where each selector must match an containing node. * The interpretation allows for "holes" - i.e. the rule (==A ==C) matches the containment * in the context (X Y A B C node) since node is contained in a C, that in turn is contained * in an A). This is similar to how the CSS containment rule works. */ public static class Containment extends Selector { private int specificity = 0; private Selector[] selectors; /** * Selectors for containers - the nearest container first * * @param selectors */ public Containment(Selector... selectors) { if(selectors == null || selectors.length < 1) throw new IllegalArgumentException("no selectors specified"); this.selectors = selectors; } @Override public boolean equalMatch(Selector selector) { if(!(selector instanceof Containment)) return false; Containment c = (Containment) selector; if(selectors.length != c.selectors.length) return false; for(int i = selectors.length; i >= 0; i--) if(!selectors[i].equalMatch(c.selectors[i])) return false; return true; } @Override public int getSpecificity() { if(specificity != 0) return specificity; for(int i = 0; i < selectors.length; i++) specificity += selectors[i].getSpecificity(); return specificity; } /** * context vector has nearest container first */ @Override public boolean matches(IDomNode node) { if(node == null) return false; IDomNode[] context = Iterators.toArray(node.parents(), IDomNode.class); int startIdx = 0; int matchCount = 0; for(int si = 0; si < selectors.length; si++) { for(int ci = startIdx; ci < context.length; ci++) if(selectors[si].matches(context[ci])) { // match startIdx = ci + 1; // next rule must match context further away matchCount++; // one match ok break; } } // match if all containment rules where satisfied return (matchCount == selectors.length) ? true : false; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("containment("); Joiner joiner = Joiner.on(", "); joiner.appendTo(builder, selectors); return builder.toString(); } } /** * A selector that is useful when debugging matching (wrap a real selector and set a breakpoint in {@link #matches(IDomNode)}. */ public static class DebugSelector extends Selector { private Selector wrappedSelector; public DebugSelector(Selector wrappedSelector) { this.wrappedSelector = wrappedSelector; } @Override public boolean equalMatch(Selector s) { return wrappedSelector.equalMatch(s); } @Override public int getSpecificity() { return wrappedSelector.getSpecificity(); } @Override public boolean matches(IDomNode node) { boolean matches = false; if(node != null) matches = wrappedSelector.matches(node); return matches; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("debug("); builder.append(wrappedSelector.toString()); builder.append(")"); return builder.toString(); } } /** * Selects on grammar element - may contain one or more grammar elements. * Selector matches if a DOM node is associated with one of the grammar elements. */ public static class GrammarSelector extends Selector { private Set<EObject> matchGrammar; GrammarSelector(EObject... grammarElements) { matchGrammar = Sets.newHashSet(grammarElements); } GrammarSelector(Iterable<? extends EObject> grammarElements) { matchGrammar = Sets.newHashSet(grammarElements); } @Override public boolean equalMatch(Selector selector) { if(selector instanceof GrammarSelector == false) return false; return matchGrammar.equals(((GrammarSelector) selector).matchGrammar); } @Override public int getSpecificity() { return GRAMMARSPECIFICITY; } @Override public boolean matches(IDomNode node) { if(node == null) return false; return matchGrammar.contains(node.getGrammarElement()); } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("grammar("); builder.append(matchGrammar.toString()); builder.append(")"); return builder.toString(); } } /** * The next to most specific selector that matches anything. * Should be combined with And selector to make the and specificity very high. * */ public static class Important extends Selector { public Important() { } @Override public boolean equalMatch(Selector selector) { return selector instanceof Important; } @Override public int getSpecificity() { return IMPORTANT_SPECIFICITY; } @Override public boolean matches(IDomNode node) { return true; } @Override public String toString() { return "imortant()"; } } /** * The most specific selector that matches on a instance using ==. * */ public static class Instance extends Selector { private final IDomNode node; public Instance(IDomNode node) { this.node = node; } @Override public boolean equalMatch(Selector selector) { if(!(selector instanceof Instance)) return false; return this.node == ((Instance) selector).node; } @Override public int getSpecificity() { return MAX_SPECIFICITY; } @Override public boolean matches(IDomNode node) { return this.node == node; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("instance("); builder.append(node.getNodeType()); builder.append(", "); builder.append(node.hashCode()); builder.append(")"); return builder.toString(); } } /** * Matches IDomNodes on NodeType, style classifier, and id. * */ public static class NodeSelector extends Selector { private Set<NodeType> matchingNodeTypes; private Set<Object> matchingClassifiers; private Object matchingId; private int specificity = 0; private static String NoIdMatch = ""; /** * Selects all/any node. */ public NodeSelector() { this(NodeType.anySet, null, null); } /** * Node must have all the given style classifiers. * * @param styleClasses */ public NodeSelector(Collection<Object> styleClasses) { this(NodeType.anySet, styleClasses, null); } /** * Node must have all of the given style classifiers and the given id. * * @param styleClasses * @param id */ public NodeSelector(Collection<Object> styleClasses, Object id) { this(NodeType.anySet, styleClasses, id); } /** * Node must have the given node type * * @param t */ public NodeSelector(NodeType t) { this(EnumSet.of(t), null, null); } /** * Node must have the given node type and all of the given style classifiers. * * @param nodeType * @param styleClasses */ public NodeSelector(NodeType nodeType, Collection<Object> styleClasses) { this(EnumSet.of(nodeType), styleClasses, null); } /** * Node must have the given node type, and all of the give style classifiers and the given id. * * @param nodeType * @param styleClasses * @param id */ public NodeSelector(NodeType nodeType, Collection<Object> styleClasses, Object id) { this(EnumSet.of(nodeType), styleClasses, id); } /** * Node must have the given node type, and the given style classifier * (may have other style classes assigned), and the given id). * * @param nodeType * @param styleClass * @param id */ public NodeSelector(NodeType nodeType, Object styleClass, Object id) { this(EnumSet.of(nodeType), Sets.newHashSet(styleClass), id); } /** * Node must have all the given node statuses (may have other node status set). * * @param nodeTypes */ public NodeSelector(Set<NodeType> nodeTypes) { this(nodeTypes, null, null); } /** * Node must have the given node statuses (may have other status set), and all of the given style classes. * * @param nodeTypes * @param styleClasses */ public NodeSelector(Set<NodeType> nodeTypes, Collection<Object> styleClasses) { this(nodeTypes, styleClasses, null); } /** * Node must have all the given node statuses (may have other status set), and all of the given style classes * and the given id. * * @param nodeTypes * - may be null (any type) * @param styleClass * - may be null (any class) * @param id * - may be null (any id) */ public NodeSelector(Set<NodeType> nodeTypes, Collection<Object> styleClass, Object id) { if(nodeTypes == null) { nodeTypes = EnumSet.noneOf(NodeType.class); } this.matchingNodeTypes = nodeTypes; this.matchingClassifiers = Sets.newHashSet(); if(styleClass != null) this.matchingClassifiers.addAll(styleClass); this.matchingId = id != null ? id : NoIdMatch; } @Override public boolean equalMatch(Selector selector) { if(!(selector instanceof NodeSelector)) return false; NodeSelector e = (NodeSelector) selector; if(!matchingNodeTypes.equals(e.matchingNodeTypes)) return false; if(!(matchingClassifiers.size() == e.matchingClassifiers.size() && e.matchingClassifiers.containsAll(matchingClassifiers))) return false; if(!matchingId.equals(e.matchingId)) return false; return true; } @Override public int getSpecificity() { if(specificity > 0) return specificity; specificity = 0; if(matchingId != NoIdMatch) specificity += IDSPECIFICITY; // NOTE: the number per matching class must be bigger than NodeStatus.numberOfValues specificity += matchingClassifiers.size() * CLASSSPECIFICITY; switch(matchingNodeTypes.size()) { case 0: // will never matching anything - specificity does not matter break; case 1: // matches one type specificity += 2; break; case NodeType.numberOfValues: // matches all types (not very specific at all) break; default: // matches several but not all types (less specific than matching one) specificity += 1; } return specificity; } @Override public boolean matches(IDomNode node) { if(node == null) return false; if(!matchingNodeTypes.contains(node.getNodeType())) return false; // i.e styleClass && styleClass && ... if(matchingClassifiers.size() > 0 && !node.getStyleClassifiers().containsAll(matchingClassifiers)) return false; if(matchingId != NoIdMatch && !matchingId.equals(node.getNodeId())) return false; return true; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("node(type("); Joiner typeJoiner = Joiner.on(" || "); typeJoiner.appendTo(builder, matchingNodeTypes); builder.append(")"); if(matchingClassifiers.size() > 0) { builder.append(", class("); Joiner classJoiner = Joiner.on(" && "); classJoiner.appendTo(builder, matchingClassifiers); builder.append(")"); } if(matchingId != NoIdMatch) { builder.append(", id("); builder.append(matchingId); builder.append(")"); } builder.append(")"); return builder.toString(); } } /** * Negates a selector and increases the specificity by one. * e.g. selection of not(a) is more important than selection of just a. * Not does not select a null node. * */ public static class Not extends Selector { private Selector selector; public Not(Selector selector) { this.selector = selector; } @Override public boolean equalMatch(Selector selector) { if(!(selector instanceof Not)) return false; Not notSelector = (Not) selector; return this.selector.equalMatch(notSelector.selector); } @Override public int getSpecificity() { return this.selector.getSpecificity() + 1; } @Override public boolean matches(IDomNode node) { if(node == null) return false; return !this.selector.matches(node); } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("not("); builder.append(selector.toString()); builder.append(")"); return builder.toString(); } } /** * A NullSelector is only useful in a NullRule. It matches nothing. * */ public static class NullSelector extends Selector { @Override public boolean equalMatch(Selector selector) { return false; } @Override public int getSpecificity() { return 0; } @Override public boolean matches(IDomNode node) { return false; } @Override public String toString() { return "nullSelector()"; } } /** * Applies the delegate selector to the parent of the node being matched. * */ public static class ParentSelector extends Selector { private Selector parentSelector; public ParentSelector(Selector parentSelector) { this.parentSelector = parentSelector; } @Override public boolean equalMatch(Selector s) { if(!(s instanceof ParentSelector)) return false; return parentSelector.equalMatch(((ParentSelector) s).parentSelector); } @Override public int getSpecificity() { return parentSelector.getSpecificity(); } @Override public boolean matches(IDomNode node) { if(node == null) return false; return parentSelector.matches(node.getParent()); } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("parent("); builder.append(parentSelector.toString()); builder.append(")"); return builder.toString(); } } public static class PredecessorSelector extends Selector { private Selector predecessorSelector; public PredecessorSelector(Selector predecessorSelector) { this.predecessorSelector = predecessorSelector; } @Override public boolean equalMatch(Selector s) { if(!(s instanceof SuccessorSelector)) return false; return predecessorSelector.equalMatch(((PredecessorSelector) s).predecessorSelector); } @Override public int getSpecificity() { return predecessorSelector.getSpecificity(); } @Override public boolean matches(IDomNode node) { if(node == null) return false; return predecessorSelector.matches(DomModelUtils.previousToken(node)); } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("predecessor("); builder.append(predecessorSelector.toString()); builder.append(")"); return builder.toString(); } } public static abstract class Selector { public static final int MAX_SPECIFICITY = Integer.MAX_VALUE; public static final int IMPORTANT_SPECIFICITY = 50000; public static final int IDSPECIFICITY = 15000; public static final int SEMANTICSPECIFICITY = 13000; public static final int CLASSSPECIFICITY = 120; public static final int GRAMMARSPECIFICITY = 100; static { assert (GRAMMARSPECIFICITY > NodeType.numberOfValues); assert (IDSPECIFICITY + 100 * CLASSSPECIFICITY + GRAMMARSPECIFICITY + NodeType.numberOfValues < IMPORTANT_SPECIFICITY); assert (IDSPECIFICITY > GRAMMARSPECIFICITY); assert (CLASSSPECIFICITY > GRAMMARSPECIFICITY); assert (CLASSSPECIFICITY - GRAMMARSPECIFICITY > NodeType.numberOfValues); } public And and(Selector selector) { return new And(this, selector); } public abstract boolean equalMatch(Selector selector); /** * Returns the specificity in the same style as used in CSS: * 100 * id count + 10 * class count + 1 * other count * * @return */ public abstract int getSpecificity(); public abstract boolean matches(IDomNode node); public Rule withStyle(IStyle<? extends Object> styles) { return new Rule(this, StyleSet.withStyles(styles)); } public Rule withStyle(StyleSet styleMap) { return new Rule(this, styleMap); } public Rule withStyles(IStyle<?>... styles) { return new Rule(this, StyleSet.withStyles(styles)); } } /** * A Selector matching on semantic information * */ public static class SemanticSelector extends Selector { private EClass eClass; private boolean instance; private boolean nearest; public SemanticSelector(EClass eClass, boolean nearest, boolean instance) { this.eClass = eClass; this.instance = instance; this.nearest = nearest; } @Override public boolean equalMatch(Selector s) { if(!(s instanceof SemanticSelector)) return false; SemanticSelector other = (SemanticSelector) s; return instance == other.instance && nearest == other.nearest && eClass.equals(other.eClass); } @Override public int getSpecificity() { return SEMANTICSPECIFICITY; } @Override public boolean matches(IDomNode node) { if(node == null) return false; EObject semantic = nearest ? node.getNearestSemanticObject() : node.getSemanticObject(); if(semantic == null) return false; if(instance) return eClass.isInstance(semantic); return eClass.isSuperTypeOf(semantic.eClass()); } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("semantic("); builder.append(eClass.getName()); builder.append(", "); builder.append(nearest ? "nearest" : "assigned"); builder.append(", "); builder.append(instance ? "instance" : "typeof"); builder.append(")"); return builder.toString(); } } /** * Applies the delegate selector to the successor of the matching node. * */ public static class SuccessorSelector extends Selector { private Selector successorSelector; public SuccessorSelector(Selector successorSelector) { this.successorSelector = successorSelector; } @Override public boolean equalMatch(Selector s) { if(!(s instanceof SuccessorSelector)) return false; return successorSelector.equalMatch(((SuccessorSelector) s).successorSelector); } @Override public int getSpecificity() { return successorSelector.getSpecificity(); } @Override public boolean matches(IDomNode node) { if(node == null) return false; return successorSelector.matches(DomModelUtils.nextToken(node)); } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("successor("); builder.append(successorSelector.toString()); builder.append(")"); return builder.toString(); } } /** * Selects matching text. * */ public static class Text extends Selector { private final String text; public Text(String text) { if(text == null) throw new IllegalArgumentException("text selector can not be null"); this.text = text; } @Override public boolean equalMatch(Selector selector) { if(!(selector instanceof Text)) return false; return this.text == ((Text) selector).text; } @Override public int getSpecificity() { return 1; } @Override public boolean matches(IDomNode node) { if(node == null) return false; return this.text.equals(node.getText()); } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("text('"); builder.append(text); builder.append("')"); return builder.toString(); } } /** * Selects a node that is matched by the given nodeSelector if the node's predecessor matches * the given predecessor selector. * * @param nodeSelector * @param predecessor * @return */ public static Select.And after(Selector nodeSelector, Selector predecessor) { return new And(nodeSelector, new PredecessorSelector(predecessor)); } public static Select.And and(Select.Selector... selectors) { return new Select.And(selectors); } public static Select.NodeSelector any() { return new Select.NodeSelector(); } public static Select.NodeSelector any(Object styleClass) { return new Select.NodeSelector(Sets.newHashSet(styleClass)); } public static Select.NodeSelector any(Object styleClass, String id) { return new Select.NodeSelector(Sets.newHashSet(styleClass), id); } /** * Selects a node matching the given nodeSelector, if this node is before a node matching the * given successor selector. * * @param nodeSelector * @param successor * @return */ public static Select.And before(Selector nodeSelector, Selector successor) { return new And(nodeSelector, new SuccessorSelector(successor)); } public static Select.And between(Selector predecessor, Selector successor) { return new And(new PredecessorSelector(predecessor), new SuccessorSelector(successor)); } public static Select.And between(Selector nodeSelector, Selector predecessor, Selector successor) { return new And(nodeSelector, new And(new PredecessorSelector(predecessor), new SuccessorSelector(successor))); } public static Select.NodeSelector comment() { return new Select.NodeSelector(NodeType.COMMENT); } /** * Matches containment where each selector must match an containing node. * The interpretation allows for "holes" - i.e. the rule (==A ==C) matches the containment * in the context (X Y A B C node) since node is contained in a C, that in turn is contained * in an A). This is similar to how the CSS containment rule works. */ public static Select.Containment containment(Select.Selector... selectors) { return new Select.Containment(selectors); } public static Select.GrammarSelector grammar(Collection<? extends EObject> grammarElements) { return new Select.GrammarSelector(grammarElements); } public static Select.GrammarSelector grammar(EObject... grammarElements) { return new Select.GrammarSelector(grammarElements); } public static Select.GrammarSelector grammar(Iterable<? extends EObject> grammarElements) { return new Select.GrammarSelector(grammarElements); } public static Selector important() { return new Important(); } public static Selector important(Selector s) { return new And(new Important(), s); } public static Select.Instance instance(IDomNode x) { return new Select.Instance(x); } public static Select.And keyword(String text) { return new Select.And(new NodeSelector(NodeType.KEYWORD), new Text(text)); } /** * Select node with a nearest semantic object with given clazz, or a supertype of given clazz. * * @param clazz * @return */ public static Select.SemanticSelector nearestSemantic(EClass clazz) { return new SemanticSelector(clazz, true, false); } /** * Select node with a nearest semantic object being an instance of the given clazz. * * @param clazz * @return */ public static Select.SemanticSelector nearestSemanticInstance(EClass clazz) { return new SemanticSelector(clazz, true, true); } public static Select.NodeSelector node(Collection<Object> styleClasses) { return new Select.NodeSelector(styleClasses, null); } public static Select.NodeSelector node(Collection<Object> styleClasses, String id) { return new Select.NodeSelector(styleClasses, id); } public static Select.NodeSelector node(NodeType type) { return new Select.NodeSelector(type); } public static Select.NodeSelector node(NodeType type, Collection<String> styleClasses, String id) { return new Select.NodeSelector(type, styleClasses, id); } public static Select.NodeSelector node(NodeType type, String styleClass) { return new Select.NodeSelector(type, Collections.singleton(styleClass), null); } public static Select.NodeSelector node(NodeType type, String styleClass, String id) { return new Select.NodeSelector(type, Collections.singleton(styleClass), id); } public static Select.NodeSelector node(Object styleClass) { return new Select.NodeSelector(Collections.singleton(styleClass), null); } public static Select.NodeSelector node(Object styleClass, String id) { return new Select.NodeSelector(Collections.singleton(styleClass), id); } public static Select.Not not(Select.Selector selector) { return new Select.Not(selector); } public static Select.NullSelector nullSelector() { return new Select.NullSelector(); } public static Select.ParentSelector parent(Selector selector) { return new Select.ParentSelector(selector); } /** * Select node with a direct semantic object with given clazz, or a supertype of given clazz. * * @param clazz * @return */ public static Select.SemanticSelector semantic(EClass clazz) { return new SemanticSelector(clazz, false, false); } /** * Select node with a direct semantic object with given clazz. * * @param clazz * @return */ public static Select.SemanticSelector semanticInstance(EClass clazz) { return new SemanticSelector(clazz, false, true); } public static Select.NodeSelector whitespace() { return new Select.NodeSelector(NodeType.WHITESPACE); } public static Selector whitespaceAfter(Selector predecessor) { return new And(new Select.NodeSelector(NodeType.WHITESPACE), new PredecessorSelector(predecessor)); } public static Selector whitespaceBefore(Selector succcessor) { return new And(new Select.NodeSelector(NodeType.WHITESPACE), new SuccessorSelector(succcessor)); } }