CSS4J
Usage

CSS4J User Guide

This project is an implementation of W3C's CSS Object Model API in the Java™ language, and also adds CSS support to the DOM4J package. It targets several different use cases, with functionalities from style sheet error detection to style computation.

Java 8 and later versions are supported (the library is fully modular).

Overview

This library provides a number of classes to deal with different CSS Object Model abstractions. For example:

You can use those classes as stand-alone objects or have them all together in a document, one of the implementations of the DOM-based CSSDocument interface:

An important advantage of using DOM-based interfaces is that you can use the same or very similar methods in your web browser's Javascript (with the exception of the CSSValue API which is based on an old W3C standard instead of the more recent Houdini's Typed OM).

Choosing a DOM implementation

Once you have chosen which DOM you prefer to use, the first step is to obtain an instance of a compatible CSSStyleSheetFactory.

There are three implementations of that interface:

Even if you want to use only stand-alone style sheets, you should use one of the above factories to create them with the createStyleSheet("title", "media") method.

The style sheet objects created by that method are empty, but you can load the style sheet rules with the AbstractCSSStyleSheet.parseStyleSheet(reader, short) method (see Parsing a Style Sheet).

Modules

The project is organized in several modules:

In general, you'll need to have at least css4j, carte-util and tokenproducer in your classpath/modulepath. If you use Gradle or Apache Maven, you may want to look at each module's build.gradle files (or even better, the .pom or the .module files at the repository) to check for the specific module dependencies.

The latest css4j-* artifacts are generally compatible with css4j releases that have the same major version number and the same or larger minor version. For example css4j-awt 4.0 is compatible with css4j 4.0.1 and also with 4.2.2.

Usage from a Gradle project

If your Gradle project depends on css4j, you can use this project's own Maven repository in a repositories section of your build file:

repositories {
    maven {
        url "https://css4j.github.io/maven/"
        mavenContent {
            releasesOnly()
        }
        content {
            includeGroup 'io.sf.carte'
            includeGroup 'io.sf.jclf'
            includeGroup 'xmlpull'
            includeGroup 'xpp3'
        }
    }
}

please use this repository only for the artifact groups listed in the includeGroup statements.

Then, in your build.gradle file:

dependencies {
    api "io.sf.carte:css4j:${css4jVersion}"
}

where css4jVersion could be defined in a gradle.properties file.

Usage from a Maven build

If you build your project (that depends on css4j) with Apache Maven, please note that neither css4j nor some of its dependencies are in Maven Central:

So you may want to add css4j's Maven repository to your POM file:

<repositories>
    <repository>
        <id>css4j</id>
        <name>CSS4J repository</name>
        <url>https://css4j.github.io/maven/</url>
    </repository>
</repositories>

Alternatively, you can also install the artifacts manually into your local Maven repository, which can be done easily with the:

scripts. And then, add the following to the <dependencies> section of your pom.xml:

<!-- This artifact is not in Maven Central -->
<dependency>
    <groupId>io.sf.carte</groupId>
    <artifactId>css4j</artifactId>
    <version>${css4j.version}</version>
</dependency>

Using css4j's native DOM implementation

You can create a DOM document from scratch and use the related DOM methods to programmatically build a document: just use the provided DOM implementation (io.sf.carte.doc.dom.CSSDOMImplementation). But if you want to parse an existing document, the procedure depends on the type of document: to parse an XML document (including XHTML documents), you can use this library's XMLDocumentBuilder with the SAX parser provided by the JDK, while to parse an HTML one (or an XHTML document that does not use namespace prefixes) you can use the validator.nu HTML5 parser (be sure to use the latest version from the nu.validator Maven group id).

An example with that parser follows:

import java.io.Reader;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import io.sf.carte.doc.dom.CSSDOMImplementation;
import io.sf.carte.doc.dom.DOMElement;
import io.sf.carte.doc.dom.HTMLDocument;
import io.sf.carte.doc.dom.XMLDocumentBuilder;
import io.sf.carte.doc.style.css.CSSComputedProperties;
import io.sf.carte.doc.style.css.CSSTypedValue;
import io.sf.carte.doc.style.css.RGBAColor;
import io.sf.carte.doc.xml.dtd.DefaultEntityResolver;
import nu.validator.htmlparser.common.XmlViolationPolicy;
import nu.validator.htmlparser.sax.HtmlParser;
[...]

// Instantiate DOM implementation (with default settings: no IE hacks accepted) 
// and configure it
CSSDOMImplementation impl = new CSSDOMImplementation();
// Alternatively, impl = new CSSDOMImplementation(flags);

// Now load default HTML user agent sheets
impl.setDefaultHTMLUserAgentSheet();

// Prepare parser
HtmlParser parser = new HtmlParser(XmlViolationPolicy.ALTER_INFOSET);
parser.setCommentPolicy(XmlViolationPolicy.ALLOW);
parser.setXmlnsPolicy(XmlViolationPolicy.ALLOW);

// Prepare builder
XMLDocumentBuilder builder = new XMLDocumentBuilder(impl);
builder.setHTMLProcessing(true);
builder.setXMLReader(parser);

// Read the document to parse, and prepare source object
Reader re = ... [reader for HTML document]
InputSource source = new InputSource(re);

// Parse. If the document is not HTML, you want to use DOMDocument instead
HTMLDocument document = (HTMLDocument) builder.parse(source);
re.close();

// Set document URI
document.setDocumentURI("http://www.example.com/mydocument.html");

Now, you have a CSS-enabled document.

You can see the above code example (and others in this guide) in the Css4jTest.java example file.

Computing styles

To compute styles, use getComputedStyle:

DOMElement element = document.getElementById("someId");
CSSComputedProperties style = element.getComputedStyle(null);

// Next line could be 'String display = style.getDisplay();'
String display = style.getPropertyValue("display");

