Flex CSS, while a fine idea, is riddled with limitations. Most notable is probably its lack of proper cascading, but equally unfortunate is that it’s a system defined by programmatic ActionScript APIs. There is a textual way of defining styles, but the Flex compiler, mxmlc, is used to turn this style sheet language into ActionScript, which is in turn compiled into a style module you can load at runtime.
The textual stylesheet language is the black sheep of the family, supporting only a limited set of the features available with runtime Flex styles. For example, take the LineSeries from the data visualization package that comes with FlexBuilder Pro. It defines a number of styles in terms of IFill or IStroke instances. How do you specify IFill or IStroker instances in Flex CSS, so that you can configure look and feel separately from application logic, or alter styles without redeploying the entire application? You don’t. You curse the mxmlc designers for not allowing richer CSS compilation. You can only specify numbers; Strings; embedded images, sounds, and fonts; and references to classes through the ClassReference directive. No instances of any other objects.
Well, what if you’re mad as hell and you won’t take it anymore?
I’m working on a visualization framework with some dynamic styling, and being able to specify “default” styles externally through standard Flex CSS is really, really important. It makes the model much simpler. I was banging my head against the Flex CSS wall and then the scales fell from my eyes:
- Mxmlc compiles Flex CSS files to ActionScript.
- That ActionScript must simply be building standard
mx.styles.CSSStyleDeclaration
classes.
- Mxmlc has a
keep-generated-actionscript
attribute.
We take global.css:
AxisRenderer {
labelAlign: "center";
}
compile with keep-generated-actionscript=true
, and we get:
core_properties.as
global.as
global_mx_core_FlexModuleFactory
styles_properties.as
Looking at these, only global.as is interesting. It has a lot of boilerplate code, but then something familiar:
...
var style:CSSStyleDeclaration;
var keys:Array;
var selector:String;
var effects:Array;
var addedEffects:Array;
selector = "AxisRenderer";
style = StyleManager.getStyleDeclaration(selector);
if (!style)
{
style = new CSSStyleDeclaration();
StyleManager.setStyleDeclaration(selector, style, false);
selectors.push(selector);
}
keys = overrideMap[selector];
if (keys == null)
{
keys = [];
overrideMap[selector] = keys;
}
style.mx_internal::setStyle('labelAlign', "center");
keys.push("labelAlign");
...
This seems fairly routine. It builds a CSSStyleDeclaration programmatically, and then adds values according to our source CSS file. And the style values seem to be just passed along. If only we could somehow “unquote” that style value, we could inject arbitrary ActionScript expressions that would be evaluated before being assigned as style values.
“Unquoting” has a venerable history in programming, so it’s not too crazy an idea. Bash does it (echo "hello $user"
), PHP does it (though it’s been a while since my PHP days and I don’t remember the syntax–something along the lines of bash if memory serves), and of course Lisp does it ((let ((user 'maciek)) `(hello ,user))
. The tricky part is the implementation.
Fortunately, it’s not really that tricky. With the keep-generated-actionscript
flag, mxmlc does most of the work for us. All we need to do is a little post-processing, and re-build the ActionScript into a swf, ignoring the intermediate output swf. Now, everybody stand back. I know regular expressions (not well, mind you, but I get by).
Since Flex has ant task wrappers for its compilers, the easiest way to approach this was to actually build a custom ant filter. Filters can be applied when moving or copying files and they can perform line-by-line transformations on a file. Looking at the generated ActionScript above, we try to find lines matching the calls to setStyle, look for an arbitary special escape sequence that we introduce in String style values (I chose a leading comma, in homage to Lisp), and strip quotes from values matching that escape sequence, to let them execute as arbitrary ActionScript code.
import org.apache.tools.ant.filters.TokenFilter;
public class StyleFilter implements TokenFilter.Filter {
public String filter(String token) {
return token.
replaceAll("(?<=style\\.mx_internal::setStyle\\('[^']{1,100}', )\",([^\"]+)\"(?=\\);)",
"$1").replaceAll("^import mx\\.core\\.mx_internal;$",
"import mx.core.mx_internal;\nimport mx.graphics.*;");
}
}
The regular expression is a little daunting. It can probably be optimized for both performance and correctness (I know I miss edge cases), but it works for now. Briefly,
"(?<=style\\.mx_internal::setStyle\\('[^']{1,100}', )\",([^\"]+)\"(?=\\);)"
: positive lookbehind for the setStyle()
call we see above–everything up to and including the comma after the style name. Note that lookbehind won’t let us use an unbounded number of characters due to regex mechanics, so I arbitrarily cap it at 100. I’m sure there are better ways around this, but you’ll seldom see 100-character style names in the wild. Note also that we need to escape the period and parentheses, and escape the backslashes escaping them, since these are Java Strings.
"(?<=style\\.mx_internal::setStyle\\('[^']{1,100}', )\",([^\"]+)\"(?=\\);)"
: the actual quoted value. This expression won’t work on escaped embedded quotes, but since ActionScript supports both single and double quotes, you can typically use single quotes instead. Note that we match the whole quoted value, with the leading comma that signifies the value is to be “de-quoted”, but we only capture the unquoted value, sans leading comma.
"(?<=style\\.mx_internal::setStyle\\('[^']{1,100}', )\",([^\"]+)\"(?=\\);)"
: We make sure the line ends as we expect with positive lookahead. Not really necessary, but a nice sanity check
We replace this whole thing with the first captured group.
Note also the second replace. That’s there to add an import statement for the mx.graphics
package, where Stroke and SolidFill, the classes I am most interested in live. Unfortunately, Flex requires import statements even for fully qualified class names (whereas in, e.g., Java, you can say something like System.out.println instead of having to import it explicitly). With this, a style like:
AxisRenderer {
labelAlign: center;
axisStroke: ',new Stroke(0x0000ff, 2)';
}
will look like this before the processing step:
style.mx_internal::setStyle('labelAlign', "center");
keys.push("labelAlign");
style.mx_internal::setStyle('axisStroke', ",new Stroke(0x0000ff, 2)");
keys.push("axisStroke");
and this after processing:
style.mx_internal::setStyle('labelAlign', "center");
keys.push("labelAlign");
style.mx_internal::setStyle('axisStroke', new Stroke(0x0000ff, 2));
keys.push("axisStroke");
This means you can put arbitrary ActionScript expressions in your style values in CSS. Compiling this yields a standard Flex style module that can be loaded like any other.
Does the special treatment for the mx.graphics package mean that you need to extend the filter with more imports if you need another package? Not necessarily. ActionScript has anonymous functions and imports can be done locally, so you can have CSS values that would give any self-respecting designer a heart attack:
AxisRenderer {
axisStroke: ',function():Object { import com.example.stroke.CustomStroke; return new CustomStroke(0x0000ff, 2); }()';
}