/******************************************************************************* * Copyright (c) 2004, 2008 John Krasnay 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: * John Krasnay - initial API and implementation * Dave Holroyd - Proper specificity for wildcard selector * John Austin - Implement sibling selectors *******************************************************************************/ package net.sf.vex.css; import java.io.Serializable; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.StringTokenizer; import net.sf.vex.dom.Element; import org.w3c.css.sac.AttributeCondition; import org.w3c.css.sac.CombinatorCondition; import org.w3c.css.sac.Condition; import org.w3c.css.sac.ConditionalSelector; import org.w3c.css.sac.DescendantSelector; import org.w3c.css.sac.ElementSelector; import org.w3c.css.sac.NegativeSelector; import org.w3c.css.sac.Selector; import org.w3c.css.sac.SiblingSelector; /** * Represents a pairing of a selector with a list of styles. This does * not exactly correspond to a rule in a style sheet; there is only * one selector associated with an instance of this class, whereas * multiple selectors may be associated with a style sheet rule. * * Note: <code>Rule</code> implements the <code>Comparable</code> * interface in order to be sorted by "specificity" as defined by the * CSS spec. However, this ordering is <em>not</em> consistent with * <code>equals</code> (rules with the same specificity may not be * equal). Therefore, <code>Rule</code> objects should not be used * with sorted collections or maps in the <code>java.util</code> * package, unless a suitable <code>Comparator</code> is also used. */ public class Rule implements Serializable { private static final long serialVersionUID = 1L; private byte source; private Selector selector; private List propertyDecls = new ArrayList(); /** * Class constructor. * * @param source Source of the rule. * @param selector Selector for the rule. */ public Rule(byte source, Selector selector) { this.source = source; this.selector = selector; } /** * Adds a property declaration to the rule. * * @param decl new property declaration to add */ public void add(PropertyDecl decl) { propertyDecls.add(decl); } /** * Returns the selector for the rule. */ public Selector getSelector() { return this.selector; } /** * Returns the source of this rule. * @return one of StyleSheet.SOURCE_DEFAULT, StyleSheet.SOURCE_AUTHOR, or * StyleSheet.SOURCE_USER. */ public byte getSource() { return this.source; } /** * Returns an array of the property declarations in this rule. */ public PropertyDecl[] getPropertyDecls() { return (PropertyDecl[]) this.propertyDecls.toArray(new PropertyDecl[propertyDecls.size()]); } /** * Calculates the specificity for the selector associated with * this rule. The specificity is represented as an integer whose * base-10 representation, xxxyyyzzz, can be decomposed into the * number of "id" selectors (xxx), "class" selectors (yyy), and * "element" selectors (zzz). Composite selectors result in a * recursive call. */ public int getSpecificity() { return specificity(this.getSelector()); } /** * Returns true if the given element matches this rule's selector. * * @param element Element to check. */ public boolean matches(Element element) { return matches(this.selector, element); } //==================================================== PRIVATE /** * Returns true if the given element matches the given selector. */ private static boolean matches(Selector selector, Element element) { if (element == null) { // This can happen when, e.g., with the rule "foo > *". // Since the root element matches the "*", we check if // its parent matches "foo", but of course its parent // is null return false; } String elementName = element.getName(); int selectorType = selector.getSelectorType(); switch (selectorType) { case Selector.SAC_ANY_NODE_SELECTOR: // You'd think we land here if we have a * rule, but instead // it appears we get a SAC_ELEMENT_NODE_SELECTOR with localName==null return true; case Selector.SAC_CONDITIONAL_SELECTOR: // This little wart is the product of a mismatch btn the CSS // spec an the Flute parser. CSS treats pseudo-elements as elements // attached to their parents, while Flute treats them like attributes ConditionalSelector cs = (ConditionalSelector) selector; if (cs.getCondition().getConditionType() == Condition.SAC_PSEUDO_CLASS_CONDITION) { if (element instanceof PseudoElement) { AttributeCondition ac = (AttributeCondition) cs.getCondition(); return ac.getValue().equals(element.getName()) && matches(cs.getSimpleSelector(), element.getParent()); } else { return false; } } else { return matches(cs.getSimpleSelector(), element) && matchesCondition(cs.getCondition(), element); } case Selector.SAC_ELEMENT_NODE_SELECTOR: String selectorName = ((ElementSelector)selector).getLocalName(); if (selectorName == null) { // We land here if we have a wildcard selector (*) or // a pseudocondition w/o an element name (:before) // Probably other situations too (conditional w/o element // name? e.g. [attr=value]) return true; } if (selectorName.equals(elementName)) { return true; } break; case Selector.SAC_DESCENDANT_SELECTOR: DescendantSelector ds = (DescendantSelector) selector; return matches(ds.getSimpleSelector(), element) && matchesAncestor(ds.getAncestorSelector(), element.getParent()); case Selector.SAC_CHILD_SELECTOR: DescendantSelector ds2 = (DescendantSelector) selector; Element parent = element.getParent(); if (element instanceof PseudoElement) { parent = parent.getParent(); // sigh - this looks inelegant, but whatcha gonna do? } return matches(ds2.getSimpleSelector(), element) && matches(ds2.getAncestorSelector(), parent); case Selector.SAC_DIRECT_ADJACENT_SELECTOR: SiblingSelector ss = (SiblingSelector) selector; if (element != null && element.getParent() != null && matches(ss.getSiblingSelector(), element) ) { // find next sibling final Iterator i = element.getParent().getChildIterator(); Element e = null; Element f = null; while (i.hasNext() && e != element) { f = e; e = (Element)i.next(); } if (e == element) { return matches(ss.getSelector(), f); } } return false; default: //System.out.println("DEBUG: selector type not supported"); // TODO: warning: selector not supported } return false; } /** * Returns true if some ancestor of the given element matches the given * selector. */ private static boolean matchesAncestor(Selector selector, Element element) { Element e = element; while (e != null) { if (matches(selector, e)) { return true; } e = e.getParent(); } return false; } private static boolean matchesCondition(Condition condition, Element element) { AttributeCondition acon; String attributeName; String value; switch (condition.getConditionType()) { case Condition.SAC_PSEUDO_CLASS_CONDITION: return false; case Condition.SAC_ATTRIBUTE_CONDITION: acon = (AttributeCondition)condition; value = element.getAttribute(acon.getLocalName()); if (acon.getValue() != null) { return acon.getValue().equals(value); } else { return value != null; } case Condition.SAC_ONE_OF_ATTRIBUTE_CONDITION: case Condition.SAC_CLASS_CONDITION: acon = (AttributeCondition)condition; if (condition.getConditionType() == Condition.SAC_CLASS_CONDITION) { attributeName = "class"; } else { attributeName = acon.getLocalName(); } value = element.getAttribute(attributeName); if (value == null) { return false; } StringTokenizer st = new StringTokenizer(value); while (st.hasMoreTokens()) { if (st.nextToken().equals(acon.getValue())) { return true; } } return false; case Condition.SAC_AND_CONDITION: CombinatorCondition ccon = (CombinatorCondition)condition; return matchesCondition(ccon.getFirstCondition(), element) && matchesCondition(ccon.getSecondCondition(), element); default: // TODO: warning: condition not supported System.out.println("Unsupported condition type: " + condition.getConditionType()); } return false; } /** * Calculates the specificity for a selector. */ private static int specificity(Selector sel) { if (sel instanceof ElementSelector) { if (((ElementSelector)sel).getLocalName() == null) { // actually wildcard selector -- see comment in matches() return 0; } else { return 1; } } else if (sel instanceof DescendantSelector) { DescendantSelector ds = (DescendantSelector) sel; return specificity(ds.getAncestorSelector()) + specificity(ds.getSimpleSelector()); } else if (sel instanceof SiblingSelector) { SiblingSelector ss = (SiblingSelector) sel; return specificity(ss.getSelector()) + specificity(ss.getSiblingSelector()); } else if (sel instanceof NegativeSelector) { NegativeSelector ns = (NegativeSelector) sel; return specificity(ns.getSimpleSelector()); } else if (sel instanceof ConditionalSelector) { ConditionalSelector cs = (ConditionalSelector) sel; return specificity(cs.getCondition()) + specificity(cs.getSimpleSelector()); } else { return 0; } } /** * Calculates the specificity for a condition. */ private static int specificity(Condition cond) { if (cond instanceof CombinatorCondition) { CombinatorCondition cc = (CombinatorCondition) cond; return specificity(cc.getFirstCondition()) + specificity(cc.getSecondCondition()); } else if (cond instanceof AttributeCondition) { if (cond.getConditionType() == Condition.SAC_ID_CONDITION) { return 1000000; } else { return 1000; } } else { return 0; } } }