// If you use a factory that has been set to setLenientSystemValues(false), next
// line may throw an exception if the 'color' property was not specified.
// The default value for lenientSystemValues is TRUE.
RGBAColor color = ((CSSTypedValue) style.getPropertyCSSValue("color")).toRGBColor();

// Suppose that the linked style sheet located at 'css/sheet.css' declares:
// background-image: url('foo.png');

String image_css = style.getPropertyValue("background-image");
String image_uri = ((CSSTypedValue) style.getPropertyCSSValue("background-image")).getStringValue();

// Then, because we already set the document URI to "http://www.example.com/mydocument.html",
// image_css will be set to "url('http://www.example.com/css/foo.png')",
// and image_uri to "http://www.example.com/css/foo.png"

The CSSComputedProperties interface extends W3C's CSSStyleDeclaration for computed styles, adding methods like getComputedFontSize() or getComputedLineHeight().

If you are computing styles for a specific medium, tell the document about it (see "Media Handling"):

document.setTargetMedium("print");

Conformance with the DOM specification

This library's native DOM implementation has some minor behavior differences with what is written in the DOM Level 3 Core Specification. For example, on elements and attributes the Node.getLocalName() method returns the tag name instead of null when the node was created with a DOM Level 1 method such as Document.createElement(). Read the io.sf.carte.doc.dom package description for additional information.

Examples with DOM4J

To use the library with dom4j (using dom4j-like documents, elements and factory) you need the css4j-dom4j module in addition to the core module. The -dom4j module optionally depends on the -agent module, but you do not need the latter unless you plan to use the DOM4J agent.

Gradle setup

For example, to set up css4j-dom4j (and the generally useful xml-dtd) with Gradle, put the following in your build file:

dependencies {
    implementation "io.sf.carte:css4j-dom4j:${css4jDom4jVersion}"
    implementation "io.sf.carte:xml-dtd:4.2.1"
}

But please remember that you have to set up css4j's repository, if you haven't already:

repositories {
    maven {
        url "https://css4j.github.io/maven/"
        mavenContent {
            releasesOnly()
        }
        content {
            includeGroup 'io.sf.carte'
            includeGroup 'io.sf.jclf'
            includeGroup 'xmlpull'
            includeGroup 'xpp3'
        }
    }
}

Maven setup

Same with Apache Maven:

<!-- This artifact requires the css4j Maven repository -->
<dependency>
    <groupId>io.sf.carte</groupId>
    <artifactId>css4j-dom4j</artifactId>
    <version>${css4jDom4j.version}</version>
</dependency>

<!-- This artifact requires the css4j Maven repository -->
<dependency>
    <groupId>io.sf.carte</groupId>
    <artifactId>xml-dtd</artifactId>
    <version>4.2.1</version>
</dependency>

Again, please keep in mind adding css4j's Maven repository to your POM file:

<repositories>
    <repository>
        <id>css4j</id>
        <name>CSS4J repository</name>
        <url>https://css4j.github.io/maven/</url>
    </repository>
</repositories>

Dependencies and compatibility

The css4j-dom4j artifact transitively implies the css4j core module, but since both artifacts have different release cycles you may want to add an explicit dependency for the core module, with the latest version.

As explained in the modules section, the latest css4j-dom4j version is supposed to be compatible with css4j artifacts that have the same major version number and equal or greater minor version. Which in practice means that you should be always using the latest css4j-dom4j combined with the latest css4j version.

If your project is modular you will need this:

requires io.sf.carte.css4j.dom4j;
requires io.sf.carte.xml.dtd; // Only if you use DefaultEntityResolver

Loading and parsing an XML document

This is the easiest way to parse an XML document to use this package with DOM4J, using that library's SAXReader:

SAXReader reader = new SAXReader(XHTMLDocumentFactory.getInstance());
reader.setEntityResolver(new DefaultEntityResolver());
Reader re = ... [reader for HTML document]
XHTMLDocument document = (XHTMLDocument) reader.read(re);

As you see, a document factory called XHTMLDocumentFactory is used. And as mentioned previously, the DefaultEntityResolver class is provided by the xml-dtd project.

Once you got the element you want the computed style for (see, for example, the DOM4J Quick Start Guide), just get it with a procedure analogous to the ViewCSS interface:

CSSComputedProperties style = ((CSSStylableElement) element).getComputedStyle(null);
String display = style.getPropertyValue("display");

Be careful to have the XHTMLDocumentFactory loaded with the desired defaults, like the user agent style sheet which was loaded by default in 1.0 but not in 2.0 and later.

Parsing HTML5 into DOM4J

It is also possible to parse an HTML5 document into a css4j-dom4j tree, with the aforementioned validator.nu HTML5 parser:

XHTMLDocumentFactory factory = XHTMLDocumentFactory.getInstance();
// Next line is optional: default is TRUE, and is probably what you want
// factory.getStyleSheetFactory().setLenientSystemValues(false);

HtmlParser parser = new HtmlParser(XmlViolationPolicy.ALTER_INFOSET);
parser.setCommentPolicy(XmlViolationPolicy.ALLOW);
parser.setXmlnsPolicy(XmlViolationPolicy.ALLOW);

// Configure the SAXReader with the HtmlParser
SAXReader builder = new SAXReader(factory);
builder.setXMLReader(parser);
// Provide an error handler to avoid exceptions
ErrorHandler errorHandler = [...]
builder.setErrorHandler(errorHandler);

// We do not set the EntityResolver, the HtmlParser does not need it
Reader re = ... [reader for HTML document]
XHTMLDocument document = (XHTMLDocument) builder.read(re);

Or parse the document with the css4j's builder, XMLDocumentBuilder (which can account for things like implied HTML parent elements), instead of dom4j's SAXReader:

XHTMLDocumentFactory factory = XHTMLDocumentFactory.getInstance();
// Next line is optional: default is TRUE, and is probably what you want
// factory.getStyleSheetFactory().setLenientSystemValues(false);

