/* StyleSheet.java -- Copyright (C) 2005 Free Software Foundation, Inc. This file is part of GNU Classpath. GNU Classpath is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. GNU Classpath is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Classpath; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions of the GNU General Public License cover the whole combination. As a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module. An independent module is a module which is not derived from or based on this library. If you modify this library, you may extend this exception to your version of the library, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. */ package javax.swing.text.html; import gnu.javax.swing.text.html.css.CSSColor; import gnu.javax.swing.text.html.css.CSSParser; import gnu.javax.swing.text.html.css.CSSParserCallback; import gnu.javax.swing.text.html.css.FontSize; import gnu.javax.swing.text.html.css.FontStyle; import gnu.javax.swing.text.html.css.FontWeight; import gnu.javax.swing.text.html.css.Length; import gnu.javax.swing.text.html.css.Selector; import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.Serializable; import java.io.StringReader; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.List; import javax.swing.border.Border; import javax.swing.event.ChangeListener; import javax.swing.text.AttributeSet; import javax.swing.text.Element; import javax.swing.text.MutableAttributeSet; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.Style; import javax.swing.text.StyleConstants; import javax.swing.text.StyleContext; import javax.swing.text.View; /** * This class adds support for defining the visual characteristics of HTML views * being rendered. This enables views to be customized by a look-and-feel, mulitple * views over the same model can be rendered differently. Each EditorPane has its * own StyleSheet, but by default one sheet will be shared by all of the HTMLEditorKit * instances. An HTMLDocument can also have a StyleSheet, which holds specific CSS * specs. * * In order for Views to store less state and therefore be more lightweight, * the StyleSheet can act as a factory for painters that handle some of the * rendering tasks. Since the StyleSheet may be used by views over multiple * documents the HTML attributes don't effect the selector being used. * * The rules are stored as named styles, and other information is stored to * translate the context of an element to a rule. * * @author Lillian Angel (langel@redhat.com) */ public class StyleSheet extends StyleContext { /** * Parses CSS stylesheets using the parser in gnu/javax/swing/html/css. * * This is package private to avoid accessor methods. */ class CSSStyleSheetParserCallback implements CSSParserCallback { /** * The current style. */ private CSSStyle style; /** * The precedence of the stylesheet to be parsed. */ private int precedence; /** * Creates a new CSS parser. This parser parses a CSS stylesheet with * the specified precedence. * * @param prec the precedence, according to the constants defined in * CSSStyle */ CSSStyleSheetParserCallback(int prec) { precedence = prec; } /** * Called at the beginning of a statement. * * @param sel the selector */ public void startStatement(Selector sel) { style = new CSSStyle(precedence, sel); } /** * Called at the end of a statement. */ public void endStatement() { css.add(style); style = null; } /** * Called when a declaration is parsed. * * @param property the property * @param value the value */ public void declaration(String property, String value) { CSS.Attribute cssAtt = CSS.getAttribute(property); Object val = CSS.getValue(cssAtt, value); CSS.addInternal(style, cssAtt, value); if (cssAtt != null) style.addAttribute(cssAtt, val); } } /** * Represents a style that is defined by a CSS rule. */ private class CSSStyle extends SimpleAttributeSet implements Style, Comparable { static final int PREC_UA = 400000; static final int PREC_NORM = 300000; static final int PREC_AUTHOR_NORMAL = 200000; static final int PREC_AUTHOR_IMPORTANT = 100000; static final int PREC_USER_IMPORTANT = 0; /** * The priority of this style when matching CSS selectors. */ private int precedence; /** * The selector for this rule. * * This is package private to avoid accessor methods. */ Selector selector; CSSStyle(int prec, Selector sel) { precedence = prec; selector = sel; } public String getName() { // TODO: Implement this for correctness. return null; } public void addChangeListener(ChangeListener listener) { // TODO: Implement this for correctness. } public void removeChangeListener(ChangeListener listener) { // TODO: Implement this for correctness. } /** * Sorts the rule according to the style's precedence and the * selectors specificity. */ public int compareTo(Object o) { CSSStyle other = (CSSStyle) o; return other.precedence + other.selector.getSpecificity() - precedence - selector.getSpecificity(); } } /** The base URL */ URL base; /** Base font size (int) */ int baseFontSize; /** The style sheets stored. */ StyleSheet[] styleSheet; /** * Maps element names (selectors) to AttributSet (the corresponding style * information). */ ArrayList css = new ArrayList(); /** * Maps selectors to their resolved styles. */ private HashMap resolvedStyles; /** * Constructs a StyleSheet. */ public StyleSheet() { super(); baseFontSize = 4; // Default font size from CSS resolvedStyles = new HashMap(); } /** * Gets the style used to render the given tag. The element represents the tag * and can be used to determine the nesting, where the attributes will differ * if there is nesting inside of elements. * * @param t - the tag to translate to visual attributes * @param e - the element representing the tag * @return the set of CSS attributes to use to render the tag. */ public Style getRule(HTML.Tag t, Element e) { // Create list of the element and all of its parents, starting // with the bottommost element. ArrayList path = new ArrayList(); Element el; AttributeSet atts; for (el = e; el != null; el = el.getParentElement()) path.add(el); // Create fully qualified selector. StringBuilder selector = new StringBuilder(); int count = path.size(); // We append the actual element after this loop. for (int i = count - 1; i > 0; i--) { el = (Element) path.get(i); atts = el.getAttributes(); Object name = atts.getAttribute(StyleConstants.NameAttribute); selector.append(name.toString()); if (atts.isDefined(HTML.Attribute.ID)) { selector.append('#'); selector.append(atts.getAttribute(HTML.Attribute.ID)); } else if (atts.isDefined(HTML.Attribute.CLASS)) { selector.append('.'); selector.append(atts.getAttribute(HTML.Attribute.CLASS)); } selector.append(' '); } selector.append(t.toString()); el = (Element) path.get(0); atts = el.getAttributes(); // For leaf elements, we have to fetch the tag specific attributes. if (el.isLeaf()) { Object o = atts.getAttribute(t); if (o instanceof AttributeSet) atts = (AttributeSet) o; else atts = null; } if (atts != null) { if (atts.isDefined(HTML.Attribute.ID)) { selector.append('#'); selector.append(atts.getAttribute(HTML.Attribute.ID)); } else if (atts.isDefined(HTML.Attribute.CLASS)) { selector.append('.'); selector.append(atts.getAttribute(HTML.Attribute.CLASS)); } } return getResolvedStyle(selector.toString(), path, t); } /** * Fetches a resolved style. If there is no resolved style for the * specified selector, the resolve the style using * {@link #resolveStyle(String, List, HTML.Tag)}. * * @param selector the selector for which to resolve the style * @param path the Element path, used in the resolving algorithm * @param tag the tag for which to resolve * * @return the resolved style */ private Style getResolvedStyle(String selector, List path, HTML.Tag tag) { Style style = (Style) resolvedStyles.get(selector); if (style == null) style = resolveStyle(selector, path, tag); return style; } /** * Resolves a style. This creates arrays that hold the tag names, * class and id attributes and delegates the work to * {@link #resolveStyle(String, String[], String[], String[])}. * * @param selector the selector * @param path the Element path * @param tag the tag * * @return the resolved style */ private Style resolveStyle(String selector, List path, HTML.Tag tag) { int count = path.size(); String[] tags = new String[count]; String[] ids = new String[count]; String[] classes = new String[count]; for (int i = 0; i < count; i++) { Element el = (Element) path.get(i); AttributeSet atts = el.getAttributes(); if (i == 0 && el.isLeaf()) { Object o = atts.getAttribute(tag); if (o instanceof AttributeSet) atts = (AttributeSet) o; else atts = null; } if (atts != null) { HTML.Tag t = (HTML.Tag) atts.getAttribute(StyleConstants.NameAttribute); if (t != null) tags[i] = t.toString(); else tags[i] = null; if (atts.isDefined(HTML.Attribute.CLASS)) classes[i] = atts.getAttribute(HTML.Attribute.CLASS).toString(); else classes[i] = null; if (atts.isDefined(HTML.Attribute.ID)) ids[i] = atts.getAttribute(HTML.Attribute.ID).toString(); else ids[i] = null; } else { tags[i] = null; classes[i] = null; ids[i] = null; } } tags[0] = tag.toString(); return resolveStyle(selector, tags, ids, classes); } /** * Performs style resolving. * * @param selector the selector * @param tags the tags * @param ids the corresponding ID attributes * @param classes the corresponding CLASS attributes * * @return the resolved style */ private Style resolveStyle(String selector, String[] tags, String[] ids, String[] classes) { // FIXME: This style resolver is not correct. But it works good enough for // the default.css. int count = tags.length; ArrayList styles = new ArrayList(); for (Iterator i = css.iterator(); i.hasNext();) { CSSStyle style = (CSSStyle) i.next(); if (style.selector.matches(tags, classes, ids)) styles.add(style); } // Sort selectors. Collections.sort(styles); Style[] styleArray = new Style[styles.size()]; styleArray = (Style[]) styles.toArray(styleArray); Style resolved = new MultiStyle(selector, (Style[]) styles.toArray(styleArray)); resolvedStyles.put(selector, resolved); return resolved; } /** * Gets the rule that best matches the selector. selector is a space * separated String of element names. The attributes of the returned * Style will change as rules are added and removed. * * @param selector - the element names separated by spaces * @return the set of CSS attributes to use to render */ public Style getRule(String selector) { Selector sel = new Selector(selector); CSSStyle best = null; for (Iterator i = css.iterator(); i.hasNext();) { CSSStyle style = (CSSStyle) i.next(); if (style.compareTo(best) < 0) best = style; } return best; } /** * Adds a set of rules to the sheet. The rules are expected to be in valid * CSS format. This is called as a result of parsing a