/*******************************************************************************
* 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 - Implement font-weight:bolder
* Dave Holroyd - Implement text decoration
* John Austin - More complete CSS constants. Add the colour "orange".
*******************************************************************************/
package net.sf.vex.css;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import net.sf.vex.core.FontSpec;
import net.sf.vex.dom.Element;
import org.w3c.css.sac.LexicalUnit;
/**
* Represents a CSS style sheet.
*/
public class StyleSheet implements Serializable {
/**
* Standard CSS properties.
*/
private static final IProperty[] CSS_PROPERTIES = new IProperty[] {
new DisplayProperty(),
new LineHeightProperty(),
new ListStyleTypeProperty(),
new TextAlignProperty(),
new WhiteSpaceProperty(),
new FontFamilyProperty(),
new FontSizeProperty(),
new FontStyleProperty(),
new FontWeightProperty(),
new TextDecorationProperty(),
new ColorProperty(CSS.COLOR),
new ColorProperty(CSS.BACKGROUND_COLOR),
new LengthProperty(CSS.MARGIN_BOTTOM, IProperty.AXIS_VERTICAL),
new LengthProperty(CSS.MARGIN_LEFT, IProperty.AXIS_HORIZONTAL),
new LengthProperty(CSS.MARGIN_RIGHT, IProperty.AXIS_HORIZONTAL),
new LengthProperty(CSS.MARGIN_TOP, IProperty.AXIS_VERTICAL),
new LengthProperty(CSS.PADDING_BOTTOM, IProperty.AXIS_VERTICAL),
new LengthProperty(CSS.PADDING_LEFT, IProperty.AXIS_HORIZONTAL),
new LengthProperty(CSS.PADDING_RIGHT, IProperty.AXIS_HORIZONTAL),
new LengthProperty(CSS.PADDING_TOP, IProperty.AXIS_VERTICAL),
new ColorProperty(CSS.BORDER_BOTTOM_COLOR),
new ColorProperty(CSS.BORDER_LEFT_COLOR),
new ColorProperty(CSS.BORDER_RIGHT_COLOR),
new ColorProperty(CSS.BORDER_TOP_COLOR),
new BorderStyleProperty(CSS.BORDER_BOTTOM_STYLE),
new BorderStyleProperty(CSS.BORDER_LEFT_STYLE),
new BorderStyleProperty(CSS.BORDER_RIGHT_STYLE),
new BorderStyleProperty(CSS.BORDER_TOP_STYLE),
new BorderWidthProperty(CSS.BORDER_BOTTOM_WIDTH, CSS.BORDER_BOTTOM_STYLE, IProperty.AXIS_VERTICAL),
new BorderWidthProperty(CSS.BORDER_LEFT_WIDTH, CSS.BORDER_LEFT_STYLE, IProperty.AXIS_HORIZONTAL),
new BorderWidthProperty(CSS.BORDER_RIGHT_WIDTH, CSS.BORDER_RIGHT_STYLE, IProperty.AXIS_HORIZONTAL),
new BorderWidthProperty(CSS.BORDER_TOP_WIDTH, CSS.BORDER_TOP_STYLE, IProperty.AXIS_VERTICAL),
new BorderSpacingProperty(),
};
/**
* The properties to calculate. This can be changed by the app.
*/
private static IProperty[] properties = CSS_PROPERTIES;
/**
* Style sheet is the default for the renderer.
*/
public static final byte SOURCE_DEFAULT = 0;
/**
* Style sheet was provided by the document author.
*/
public static final byte SOURCE_AUTHOR = 1;
/**
* Style sheet was provided by the user.
*/
public static final byte SOURCE_USER = 2;
/**
* The rules that comprise the stylesheet.
*/
private Rule[] rules;
/**
* Computing styles can be expensive, e.g. we have to calculate the styles
* of all parents of an element. We therefore cache styles in a map of
* element => WeakReference(styles). A weak hash map is used to avoid
* leaking memory as elements are deleted. By using weak references to
* the values, we also ensure the cache is memory-sensitive.
*
* This must be transient to prevent it from being serialized, as
* WeakHashMaps are not serializable.
*/
private transient Map styleMap = null;
/**
* Class constructor.
*
* @param rules Rules that constitute the style sheet.
*/
public StyleSheet(Rule[] rules) {
this.rules = rules;
}
/**
* Flush any cached styles for the given element.
* @param element Element for which styles are to be flushed.
*/
public void flushStyles(Element element) {
this.getStyleMap().remove(element);
}
/**
* Returns a pseudo-element representing content to be displayed
* after the given element, or null if there is no such content.
* @param element Parent element of the pseudo-element.
*/
public Element getAfterElement(Element element) {
PseudoElement pe = new PseudoElement(element, PseudoElement.AFTER);
Styles styles = this.getStyles(pe);
if (styles == null) {
return null;
} else {
return pe;
}
}
/**
* Returns a pseudo-element representing content to be displayed
* before the given element, or null if there is no such content.
* @param element Parent element of the pseudo-element.
*/
public Element getBeforeElement(Element element) {
PseudoElement pe = new PseudoElement(element, PseudoElement.BEFORE);
Styles styles = this.getStyles(pe);
if (styles == null) {
return null;
} else {
return pe;
}
}
/**
* Returns the array of standard CSS properties.
*/
public static IProperty[] getCssProperties() {
return CSS_PROPERTIES;
}
/**
* Returns the styles for the given element. The styles are cached to
* ensure reasonable performance.
*
* @param element Element for which to calculate the styles.
*/
public Styles getStyles(Element element) {
Styles styles;
WeakReference ref = (WeakReference) this.getStyleMap().get(element);
if (ref != null) {
// can't combine these tests, since calling ref.get() twice
// (once to query for null, once to get the value) would
// cause a race condition should the GC happen btn the two.
styles = (Styles) ref.get();
if (styles != null) {
return styles;
}
} else if (this.getStyleMap().containsKey(element)) {
// this must be a pseudo-element with no content
return null;
}
styles = calculateStyles(element);
if (styles == null) {
// Yes, they can be null if element is a PseudoElement with no content property
this.getStyleMap().put(element, null);
} else {
this.getStyleMap().put(element, new WeakReference(styles));
}
return styles;
}
private Styles calculateStyles(Element element) {
Styles styles = new Styles();
Styles parentStyles = null;
if (element.getParent() != null) {
parentStyles = this.getStyles(element.getParent());
}
Map decls = this.getApplicableDecls(element);
LexicalUnit lu;
// If we're finding a pseudo-element, look at the 'content' property
// first, since most of the time it'll be empty and we'll return null.
if (element instanceof PseudoElement) {
lu = (LexicalUnit) decls.get(CSS.CONTENT);
if (lu == null) {
return null;
}
List content = new ArrayList();
while (lu != null) {
if (lu.getLexicalUnitType() == LexicalUnit.SAC_STRING_VALUE) {
content.add(lu.getStringValue());
}
lu = lu.getNextLexicalUnit();
}
styles.setContent(content);
}
for (int i = 0; i < properties.length; i++) {
IProperty property = properties[i];
lu = (LexicalUnit) decls.get(property.getName());
Object value = property.calculate(lu, parentStyles, styles);
styles.put(property.getName(), value);
}
// Now, map font-family, font-style, font-weight, and font-size onto
// an AWT font.
int styleFlags = FontSpec.PLAIN;
String fontStyle = styles.getFontStyle();
if (fontStyle.equals(CSS.ITALIC) || fontStyle.equals(CSS.OBLIQUE)) {
styleFlags |= FontSpec.ITALIC;
}
if (styles.getFontWeight() > 550) {
// 550 is halfway btn normal (400) and bold (700)
styleFlags |= FontSpec.BOLD;
}
String textDecoration = styles.getTextDecoration();
if (textDecoration.equals(CSS.UNDERLINE)) {
styleFlags |= FontSpec.UNDERLINE;
} else if (textDecoration.equals(CSS.OVERLINE)) {
styleFlags |= FontSpec.OVERLINE;
} else if (textDecoration.equals(CSS.LINE_THROUGH)) {
styleFlags |= FontSpec.LINE_THROUGH;
}
styles.setFont(new FontSpec(styles.getFontFamilies(),
styleFlags,
Math.round(styles.getFontSize())));
return styles;
}
/**
* Returns the list of properties to be parsed by StyleSheets in this app.
*/
public static IProperty[] getProperties() {
return StyleSheet.properties;
}
/**
* Returns the rules comprising this stylesheet.
*/
public Rule[] getRules() {
return this.rules;
}
/**
* Sets the list of properties to be used by StyleSheets in this application.
* @param properties New array of IProperty objects to be used.
*/
public static void setProperties(IProperty[] properties) {
StyleSheet.properties = properties;
}
//========================================================= PRIVATE
/**
* Returns all the declarations that apply to the given element.
*/
private Map getApplicableDecls(Element element) {
// Find all the property declarations that apply to this element.
List declList = new ArrayList();
Rule[] rules = this.getRules();
for (int i = 0; i < rules.length; i++) {
Rule rule = rules[i];
if (rule.matches(element)) {
PropertyDecl[] ruleDecls = rule.getPropertyDecls();
for (int j = 0; j < ruleDecls.length; j++) {
declList.add(ruleDecls[j]);
}
}
}
// Sort in cascade order. We can then just stuff them into a
// map and get the right values since higher-priority values
// come later and overwrite lower-priority ones.
Collections.sort(declList);
Map decls = new HashMap();
Iterator iter = declList.iterator();
while (iter.hasNext()) {
PropertyDecl decl = (PropertyDecl) iter.next();
PropertyDecl prevDecl = (PropertyDecl) decls.get(decl.getProperty());
if (prevDecl == null || !prevDecl.isImportant() || decl.isImportant()) {
decls.put(decl.getProperty(), decl);
}
}
Map values = new HashMap();
for (Iterator it = decls.keySet().iterator(); it.hasNext();) {
PropertyDecl decl = (PropertyDecl) decls.get(it.next());
values.put(decl.getProperty(), decl.getValue());
}
return values;
}
private Map getStyleMap() {
if (this.styleMap == null) {
this.styleMap = new WeakHashMap();
}
return this.styleMap;
}
}