HtmlParser parser = new HtmlParser(XmlViolationPolicy.ALTER_INFOSET);
parser.setCommentPolicy(XmlViolationPolicy.ALLOW);
parser.setXmlnsPolicy(XmlViolationPolicy.ALLOW);

XMLDocumentBuilder builder = new XMLDocumentBuilder(factory);
builder.setHTMLProcessing(true);
builder.setXMLReader(parser);

Reader re = ... [reader for XML document]
InputSource source = new InputSource(re);
XHTMLDocument document = (XHTMLDocument) builder.parse(source);
re.close();

Caveats about DOM4J and HTML

A plain HTML document may contain STYLE elements with selectors like div>p, and css4j's native DOM handles that correctly. However, XML-oriented DOM implementations are likely to serialize them as "div&gt;p", breaking the CSS. For example, if you serialize a DOM4J document with asXML() or code like the following:

Writer wri = ... [put your writer here]
OutputFormat format = OutputFormat.createPrettyPrint();
format.setXHTML(true);
HTMLWriter writer = new HTMLWriter(wri, format);
writer.write(document);

The result will be the "div&gt;p". As explained below, with other DOM implementations it is possible to work around this problem if one serializes to XHTML with a properly configured Transformer, but not with DOM4J where you are forced to serialize to plain HTML instead.

Serializing HTML with an XML DOM

As was previously mentioned, if you serialize a plain HTML document (which does not contain CDATA sections) with an XML-oriented DOM to obtain XHTML, you can avoid serializing div>p as "div&gt;p" if you use a Transformer which is configured for the task.

Serializing to XHTML

The code would be like the following:

// Serialize to XHTML
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.setOutputProperty(OutputKeys.METHOD, "xml");
// Put other Transformer configurations here
// ...
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
// Put the embedded style sheets inside CDATA sections
transformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, "script style");

Writer wri = ... [put your writer here]
transformer.transform(new DOMSource(document), new StreamResult(wri));

which —as the comment says— puts the embedded style sheets inside CDATA sections. Note, however, that:

  1. In HTML, the CDATA sections can only be used in foreign content —otherwise being considered as "bogus comments"— so you are now in XHTML instead of HTML.
  2. If you use the above code with css4j-dom4j (3.6.1 or later is required), it will not produce the desired result, so you have to resort to plain HTML serialization by setting OutputKeys.METHOD to "html" (see next example).

Serializing to plain HTML (no CDATA sections)

// Serialize to plain HTML
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.setOutputProperty(OutputKeys.METHOD, "html");
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");

Writer wri = ... [put your writer here]
transformer.transform(new DOMSource(document), new StreamResult(wri));

Unfortunately this procedure adds a META element with an http-equiv attribute, which is something that you probably did not want.

To handle HTML, the native DOM is recommended over any XML DOM.

Usage with the DOM wrapper

If you choose to build your document with your favorite DOM implementation instead of the CSS4J one or the DOM4J back-end, you can use the DOMCSSStyleSheetFactory.createCSSDocument(document) method to wrap a pre-existing DOM Document. Example:

DOMCSSStyleSheetFactory cssFactory = new DOMCSSStyleSheetFactory();
CSSDocument document = cssFactory.createCSSDocument(otherDOMdocument);

Unlike the native DOM or the DOM4J back-end, the DOM resulting from the DOM wrapper is read-only, although you can change the values of some nodes.

Consistency of different DOM implementations

Beware that the computed styles found by each method (native DOM, DOM4J back-end or DOM wrapper) may not be completely identical, due to differences in the underlying document DOM implementations:

  • XML-oriented DOM implementations do not implement the case sensitivity subtleties of HTML (although this library attempts to emulate the basics).
  • The :dir() pseudo-class is not fully supported on DOM4J, due to limitations in its DOM support.

If you find a difference in styles computed from different back-ends that you believe to be a bug, please report it.

Accessing the declared style sheets

CSSOM indexed access

To access the styles declared in style sheets, we can use the relevant CSS Object Model methods. For example, suppose that the first style sheet of the document contains only the following rule:

