Replace the JavaFX CSS parser with a W3C-compliant parser that is easier to maintain, and introduce a CSS Syntax API that paves the way for more feature enhancements in the future.
It is not a goal to deliver additional CSS features (aside from variable substitution, more on that later).
In order to understand the motivation for this proposal, it is necessary to understand how CSS is currently implemented in JavaFX:
Parsing begins with CssParser
, which takes in a CSS file and returns a Stylesheet
object. This object contains all rules of the CSS file, each of which contain their respective declarations. The content of a Declaration
is a ParsedValue
, which is an opaque class that contains ad-hoc content.
When a style is applied to a StyleableProperty
, the CSS engine passes the ParsedValue
to the StyleConverter
associated with that property, which converts it to a JavaFX object.
CssParser
not only parses the CSS file, it also interprets its contents. For example, consider the following CSS file:
.button {
-fx-background-color: gray;
}
When CssParser
encounters this declaration, it converts the value "gray" to a Paint
object and puts it in a ParsedValue
. But how does the parser know that "gray" is a Paint
? (This is not only a theoretical question: "gray" is also a valid value for -fx-font-smoothing-type
.)
When the parser interprets a value, it takes hints from the name of the property, and then assumes that the value should be a Paint
. However, if the value is encountered in a -fx-font-smoothing-type
declaration, the value is encoded as String
. This approach has the following disadvantages:
- It entangles the names of properties (which are defined by controls) with the core CSS parser.
- It requires lots of hard-coded special cases. The source code of
CssParser
contains lots of comments like:// TODO: Figure out a way that these properties don't need to be special cased.
- It is not extensible. A third-party control cannot "hook into"
CssParser
to add more special cases for its own properties and their respective grammar. - It is unsound. If a third-party control declares a property with the same name, but a different meaning than one of the hard-coded names, CSS styling might fail.
The solution is to split the problem into two different phases: parsing, which happens when the CSS file is converted into a Stylesheet
, and interpretation, which happens when a declaration is applied to a StyleableProperty
(this is also called "value computation" by the CSS spec). It is only during value computation when we can figure out what the property value means according to the grammar of the property.
A W3C-compliant parser will be implemented (see CSS Syntax Module Level 3). The output of the parser is a CSS token tree. This token tree is modelled as an algebraic data structure (using sealed interfaces and records/classes) in the new package javafx.css.syntax
.
For example, consider the following declaration:
-fx-background-color: rgb(100% 50% 0%);
This corresponds to the following data structure:
<ident>-fx-background-color
<colon>
<function>rgb ( <percentage>100 <percentage>50 <percentage>0 )
<semicolon>
The interfaces and classes in the javafx.css.syntax
package constitute a public-facing API that allows third-party authors to consume parsed CSS values, something that would previously require hard-coded additions to CssParser
. It also allows us to remove all special-casing from the CSS parser.
The fundamental type of the algebraic data structure is a ComponentValue
:
public sealed interface ComponentValue
permits Block, AtKeywordToken, BadStringToken, BadUrlToken, ColonToken, CommaToken, CDCToken, CDOToken,
DelimToken, HashToken, IdentToken, NumericToken, RightCurlyToken, RightParenToken,
RightBracketToken, SemicolonToken, StringToken, UrlToken, WhitespaceToken {}
A Block
is a composite component value that contains other component values:
public sealed interface Block
extends List<ComponentValue>, ComponentValue
permits SimpleBlock, Function {}
All CSS expressions are represented according to the W3C specification.
When a style is applied to a JavaFX property, CssStyleHelper
looks up the computed value for the property. Currently, this is a ParsedValue
, containing an ad-hoc composition of objects. This value is passed to the StyleConverter
associated with the JavaFX property, which converts it to a JavaFX object.
This is the phase where we'll take a little toll on our incompatibility budget, because we will need to change how a StyleConverter
works. We will later discuss why changing the API here might not be as serious as one will initially assume.
Currently, the main workhorse is the convert
method:
class StyleConverter {
...
public T convert(ParsedValue<F, T> value, Font font);
...
}
As discussed, we can't use the ParsedValue
implementation of the current CSS parser, as it has all of the unsound assumptions baked into it. We need to change the method to consume the new Syntax API instead:
class StyleConverter {
...
public T convert(ComponentValue value, Font font);
...
}
The ComponentValue
in the method signature is a CSS token tree (or a single token) corresponding to the declared value of the property. For example, assume a declaration -fx-pref-height: 2em
. The value part of this declaration (2em
) will be passed into the style converter associated with -fx-pref-height
, which converts it to a Number
.
This is what a style converter looks like (simplified), showcasing how the Syntax API will be used:
class SizeConverter extends StyleConverter<Number> {
@Override
public Number convert(ComponentValue value, Font font) {
return switch (value instanceof Block block ? block.getFirst() : value) {
case NumberToken number -> number.value();
case DimensionToken dimension -> {
var sizeUnits = switch (dimension.unit()) {
case "%" -> SizeUnits.PERCENT;
case "em" -> SizeUnits.EM;
...
case "s" -> SizeUnits.S;
case "ms" -> SizeUnits.MS;
default -> ...;
};
yield sizeUnits.pixels(1, dimension.value().doubleValue(), font);
}
default -> ...;
};
}
}
Since style converters work on CSS token trees, they allow easy composition. For example, suppose a style converter is created to parse effects like dropshadow
:
dropshadow( <blur-type> , <color> , <number> , <number> , <number> , <number> )
Instead of interpreting the <color>
component itself, it can simply pass the CSS subtree for this component to ColorConverter
, which already has an implementation to parse the color grammar.
The current CSS implementation of JavaFX allows color values (and only those) to reference custom properties:
.root {
--my-custom-color: red;
}
.button {
-fx-background-color: --my-custom-color;
}
The new implementation must retain this capability, but work with the new architecture. In order to solve this, we will implement the var()
function as described in CSS Custom Properties for Cascading Variables Module Level 1.
The crucial difference between color lookups and the var()
function is that the latter works on the level of CSS tokens, which is the foundation of the proposed new architecture. This means that we will not only get variable substitution for colors, but for all valid CSS expressions:
.root {
--my-insets: 1em 1em 1em 1em;
}
.button {
-fx-padding: var(--my-insets);
}
However, we will need -- at least as a temporary measure -- a way to recognize the old syntax without the presence of the var()
function. This will be a postprocessing step of the new CSS parser, where we will use our closed-world knowledge of known property names to surround color lookups with var()
where necessary.
While changing the API of StyleConverter
sounds like a lot, the closed-world assumption of the current architecture works to our advantage. It is highly unlikely that developers would implement StyleConverter
themselves (as of today), as this would require special-case code in CssParser
for any non-trivial style converter.
What's far more likely is that developers use one of the existing style converters when defining styleable properties on their controls. For example, a custom control with a fill
property would define the CssMetaData
for this property like so:
static final CssMetaData<MyControl, Paint> FILL =
new CssMetaData<>(
"fill",
PaintConverter.getInstance(),
Color.BLACK) {
...
};
Even though the API of StyleConverter
changes, the declaration of CssMetaData
doesn't change. This allows most users of style converters to compile without changing any code.