div > .myClass {
  font-size: 14pt;
  color: var(--myColor, #46f);
}

then, to access that style sheet and that rule we could use something like this:

/*
 * Access style sheet(s) and its rule(s)
 */
// Obtain the list of CSS style sheets
StyleSheetList sheets = document.getStyleSheets();
// Access the first sheet
AbstractCSSStyleSheet sheet = sheets.item(0);
// The first rule in the sheet
AbstractCSSRule rule = sheet.getCssRules().item(0);
// What kind of rule is it?
int ruleType = rule.getType();
// It is a style rule (CSSRule.STYLE_RULE)

Now we can cast to a StyleRule which gives us access to the selector list and the declared style.

Rule iterators

The same as above, but with a rule iterator:

/*
 * It is possible to use iterators to retrieve the rules.
 */
sheet = sheets.item(0);
Iterator<AbstractCSSRule> it = sheet.getCssRules().iterator();
// it.hasNext() returns true

// Access the first rule in the sheet
rule = it.next();
// What kind of rule is it?
ruleType = rule.getType();
// It is a style rule (CSSRule.STYLE_RULE)

Sheet Visitors

You can also use the Visitor API to perform many of the tasks that can be achieved with the standard CSSOM, for example:

  • acceptStyleRuleVisitor to visit style rules in a sheet.
  • acceptDeclarationRuleVisitor to visit declaration rules in a sheet. In the terminology of this library, a declaration rule is a rule that contains any kind of declaration, be it property or descriptor declarations. For example, style, font-face and keyframe rules are all "declaration rules".
  • acceptDescriptorRuleVisitor to visit declaration rules with descriptors within a sheet.

Those methods also apply to sheet lists:

With Visitors, you do not need to navigate the different rules yourself. For an example of its use, see Embedding SVG in HTML documents.

Dealing with selectors

If your use case requires inspecting the selectors, you will need to look at the NSAC api, starting with the Selector class.

// Cast
StyleRule styleRule = (StyleRule) rule;
// Obtain the selector list
SelectorList selist = styleRule.getSelectorList();

// First (and only) selector
Selector selector = selist.item(0);
// Selector type (which we can use to cast the selector)
SelectorType selType = selector.getSelectorType();
// For "div > .myClass" selType would be SelectorType.CHILD

// Cast to CombinatorSelector (see CHILD javadoc)
CombinatorSelector comb = (CombinatorSelector) selector;
// Obtain the two selectors that are combined by ">"
Selector firstSel = comb.getSelector();
SimpleSelector secondSel = comb.getSecondSelector();

// Examine the first selector
SelectorType firstSelType = firstSel.getSelectorType();
// Type is SelectorType.ELEMENT (div)

// Cast to ElementSelector (see ELEMENT javadoc)
ElementSelector elemSel = (ElementSelector) firstSel;
String elemName = elemSel.getLocalName();
// It is "div"

// Now the second selector
SelectorType secondSelType = secondSel.getSelectorType();
// Type is SelectorType.CONDITIONAL (.myClass)

// Cast to ConditionalSelector
ConditionalSelector condSel = (ConditionalSelector) secondSel;
Condition condition = condSel.getCondition();
ConditionType condType = condition.getConditionType();
// The condition type is ConditionType.CLASS

// Cast condition to AttributeCondition (as suggested by the CLASS javadoc)
AttributeCondition attrCond = (AttributeCondition) condition;
// Retrieve the condition (class) name
String cssClassName = attrCond.getValue();
// It is "myClass"

Low-level selector parsing

Sometimes one has to work with stand-alone selectors, for which you can use a NSAC parser:

CSSParser parser = new CSSParser();
SelectorList selist = parser.parseSelectors("a:not([href]):not([tabindex]):focus,pre,.myClass span");

Note: you should avoid stand-alone parsing namespaced selectors with parseSelectors and equivalent single-argument methods in CSSParser, as the namespace prefixes won't map to any namespace URI. See parseSelectors(String,Parser.NamespaceMap) for a solution to the issue.

Using the declared styles

In the previous example we got the style rule in a styleRule variable. Now we use it to obtain the declared style with getStyle() and examine the declared properties:

// Obtain the declared style from the rule
AbstractCSSStyleDeclaration style = styleRule.getStyle();

// Obtain the number of properties
int pCount = style.getLength();
// Two properties

/*
 * The first property
 */
// Retrieve the name of the first property
String pname0 = style.item(0);
// pname0 is "font-size"

// Obtain the value of "font-size"
StyleValue fontSize = style.getPropertyCSSValue("font-size");

// Is it a length?
SyntaxParser synparser = new SyntaxParser();
CSSValueSyntax syntaxLength = synparser.parseSyntax("<length>");
Match match0 = fontSize.matches(syntaxLength);
// Match.TRUE: yes it is a length

// Let's check the primitive type
CSSValue.Type primiType0 = fontSize.getPrimitiveType();
// CSSValue.Type.NUMERIC: It is numeric

// Obtain the CSS unit and numeric value
NumberValue numericValue = (NumberValue) fontSize;
short unit = numericValue.getUnitType();
// Unit is typographic points (CSSUnit.CSS_PT)
float floatVal = numericValue.getFloatValue(unit);
// Value is 14 (14pt)

// Change the value to 23px (could use any length unit)
numericValue.setFloatValue(CSSUnit.CSS_PX, 23f);

/*
 * The second property
 */
// Retrieve the name of the second property
String pname1 = style.item(1);
// pname1 is "color"

// Obtain the value of "color"
StyleValue color = style.getPropertyCSSValue("color");

// Let's check the global type
CSSValue.CssType type1 = color.getCssValueType();
// It is a CSSValue.CssType.PROXY, which means that it is either a
// custom property value (it is not) or a type not known in advance

// But may it be a color?
CSSValueSyntax syntaxColor = synparser.parseSyntax("<color>");
Match match1 = color.matches(syntaxColor);
// Gives Match.PENDING: yes it could be a color, but we do not know for sure
// until the value is computed

// Check the primitive type
CSSValue.Type primiType1 = color.getPrimitiveType();
// It is a var() function (CSSValue.Type.VAR)

// Obtain the custom property name
VarValue varValue = (VarValue) color;
String customPropertyName = varValue.getName();
// Name is --myColor

// In this case we have a fallback value (otherwise next line returns null)
LexicalUnit lexUnit = varValue.getFallback();
// Let's see its unit type
LexicalType luType = lexUnit.getLexicalUnitType();
// It is a LexicalType.RGBCOLOR

// And the color is...
String strColor = lexUnit.getCssText();
// Is '#46f'

// Access the individual color components.
// RGB colors are accessed as a rgb() function, so we retrieve
// the function parameters as a doubly-linked list:
LexicalUnit firstComponent = lexUnit.getParameters();

// Let's see the lexical type to see how we access the value
LexicalType firstLexUnitType = firstComponent.getLexicalUnitType();
// It is a LexicalType.INTEGER (RGB components could be REAL and PERCENTAGE as well)

// Therefore we use getIntegerValue()
int color_R = firstComponent.getIntegerValue();
// Is 0x44 hex (68 decimal)

// Second component is the next in the linked list.
// Beware that if the color was specified with commas,
// like "rgb(68, 102, 255)", the next one could be a comma!
LexicalUnit secondComponent = firstComponent.getNextLexicalUnit();
int color_G = secondComponent.getIntegerValue();
// Is 0x66 (102 decimal)

// Third component
LexicalUnit thirdComponent = secondComponent.getNextLexicalUnit();
int color_B = thirdComponent.getIntegerValue();
// Is 0xff (255 decimal)

Now let's set another property using setProperty:

/*
 * Set another property, with important priority
 */
style.setProperty("background-color", "#d0dfee7a", "important");

pCount = style.getLength();
// Now we have three properties

If you are updating any of the declared sheets —that is, the sheets returned by document.getStyleSheets()— with setProperty while you are computing styles, the changes will not apply to the computed styles until you update the cascade with a call to rebuildCascade().

Updating internal sheets

IMPORTANT: once we are done updating a style sheet, if the sheet is internal to the document (that is, it is located inside a <style> element), the actual contents of the <style> element will not be updated until you execute normalize() on that element node:

// Obtain the owner node
org.w3c.dom.Node styleNode = sheet.getOwnerNode();

// Which type is it?
short nodeType = styleNode.getNodeType();
// It is a ELEMENT_NODE (could also be a PROCESSING_INSTRUCTION_NODE)

// Get the name of the element
String nodeName = styleNode.getNodeName();
// It is "style", as expected

// Get the element content (the initial serialized style sheet)
String preContent = styleNode.getTextContent();

// Serialize the current style sheet into the inner text node
styleNode.normalize();

// Get the normalized element content (the current serialization)
String afterContent = styleNode.getTextContent();
// preContent and afterContent are different

Working with inline styles

Now we are going to obtain the inline style of an element, as given by the style attribute. In the next example, the inline style is supposed to contain the declaration of a custom property called --myColor with a value #54e.

/*
 * Access a style attribute (inline styles)
 */
// First obtain the desired element, for example:
CSSElement element = document.getElementById("someId");
// Now the inline style. It can be directly accessed
CSSStyleDeclaration inlineStyle = element.getStyle();

// How many properties are defined?
int inlinePCount = inlineStyle.getLength();
// Suppose that it is just one called --myColor

// Retrieve its name
String inlinePname0 = inlineStyle.item(0);
// It is "--myColor"

// Now get the value
CSSValue myColor = inlineStyle.getPropertyCSSValue(inlinePname0);

/*
 * Handle the custom property in myColor
 */
// Get the general value type
CSSValue.CssType type = myColor.getCssValueType();
// Being a custom property, it must be a CssType.PROXY

// But is it (or could be) a color?
Match matchCP = myColor.matches(syntaxColor);
// Match.TRUE: Yes it is for sure!

// Let's see the primitive type, although...
CSSValue.Type primiType = myColor.getPrimitiveType();
// custom properties are *always* Type.LEXICAL values

LexicalValue lvColor = (LexicalValue) myColor;
// Can we know its final value type once it is processed?
Type finalType = lvColor.getFinalType();
// Yes, as suggested by the result of calling 'matches'
// ...it is a Type.COLOR

// Let's retrieve its value
LexicalUnit lexUnitCP = lvColor.getLexicalUnit();
// First see its unit type
LexicalType luTypeCP = lexUnitCP.getLexicalUnitType();
// It is a RGBCOLOR

// And the color is...
String strlexUnitCP = lexUnitCP.getCssText();
// Is '#54e'. We could also access its components

// Let's change the value of the custom property
// We want to set '#667'
inlineStyle.setProperty(inlinePname0, "#667", null);

// Check that we successfully assigned the new value:
String newInlineValue = inlineStyle.getPropertyValue(inlinePname0);
// Yes it is '#667' as we wanted

After changing an inline style you do not need to call any method (like how normalize() was used for internal sheets) to modify the document. The serialization of (inline) style attributes is always up to date.

The small drawback is that the style attributes may not appear exactly as they were specified, but instead according to the serialization given by this library (which can be tailored a bit, for example by customizing endInlinePropertyDeclaration() in your own StyleFormattingContext, see CSS style formatting for details).

Override styles

Override styles, that come after the author style sheet in the cascade algorithm, are not part of the formal standard but are supported by this library; they are like "hidden" inline styles.

To access the override style of an element, use CSSElement's getOverrideStyle. For example:

element.getOverrideStyle(null).setCssText("padding: 6pt;");

Override styles are defined at the DocumentCSS interface description.

Configuring the cascade

Depending on your use case, you may need to set the user agent (UA) style sheet before starting to compute styles. The library supports two different UA sheets, one for STRICT ('standards') mode and another for QUIRKS. To set either of those sheets, first obtain an instance of the factory that you are using:

// Instantiate the new factory or get it from an object that you are already using.
AbstractCSSStyleSheetFactory cssFactory = ...

If you are using the DOM4J classes, you may want to do:

AbstractCSSStyleSheetFactory cssFactory = XHTMLDocumentFactory.getInstance().getStyleSheetFactory();

If you are processing HTML, css4j's default HTML5 UA sheet (based on W3C/WHATWG recommendations) should be appropriate for you:

cssFactory.setDefaultHTMLUserAgentSheet();

But if you want to set your own UA sheet, first obtain a reference to the sheet:

BaseCSSStyleSheet sheet = cssFactory.getUserAgentStyleSheet(CSSDocument.ComplianceMode.STRICT);

This is assuming the STRICT mode, i.e. that you use documents with a DOCTYPE, otherwise use QUIRKS (or you may want to set both UA sheets).

If the UA sheet already contains rules (it is empty by default), clean it:

sheet.getCssRules().clear();

And now load the new sheet:

Reader reader = ... [reads the UA sheet]
sheet.parseStyleSheet(reader, CSSStyleSheet.COMMENTS_IGNORE);
reader.close();

As the UA sheet's comments are rarely of interest at the OM level, they were ignored during the parse process (notice the COMMENTS_IGNORE flag).

Note: this implementation does not support important style declarations in the UA sheet.

Setting the user style sheet

There is also the possibility to set a user style sheet (with 'user' origin) via setUserStyleSheet:

Reader reader = ... [reads the user sheet]
cssFactory.setUserStyleSheet(reader);
reader.close();

or the URL variant:

String url = "https://www.example.com/user.css";
cssFactory.setUserStyleSheet(url, null);

Both important and normal declarations are supported in the user style sheet.

Media handling

Setting a target medium for computed styles

By default, computed styles only take into account generic styles that are common to all media. If you want to target a more specific medium, you have to use the CSSDocument.setTargetMedium("medium") method. For example, if your document has the following style sheets linked:

<link href="http://www.example.com/css/sheet.css" rel="stylesheet" type="text/css" />
<link href="http://www.example.com/css/sheet_for_print.css" rel="stylesheet" media="print" type="text/css" />

Computed styles will initially take into account only the "sheet.css" style sheet. However, if you execute the following method:

document.setTargetMedium("print");

Then all subsequently computed styles will account for the merged style sheet from "sheet.css" and "sheet_for_print.css".

This way to tie a document with a medium is not totally standard, as the W3C APIs would probably expect a DeviceFactory-related object implementing the ViewCSS interface and referencing the document, but this approach allows to isolate DOM logic inside DOM objects and keep the DeviceFactory for media-specific information only.

Media queries

The library provides an object-oriented interface to access media queries. If you obtain a MediaQueryList from a style sheet or a factory object, you can handle it like in the following example, where we create a media query and then produce a style sheet tied to it:

// We instantiate a factory based on CSSDOMImplementation,
// but could do the same with any of the other factories
AbstractCSSStyleSheetFactory cssFactory = new CSSDOMImplementation();

/*
 * Parsing a Media Query
 */
MediaQueryList mql = cssFactory.createMediaQueryList("screen and (600px <= width < 1200px)",
	null);

// How many queries we got?
int numQueries = mql.getLength();
// One

// Get the first and only query
MediaQuery mediaQ = mql.getMediaQuery(0);

// The media type
String medium = mediaQ.getMediaType();
// It is "screen"

/*
 * Have a look at the associated boolean condition(s)
 */
BooleanCondition cond = mediaQ.getCondition();

// The condition type
BooleanCondition.Type condType = cond.getType();
// It is BooleanCondition.Type.AND

// As the AND javadoc suggests, call getSubConditions()
List<BooleanCondition> andConds = cond.getSubConditions();
// andConds.size() is 2

// The first operand of the AND
BooleanCondition cond1 = andConds.get(0);
// The condition type
BooleanCondition.Type cond1Type = cond1.getType();
// It is BooleanCondition.Type.PREDICATE

// Now, following the indication from the getCondition() javadoc
// we cast the predicate to a MediaQueryPredicate
MediaQueryPredicate predicate1 = (MediaQueryPredicate) cond1;
// predicate1.getPredicateType() is MediaQueryPredicate.MEDIA_TYPE
// predicate1.getName() is "screen"

// The second operand of the AND
BooleanCondition cond2 = andConds.get(1);
// The condition type
BooleanCondition.Type cond2Type = cond2.getType();
// It is BooleanCondition.Type.PREDICATE

// Again, following the indication from the getCondition() javadoc
// we cast the predicate to a MediaQueryPredicate
MediaQueryPredicate predicate2 = (MediaQueryPredicate) cond2;
// predicate2.getPredicateType() is MediaQueryPredicate.MEDIA_FEATURE
// predicate2.getName() is "width"

// Cast predicate2 to MediaFeature
MediaFeature feature = (MediaFeature) predicate2;
// feature.getRangeType() is MediaFeaturePredicate.FEATURE_LE_AND_LT

// Obtain the first value in the LE_AND_LT range
CSSTypedValue value = feature.getValue();
// value.getPrimitiveType() is Type.NUMERIC
// value.getUnitType() is CSSUnit.CSS_PX (pixels)

// Get the numeric value
float floatValue = value.getFloatValue(CSSUnit.CSS_PX);
// Value is 600 (px)

// Now the second value in the LE_AND_LT range
CSSTypedValue value2 = feature.getRangeSecondValue();
// value.getPrimitiveType() is Type.NUMERIC
// value.getUnitType() is CSSUnit.CSS_PX

// Get the numeric value
float floatValue2 = value2.getFloatValue(CSSUnit.CSS_PX);
// Value is 1200 (px)

// Finally, create a sheet attached to that query
AbstractCSSStyleSheet sheet = cssFactory.createStyleSheet(null, mql);

// The object returned by 'sheet.getMedia()' and the 'mql' variable
// would be the same one

Style sheet sets

The library supports alternative style sheets: see CSSDocument's methods enableStyleSheetsForSet, getStyleSheetSets, getSelectedStyleSheetSet and setSelectedStyleSheetSet. For example, if you have a document with these linked sheets:

<link href="http://www.example.com/commonsheet.css" rel="stylesheet" type="text/css" />
<link href="http://www.example.com/alter1.css" rel="alternate stylesheet" type="text/css" title="Alter 1" />
<link href="http://www.example.com/alter2.css" rel="alternate stylesheet" type="text/css" title="Alter 2" />
<link href="http://www.example.com/default.css" rel="stylesheet" type="text/css" title="Default" />

Initially, sheets 'alter1.css' and 'alter2.css' will not be used to compute styles. But then you can write code like the following:

String defset = document.getSelectedStyleSheetSet();  // Sets 'defset' to "Default"
document.setSelectedStyleSheetSet("Alter 1");  // Selects the set with title "Alter 1"
document.setSelectedStyleSheetSet("Alter 2");  // Selects the set with title "Alter 2"

These methods have been removed from the DOM standard unfortunately, but you can still read the specification at the CSSOM 5 December 2013 Working Draft.

Style sheet error checking

You can check for errors and warnings in the document's sheets using the non-standard getErrorHandler method. Example:

if (document.getStyleSheets().item(0).getErrorHandler().hasSacErrors())
	... error processing / reporting

The merged style sheet obtained from the getStyleSheet method has the merged error/warning state from the document's active sheets.

A typical source of errors are the non-compliant IE hacks, like prefixing property names with an asterisk (you may want to use the proper NSAC flags when creating the factory, see Compatibility with legacy browsers), or charset rules found in the wrong place.

Parsing a style sheet

Although document's style sheet fetching and parsing is automatic with this library (for sheets referenced from a document), it is possible to manually parse rules from a source stream with the CSSStyleSheet.parseStyleSheet(Reader, short) method:

Reader re = ...
sheet.parseStyleSheet(re, CSSStyleSheet.COMMENTS_AUTO);

When the second argument is COMMENTS_IGNORE, the comments in the source stream are ignored when parsing (see Accessing Style Sheet Comments).

Important notes:

  • Parsing a style sheet won't produce a DOMException unless the error handler (which you can customize) does raise it.
  • The new CSS rules found in the source stream are added to the already present ones, i.e. the sheet is not reset by this method (although the error handler is), so if you want to refill a sheet you need to clear the rules before parsing:

    sheet.getCssRules().clear();
    

Security model

Linked style sheets accessed through DOM are automatically fetched, but if your LINK element or @import rule point to a file: or jar: URL, the style sheet won't be retrieved unless you set the documentURI of your document to one of those URIs.

A similar reasoning applies to the contents of the href attribute in the BASE element. If you load a document that contains a <base href="file:///some/path">, that won't take effect until you call setDocumentURI() to set a URI with a file: or jar: scheme. This prevents denial of service attacks that could cause thread starvation (for example by linking to file:///dev/zero) or deplete the pool of entropy in your server (file:///dev/random), as well as jar: decompression bombs.

Compatibility with legacy browsers

Today's style sheets often contain non-conformant styles that target specific versions of old web browsers, like Internet Explorer. Several web sites contain information about that, including:

Although it could be argued whether those hacks should be used or not, the point is that actual style sheets do contain them, so this library supports them.

By default, a factory is configured to use a flagless NSAC parser which would produce an error on any of those non-standard constructs, but a set of compatibility flags can be specified in the constructors for the factory implementations. The different flags are documented in the NSAC javadocs.

The flags available at the time of this guide's update are the following:

  • STARHACK. When set, the parser will handle asterisk-prefixed property names as accepted, normal names.
  • IEVALUES supports values ending with \9 or \0, as well as progid filters and IE expressions.
  • IEPRIO allows values ending with the '!ie' priority hack.
  • IEPRIOCHAR accepts values with an '!important!' priority hack (note the '!' at the end).

The object model manages these compatibility values in parallel to standard ones. For example, after parsing this declaration with IEVALUES set:

width: 900px; width: 890px\9;

its serialization would be identical (if the flag was set correctly):

width: 900px; width: 890px\9;

but the declaration's length shall be only 1. And computed styles only use the standard values unless there are no alternatives (no standard value was set). The workings are similar for IEPRIO and IEPRIOCHAR:

width: 890px !ie;
width: 890px !important!;

with the last one being handled as of important priority. Values created by IEPRIOCHAR are never used in computed styles.

Instead, declarations including asterisk-prefixed property names (created by STARHACK) always increase the declaration's length. For example, the length of the following declaration would be 2:

width: 900px;
*width: 890px;

If you want to use these flags at the NSAC level (instead of the Object Model), you may want to read the 'Parser Flags' section in the NSAC package description, as well as the documentation for the individual flags in Parser.Flag.

CSS style formatting

The serialization of the cssText attribute in rules and style declarations can be customized with an implementation of the StyleFormattingContext interface. You can set your StyleFormattingFactory (which produces your customized formatting context) to the sheet factory with the CSSStyleSheetFactory.setStyleFormattingFactory method, or subclass your style sheet factory and override the createDefaultStyleFormattingFactory method.

Look at the DefaultStyleFormattingContext class for an example of a formatting context implementation.

To customize the serialization of computed styles, you want to override the createComputedStyleFormattingContext() method. The convenience RGBStyleFormattingFactory serializes computed colors as RGB and may serve as a customization example.

String value formatting

There is also the possibility to customize the default serialization of string values, with the CSSStyleSheetFactory.setFactoryFlag(byte) method. You can set two flags that govern which quotation you prefer, or keep the default behaviour:

  • Default: Try to keep the original quotation (single or double quotes), unless the alternative is more efficient.
  • STRING_DOUBLE_QUOTE: Use double quotes unless single quotes are more efficient (when the string contains more double quotes than single).
  • STRING_SINGLE_QUOTE: Use single quotes unless double quotes are more efficient (when the string contains more single quotes than double).

Minification

The library provides methods for minified serialization at different levels (values, declarations, rules, lists, etc) but one generally wants to work at the style sheet level, in which case a generic CSS minification method could look like the following:

import io.sf.carte.doc.dom.CSSDOMImplementation;
import io.sf.carte.doc.style.css.CSSStyleSheet;
import io.sf.carte.doc.style.css.nsac.Parser;
import io.sf.carte.doc.style.css.om.AbstractCSSStyleSheet;
import io.sf.carte.doc.style.css.om.AbstractCSSStyleSheetFactory;

public static String minifyCSS(String css) {
    // Instantiate any style sheet factory, with parser flags allowing IE hacks
    AbstractCSSStyleSheetFactory cssFactory = new CSSDOMImplementation(
        EnumSet.allOf(Parser.Flag.class));
    // Create an empty style sheet
    AbstractCSSStyleSheet sheet = cssFactory.createStyleSheet(null, null);
    // Parse and check for return value
    try {
        if (sheet.parseStyleSheet(new StringReader(css),
                CSSStyleSheet.COMMENTS_IGNORE)) {
            // Parsed without errors
            return sheet.toMinifiedString();
        }
    } catch (IOException e) {
        // Cannot happen with StringReader
    }
    // Error detected, return the source
    return css;
}

Beware of YUI Compressor

For further minification, you could choose to apply a specialized minification utility to the output of toMinifiedString(), and YUI Compressor was a popular choice. But that library is not maintained anymore and the latest release was done in 2013: if you decide to apply the YUI Compressor anyway, beware that it will break calc() expressions and other stuff.

Although there are a few post-2.4.8 updates that were applied to the Compressor's master branch on its Github repository, that code is completely broken: it does not compile and at least one of the new regular expressions is wrong.

Accessing style sheet comments

CSS style sheets often have comments, like:

/* This is a preceding comment */
p {color: blue; } /* This is a trailing comment */

(XML-style comments can also be present in a style sheet, but both NSAC and the CSSOM skip them.)

There is no standard CSSOM API for accessing comments in style sheets, but the CSSRule.getPrecedingComments() and CSSRule.getTrailingComments() methods are provided for that:

StringList pcomments = document.getStyleSheets().item(0).getCssRules().item(3).getPrecedingComments();
StringList tcomments = document.getStyleSheets().item(0).getCssRules().item(3).getTrailingComments();

Other API models were considered for object-model comments, like having a special type of comment rule, but stylesheet-level comments are often related to a rule and if that rule is moved or deleted, those stand-alone comments would have the potential to create a lot of confusion.

By default, comments are parsed with the COMMENTS_AUTO mode, which should be appropriate for human-readable sheets like the one shown above. But a lot of sheets are serialized in a way that there are no newline characters (or only a few). For these cases, COMMENTS_PRECEDING could be used in parseStyleSheet(Reader,short), and all the comments will be considered as belonging to the next rule. With COMMENTS_IGNORE, all comments found while parsing the sheet will be ignored.

The comments preceding/trailing any rule will be included in the text returned by the sheet's AbstractCSSStyleSheet.toString() and toStyleString() methods, while other comments (located at places that cannot be easily related to a rule) are lost.

In css4j version 1.0, comments that were intended to apply to the previous rule may be assigned to the next one, but this is addressed in version 2.0 where NSAC 2.0 allows a more accurate approach.

Comments in the default HTML UA style sheet are not available, as the parser is instructed to ignore them when parsing.

Providing device information for style computation

One of the most important functionalities in the library is the ability to compute styles for a given element.

In practice, to obtain the 'computed' or 'used' values required for actual rendering, a box model implementation is needed, and also device information. The library provides a simple box model that could be used, but the details of the rendering device can be more difficult.

Depending on the use case, the target device may not be the same one where the library is running (and some exact details hence not available). To help in the computation, this library provides a few helper interfaces. The most important are:

  • DeviceFactory. Delivers the relevant abstractions for a requested medium: StyleDatabase and CSSCanvas (also provides the objects required by media queries to work).
  • StyleDatabase. Provides medium-specific information like available fonts and colors.
  • CSSCanvas. Has knowledge of medium-specific information that depends (or may depend) on a specific viewport, like supported media features. It is linked to a viewport, if there is any. This interface is used to determine the state of active pseudo-classes.
  • Viewport. It represents a viewport defined as per the CSS specifications.

The differences between style databases and canvases can be subtle, and for some media features it could be argued that they belong to one or the other. The basic idea is that style databases should be relatively easy to implement for a given medium, while canvases are probably only going to exist if there is an actual rendering engine implemented.

For an example implementation of a DeviceFactory and related classes, please check out MyDeviceFactory, MyStyleDatabase and MyCanvas in EchoSVG's CSSTranscodingHelper, or AWTStyleDatabase in the css4j-awt project.

Create an AWT font/color from a computed style (css4j-awt module)

Once you have a computed style, on systems where AWT is available you can create an AWT font or color with the createFont and getAWTColor static methods found in the AWTHelper class:

CSSComputedProperties style = ...
java.awt.Font font = AWTHelper.createFont(style);
CSSTypedValue cssColor = (CSSTypedValue) style.getPropertyCSSValue("color");
java.awt.Color color = AWTHelper.getAWTColor(cssColor);

Note that AWTHelper.createFont requires a computed style as argument, while the AWTHelper.getAWTColor method can create colors from any typed value that represents a color, regardless of it coming from a computed style or a style declaration (the getPropertyCSSValue shown above does not require the style to be computed).

These classes and methods belong to the css4j-awt module (which is the only css4j module that has a dependency on AWT).

CSS3 support

CSS3 (defined as "all CSS after CSS2.1") is partially supported by the library; the following table summarizes the support (supporting generally means that it can decompose shorthands, initial values are known, the rules are in the Object Model, etc.):

CSS3 Spec NameSupportObservations
Animations 2YesSupports the @keyframes rule and decomposes the the animation shorthand.
Background / Border 3Yes
Color 4Yes
Color 5Partial Supports full level 4 but only color-mix() from level 5 (this is aligned with web browser support at the time of writing).
Conditional Rules 4Yes
Fonts 4PartialHas the @font-face and @font-feature-values rules, also decomposes the the font shorthand.
Media Queries 4 PartialEvent handling with addListener/removeListener is not supported. Relies on CSSCanvas implementations to match/unmatch media features.
Selectors 4Yes
Lists and Counters 3PartialDecomposes the list-style shorthand. Limited support by the simple box model.
Multi-column LayoutPartialDecomposes the columns shorthand. Not supported by the simple box model.
MaskingPartialDecomposes the mask shorthand. Not supported by the simple box model.
TransitionsYesDecomposes the transition shorthand.
Values 4YesAdvanced attr() from level 5 also has early support.
VariablesYesvar() is supported in computed styles.
CSS Properties and Values APIYesFull support (also in computed styles).
Grid / Template / Alignment PartialLegacy gap properties (grid-row-gap, grid-column-gap, and grid-gap) are not supported, although the longhands can be used if declared explicitly). Not supported by the simple box model.

Pseudo-classes

The following pseudo-classes are supported by the library: first-child, last-child, only-child, nth-child(), nth-last-child(), first-of-type, last-of-type, only-of-type, nth-of-type(), nth-last-of-type(), dir, lang, any-link, link, visited, target, root, empty, blank, disabled, enabled, read-write, read-only, is, where, has, not, placeholder-shown, default, checked and indeterminate.

State pseudo-classes like hover are supported through the CSSCanvas.isActivePseudoClass(CSSElement, String) method.

Java™ Runtime Environment requirements

The classes in the binary packages have been compiled with a Java compiler with 1.8 compiler compliance level, except for the module-info file which targets Java 11.