Fast, Java-based, plugin-oriented CSS3+ parser.
Omakase (お任せ o-ma-ka-say) has very few dependencies and doesn't need to execute Ruby or JavaScript code. It runs entirely in Java.
Omakase can work as a parser, preprocessor, linter, minifier or all four. It gives you an AST that can be modified and validated.
Omakase is engineered for speed. Compared to other Java-based, modern CSS parsers, Omakase parses ~300 lines of code up to 3x faster and ~1000 lines up to 5x faster. For very large files (over 10k lines) Omakase can potentially save hundreds of ms.
Omakase is plugin-oriented, which means you can create plugins to modify, add, remove, validate or lint any aspect of the CSS.
Plugins can also be used to extend the CSS grammar and syntax. This allows you to create common features like variables, mixins and nesting using whatever format you like. In fact much of the built-in functionality uses plugins.
Omakase bundles several plugins, most notably:
Omakase is built 100% solely for parsing CSS, which means that the error messages are often more specific and easier to understand than from other parsers created from generic parser generators.
Omakase is focused on runtime usage, and provides special features to make runtime parsing even faster, namely a 2-level parsing strategy. The first level separates the source into selectors, declarations and at-rules only. The (optional) second level breaks each unit down further, for example into class selectors, type selectors, id selectors, etc... This allows you to do a full pass during build time, but during runtime only parse the aspects of CSS that need to be reworked. This speed improvement is on top of the general performance numbers mentioned above.
You can also parse snippets of CSS on-the-fly, such as a single selector or declaration value.
Omakase is hosted on Maven Central. You can add it as a Maven dependency in your pom.xml:
<dependency>
<groupId>com.salesforce</groupId>
<artifactId>omakase</artifactId>
<version>1.4.4</version>
</dependency>
Be sure to use the latest version, which is displayed at the top of this readme.
You can alternatively build jars from source by cloning this project locally and running mvn install
from the project root (must have Java and Maven installed). Then use the jars as appropriate for your project.
All parsing starts with the Omakase
class. The CSS source is specified using the #source(CharSequence)
method, optional plugins are then registered, and then parsing is performed with a call to #process()
. An example of the most basic form of parsing is as follows:
Omakase.source(input).process();
You will almost always include one or more plugins though. Plugins are used for output/minification, automatic vendor prefixing, modifications to the AST, custom linting, and more.
Unless you are specifically optimizing for performance, you should at least add the StandardValidation
plugin, and it should be last, after other plugins.
Note that only one instance of a plugin can be registered per parsing operation.
Use the StyleWriter
plugin to write the processed CSS:
StyleWriter verbose = StyleWriter.verbose();
StandardValidation validation = new StandardValidation();
Omakase.source(input).use(verbose).use(validation).process();
String out = verbose.write();
You can also write to an Appendable
StyleWriter verbose = StyleWriter.verbose();
StandardValidation validation = new StandardValidation();
Omakase.source(input).use(verbose).use(validation).process();
StringBuilder builder = new StringBuilder();
String out = verbose.writeTo(builder);
By default, CSS is written out in inline mode. Other available modes include verbose and compressed. Verbose mode will output newlines, spaces, comments, etc... Inline mode will write each rule on a single line. Compressed mode will eliminate as many characters as possible, including newlines, spaces, etc...
StyleWriter verbose = StyleWriter.verbose();
StyleWriter inline = StyleWriter.inline();
StyleWriter compressed = StyleWriter.compressed();
In some cases you may want to write out an individual, stand-alone syntax unit:
Declaration declaration = new Declaration(Property.DISPLAY, KeywordValue.of(Keyword.NONE));
String output = StyleWriter.inline().writeSingle(declaration);
You can also override how any individual syntax unit is written. For more information see the Custom writers section below.
In Omakase, validation refers to both actual syntax validation (e.g., that the arguments to an rgba
function are well-formed) as well as what is commonly known as linting (e.g., that fonts are specified using relative units instead of pixels).
All validation is written and registered as plugins. To enable the standard validations, register an instance of the StandardValidation
plugin:
Omakase.source(input).use(new StandardValidation()).process();
This auto-refines every selector, declaration and at-rule (see the AutoRefine section below for more information on refinement) and registers the standard list of built-in validators.
Keep in mind that validation methods will always be invoked after rework methods, but otherwise they will be executed in the order that the plugin class was registered.
You can also add your own custom validators. For examples see the Custom validation section below.
Note that basic CSS grammar is verified independently of any plugins as part of the core parser.
When registering plugins there are important details to keep in mind:
- Only one instance of a plugin can be registered.
- Subscription methods will be executed in the order that its plugin class was registered.
- All
@Rework
subscription methods will be executed before@Validate
, regardless of the order in which the plugins were registered. Essentially this means validation always happens after rework modification is fully completed.
The SyntaxTree
plugin is an extremely simple plugin that only grabs and stores a reference to the parsed Stylesheet
object. It's an easy way for you to get access to the Stylesheet
object without having to write a custom plugin.
SyntaxTree tree = new SyntaxTree();
Omakase.source(input).use(tree).process();
Stylesheet stylesheet = tree.stylesheet();
System.out.println("#statements = " + stylesheet.statements().size());
The AutoRefine
plugin is responsible for automatically refining all or certain Refinable
objects. Currently this includes Selector
, Declaration
, RawFunction
and AtRule
. Refinement refers to the process of taking a generic syntax string (e.g., ".class > #id") and parsing out the individuals units (e.g., ClassSelector
, Combinator
, IdSelector
).
Unless refinement occurs the syntax object may contain invalid CSS. For example a Selector
may contain raw content consisting of ".class~!8391", but no errors will be thrown until it is refined. The AutoRefine
plugin can do this automatically:
AutoRefine all = AutoRefine.everything()); // refine everything
AutoRefiner selectors = AutoRefine.only(Match.SELECTORS); // refine selectors
AutoRefiner declarations = AutoRefine.only(Match.DECLARATIONS); // refine declarations
Using StandardValidation
automatically includes AutoRefine
unless otherwise configured.
You may be wondering when you wouldn't want auto refinement. The main use-case is during runtime or other performance-sensitive environments.
You first parse the CSS with full refinement. This ensures you actually have valid CSS. You can store this preprocessed source code in memory or on the filesystem. During runtime, you can parse the CSS again to perform dynamic substitutions. However this time, since you have already ensured that the CSS is valid, there is no need to parse more than what is necessary to perform the dynamic substitions. You can simply refine only those selectors or declarations that you need and nothing more. This will result in faster parsing performance. For more information on this see the section on conditional refinement.
Conditionals allow you to vary the CSS output based on specific true conditions. Here's an example:
.button {
background: linear-gradient(#aaa, #ddd);
}
@if (ie7) {
.button {
background: #aaa;
}
}
If "ie7" is passed in as a true condition then the block will be retained, otherwise it will be removed.
To use conditionals, register the Conditionals
plugin:
Conditionals conditionals = new Conditionals("ie7");
Omakase.source(input).use(conditionals).process();
You can manage the set of true conditions to print out variations of the CSS:
Conditionals conditionals = new Conditionals();
StyleWriter writer = StyleWriter.compressed();
Omakase.source(input).use(conditionals).use(writer).process();
// ie7
conditionals.config().replaceTrueConditions("ie7");
String ie7 = writer.write();
// firefox
conditionals.config().replaceTrueConditions("firefox");
String firefox = writer.write();
// chrome
conditionals.config().replaceTrueConditions("webkit", "chrome");
String chrome = writer.write();
You can also use logical negation and logical or operators in the CSS:
@if (!ie7) {
.button {
display: inline-block;
}
}
@if (ie8 || ie9 || ie10) {
.button {
margin: 7px;
}
}
Finally, if you would like to enable conditionals and validate them but hold off on actually evaluating them, you can specify passthroughMode
as true:
new Conditionals("ie7").config().passthroughMode(true);
new Conditionals(true).config().addTrueConditions("ie7"); // same as above
Of course, any string can be used and referred to as a true condition, not just browsers. Finally, note that the default behavior is to automatically convert all conditions found in the input as well as the specified true conditions to lower-case. Thus, usage is not case-dependent.
The ConditionalsCollector
plugin can be used when you need to know what conditions were actually used in the input CSS, say, to determine what CSS variations you need to write.
StyleWriter writer = StyleWriter.compressed();
Conditionals conditionals = new Conditionals();
ConditionalsCollector collector = new ConditionalsCollector();
Omakase.source(input).use(conditionals).use(collector).use(writer).process();
Map<String, String> variations = new HashMap<String, String>();
variations.put("default", writer.write());
for (String condition : collector.foundConditions()) {
conditionals.config().replaceTrueConditions(condition);
variations.put(condition, writer.write());
}
Note that ConditionalsCollector
automatically registers an instance of the Conditionals
plugin as well. If you are explicitly adding the Conditionals
plugin, it must be registered before the ConditionalsCollector
instance.
The ConditionalsValidator
plugin can be used to ensure only certain conditions can be used in the CSS:
ConditionalsValidator validation = new ConditionalsValidator("ie8", "ie9", "ie10", "chrome", "firefox");
Omakase.source(input).use(validation).process();
Note that ConditionalsValidator
automatically registers an instance of the Conditionals
plugin as well. If you are explicitly adding the Conditionals
plugin, it must be registered before the ConditionalsValidator
instance.
If you are in the unfortunate situation of using crappy legacy IE filters then the UnquotedIEFilterPlugin
must be registered, otherwise syntax errors will occur.
UnquotedIEFilterPlugin ieFilters = new UnquotedIEFilterPlugin();
Omakase.source(input).use(ieFilters).process();
Note that quoted IE filters do not require this plugin. An example of an unquoted IE filter:
filter: progid:DXImageTransform.Microsoft.Shadow(color='#969696', Direction=145, Strength=3);
compared to quoted:
-ms-filter: "progid:DXImageTransform.Microsoft.Shadow(color='#969696', Direction=145, Strength=3)";
This plugin must be registered before StandardValidation
or AutoRefine
.
The Prefixer
plugin enables automatic vendor prefixing. It will analyze all prefixable selectors, properties, at-rules
function and keyword names, and automatically prepend prefixed-equivalents based on the specified level of browser support. For example:
.class {
border-radius: 3px;
}
gets transformed into:
.class {
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
The Prefixer
plugin determines which prefixes to actually use based on whether the browsers you'd like to support actually need them. It maintains a list of every browser version and which properties they require prefixed to accomplish this. The data is retrieved from the famous caniuse.com website database.
Here is how you would register the Prefixer
plugin using the default browser level support:
Prefixer prefixer = Prefixer.defaultBrowserSupport();
Omakase.source(input).use(prefixer).process();
The default browser version support includes the last six versions of iOS Safari, last five versions of Chrome and Firefox, last three of Android, IE 7+, and the latest versions of Safari, IE Mobile and Opera Mini.
You can specify an alternative set of browsers to support as well:
Prefixer prefixing = Prefixer.customBrowserSupport();
prefixing.support().all(Browser.IE);
prefixing.support().latest(Browser.FIREFOX);
prefixing.support().browser(Browser.SAFARI, 6.1);
prefixing.support().last(Browser.SAFARI, 2);
Omakase.source(input).use(prefixing).process();
This is cumulative, so you can also add extra support to the defaults instead.
To manually update the prefix data, see the Scripts section below. Updating is a one-line shell command, and after an update the processed CSS automatically reflects any changes right away. This can be more efficient than using a mixin to handle vendor prefixes, as you would have to constantly check each prefixable property, selector, etc... to see if a prefix is still required.
Note that the Prefixer
plugin will not trigger refinement of a selector, declaration or at-rule just to check if a prefix is needed. This means you need to register AutoRefine
(or StandardValidation
) if you would like all selectors, declarations, etc... to be considered. See the AutoRefine section above for more information.
The Prefixer
plugin works well with existing CSS that is already littered with various vendor prefixes. By default, if a prefix is already present then it will be preserved as-is. That is, a duplicate prefix will not be added. This allows you to turn on and use the plugin right away without having to clean up your CSS file. It can even be a way for you to specify a value for the prefixed declaration that differs from the unprefixed one.
On the other hand, in many cases it will be more performant to actually have all unnecessary prefixes removed. You can do this with the prune
method.
For example, take the following:
.class {
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
-ms-border-radius: 3px;
-o-border-radius: 3px;
border-radius: 3px;
}
Many people still have CSS that looks like this, without realizing that border-radius has not required a prefix for webkit
or moz
for a very long time, and the ms
and o
prefixes were never actually needed at all.
Turn on pruning to have these prefixes automatically removed:
Prefixer prefixer = Prefixer.defaultBrowserSupport().prune(true);
Then go back and update your CSS files to remove them as well at your leisure.
Similarly, sometimes people end up with CSS that looks like this:
.class {
-webkit-border-radius: 3px;
border-radius: 3px;
-moz-border-radius: 3px;
}
It's always best practice for the unprefixed version to be last, that way if the browser supports the property unprefixed that is the one it uses. You can turn on rearranging to ensure that any prefixes that exist in the source file are moved around to make this true:
Prefixer prefixer = Prefixer.defaultBrowserSupport().rearrange(true);
Here's a list of what's currently supported:
At Rule
----------------------------
keyframes
Selector
----------------------------
placeholder
selection
Property
----------------------------
align-content
align-items
align-self
animation
animation-delay
animation-direction
animation-duration
animation-fill-mode
animation-iteration-count
animation-name
animation-play-state
animation-timing-function
appearance
backface-visibility
background-clip
background-origin
background-size
border-bottom-left-radius
border-bottom-right-radius
border-image
border-image-outset
border-image-repeat
border-image-slice
border-image-source
border-image-width
border-radius
border-top-left-radius
border-top-right-radius
box-shadow
box-sizing
column-count
column-fill
column-gap
column-rule
column-rule-color
column-rule-style
column-rule-width
column-span
column-width
columns
flex
flex-basis
flex-direction
flex-flow
flex-grow
flex-shrink
flex-wrap
hyphens
justify-content
order
perspective
perspective-origin
tab-size
transform
transform-origin
transform-style
transition
transition-delay
transition-duration
transition-property
transition-timing-function
user-select
Keyword
----------------------------
flex
inline-flex
Function
----------------------------
calc
linear-gradient
repeating-linear-gradient
You can view this yourself from the command line, as well as which of these will actually be auto-prefixed by default, by using the omakase --prefixed-all
command. See the Scripts section below.
It's usually a good idea to add the PrefixCleaner
plugin after the Prefixer
plugin:
Prefixer prefixer = Prefixer.defaultBrowserSupport();
PrefixCleaner cleaner = PrefixCleaner.mismatchedPrefixedUnits();
Omakase.source(input).use(prefixer).use(cleaner).process();
This will remove prefixed declarations inside of prefixed at rules, where the declaration's prefix doesn't match the at-rule's prefix.
This plugin will handle flipping certain property names and values from left-to-right to right-to-left.
DirectionFlipPlugin rtl = new DirectionFlipPlugin();
Omakase.source(source).use(rtl).process();
It flips property names such as left
and border-left
to right
and border-right
. It also rearranges certain values like padding, margin and border-radius shorthand so that the right and left units are swapped.
If you need to prevent certain declarations from being flipped, you can use a CSS annotation like so:
.button {
/* @noflip */ padding-left: 10px;
}
Take note of the CSS annotation format. For example, if you use two asterisks to start the comment block instead of one then it will not be recognized.
In addition to the standard library plugins, you can create and register your own custom plugins. Custom plugins allow you to rework the processed CSS or add your own custom validation and linting rules. You can also use plugins to extend the CSS syntax and grammar.
Plugins are essentially plain java objects that implement one of the plugin interfaces and define one or more subscription methods to a particular AST object (e.g., Selector
or Declaration
). The subscription method does the actual rework or validation as appropriate.
Plugins are registered exactly the same as as any of the standard built-in plugins such as StyleWriter
or AutoRefine
.
See the Subscribable Syntax Units section below for the definitive list of all subscribable AST objects.
To get started, a plugin must first implement one or more of the plugin interfaces, listed as follows:
- Plugin - the basic plugin.
- DependentPlugin - for plugins that have dependencies on other plugins.
- GrammarPlugin - for plugins that customize syntax and grammar.
- ParserPlugin - for plugins customize individual parser behavior.
- PostProcessingPlugin for plugins that need notification after all processing has completed.
Most plugins will implement just the Plugin
or DependentPlugin
interface.
The term rework refers to the process of changing the processed CSS source code, e.g., changing a class name, adding a declaration, removing a rule, etc... Omakase allows you to easily change nearly any aspect of the CSS. You can add, change or remove selectors, add, change or remove declarations, add, change or remove rules, and... you get the picture.
Each AST object contains setters and getters for working with its values. In addition there are several utility classes.
Here is an example of a simple rework operation to prefix every selector with a class name called "myPrefix":
public class PrefixAllSelectors implements Plugin {
@Rework
public void prefixClass(Selector selector) {
// create a new simple selector
ClassSelector prefix = new ClassSelector("myPrefix");
// prepend the simple selector to the beginning of the selector
selector.parts().prepend(prefix);
}
}
Notice the following details:
- It implements
Plugin
- It has one subscription method with the
@Rework
annotation - The subscription method has one argument, which is the AST object to be reworked
For more advanced examples on performing rework see the ReworkTest.java class.
Here are some code examples illustrating some common operations during rework:
// create a new selector
Selector selector = new Selector(new ClassSelector(".test"));
// another example
IdSelector theId = new IdSelector("main");
ClassSelector theClass = new ClassSelector("inner");
Selector selector = new Selector(theId, Combinator.descendant(), theClass);
// append a simple selector part to an existing selector
selector.parts().append(new ClassSelector("another-class"));
// append a selector to a rule
rule.selectors().append(myNewSelector);
// alternative to above
someSelector.append(myNewSelector);
// change a value on a class selector
myClassSelector.name("the-new-name");
// check if a selector has a particular (class|id|type) simple selector
if (Selectors.hasClassSelector(selector, "myClass")) {...}
if (Selectors.hasIdSelector(selector, "myId")) {...}
if (Selectors.hasTypeSelector(selector, "div")) {...}
// find the first matching (class|id|type) simple selector
Optional<ClassSelector> button = Selectors.findClassSelector(selector, "button");
if (button.isPresent()) {
System.out.println(button.get());
}
// check if a 'div' type selector is adjoined to a class selector (e.g., "div.selector1")
if(Selectors.hasTypeSelector(Selectors.adjoining(selector1), "div")) {...}
// create a new declaration
Declaration declaration = new Declaration(Property.DISPLAY, KeywordValue.of(Keyword.NONE));
// another example
PropertyName name = PropertyName.of("new-prop");
Declaration declaration = new Declaration(name, KeywordValue.of("blah"));
// another example
NumericalValue val1 = NumericalValue.of(1, "px");
NumericalValue val2 = NumericalValue.of(5, "px");
PropertyValue value = PropertyValue.ofTerms(OperatorType.SPACE, val1, val2);
Declaration declaration = new Declaration(Property.MARGIN, value);
// another example
PropertyName prop = PropertyName.of(Property.BORDER_RADIUS).prefix(Prefix.WEBKIT);
Declaration declaration = new Declaration(prop, KeywordValue.of(Keyword.NONE));
// append a declaration to a rule
rule.declarations().append(myNewDeclaration);
// create a new rule
Rule rule = new Rule();
rule.selectors().append(new Selector(new ClassSelector("new")));
rule.declarations().append(new Declaration(Property.COLOR, HexColorValue.of("#fff")));
rule.declarations().append(new Declaration(Property.FONT_SIZE, NumericalValue.of(1.5).unit("em")));
// check if a declaration is for a specific property name
if (declaration.isProperty(Property.DISPLAY)) {...}
if (declaration.isProperty("new-prop")) {...}
// check if a declaration has a value
PropertyValue value = declaration.propertyValue();
Optional<KeywordValue> keyword = Values.asKeyword(value);
if (keyword.isPresent()) {...}
// use the method appropriate to what value you think it is (say, based on the property name)
Optional<HexColorValue> color = Values.asHexColor(value);
Optional<NumericalValue> number = Values.asNumerical(value);
Optional<StringValue> string = Values.asString(value);
// change a property value
Optional<KeywordValue> keyword = Values.asKeyword(declaration.propertyValue());
if (keyword.isPresent()) {
keyword.get().keyword(Keyword.NONE);
}
// remove (destroy) a selector, declaration, rule, etc...
someSelector.destroy();
someDeclaration.destroy();
someRule.destroy();
// for more examples see the many unit tests
Keep in mind that dynamically created units will be automatically delivered to all @Rework
subscription methods interested in the syntax unit's type, as well as to @Validate
subscriptions later on. Thus, dynamically created CSS is fully integrated with all of your custom rework and validation plugins.
You can remove any unit from the tree by calling #destroy
. Note that doing this will prevent that unit from being delivered to any subsequent subscription methods. Destroyed units cannot be added back to the tree, but they can be cloned with #copy
.
There are other utilities for working with units in the following utility classes:
Besides rework, you can also register subscription methods to perform validation and linting. Just like rework, you declare a method with the first parameter being the type of syntax unit you would like to validate. In addition there is a second parameter which is the ErrorManager
used to report any problems.
Here is an example of a class with two validation subscription methods:
public class Validations implements Plugin {
@Validate
public void validateRelativeFonts(Declaration declaration, ErrorManager em) {
if (!declaration.isProperty(Property.FONT_SIZE)) return; // only process font-size properties
Optional<NumericalValue> number = Values.asNumerical(declaration.propertyValue());
if (number.isPresent() && number.get().unit().isPresent() && number.get().unit().get().equals("px")) {
em.report(ErrorLevel.FATAL, declaration, "Font sizes must use relative units");
}
}
@Validate
public void validateIncludesUnprefixed(Declaration declaration, ErrorManager em) {
// check that all prefixed declarations include an unprefixed declaration as well.
PropertyName propertyName = declaration.propertyName();
if (propertyName.isPrefixed()) {
boolean found = false;
String expected = propertyName.unprefixed();
// go through each declaration in the block, looking for one with the unprefixed name
for (Declaration d : declaration.group()) {
if (d.isProperty(expected)) {
found = true;
break;
}
}
if (!found) {
em.report(ErrorLevel.WARNING, declaration, "Prefixed declaration without unprefixed version");
}
}
}
}
Keep in mind that all validators run after the rework phase has been completed. Validation methods should not change any aspect or content of the objects they are validating. They should only check values and reports errors as applicable.
As mentioned above, many plugins, especially ones with @Rework
, will need to register dependencies on other plugins. Here is an example of a plugin with dependencies:
public class Dependent implements DependentPlugin {
@Override
public void dependencies(PluginRegistry registry) {
registry.require(SelectorPlugin.class);
registry.require(MyPlugin.class, MyPlugin::new);
}
}
The #require
method takes the class of the plugin. If the plugin is already registered then the registered instance is simply returned. Otherwise one is automatically created and added to the registry. You can then proceed to configure the plugin as necessary for your use case.
You can also require your own custom plugins by using the #require(Class, Supplier)
method.
Note that any particular plugin can have as many @Rework
and @Validate
annotated methods as it needs. That is, rework and validation do not need to be separated out in to multiple classes.
You can also subscribe to the exact same syntax type in multiple methods. However there is no guarantee to the execution order of subscription methods to the exact same syntax unit type for the exact same operation (rework or validate). This means, for example, that if two @Rework
methods subscribed to ClassSelector
are needed, and that execution order is important, then these methods should be separated out into their own classes. The classes should then be registered in the intended execution order.
You can also register anonymous inner classes as plugins too:
Omakase.source(input)
.add(new StandardValidation())
.add(new Plugin() {
@Rework
public void rework(Declaration d) {
...
}
})
.add(new Plugin() {
@Validate
public void validate(Declaration d, ErrorManager em) {
...
}
})
.process();
Not only can you subscribe to concrete types such as ClassSelector
and IdSelector
, you can also subscribe to higher-level interfaces such as Statement
, SimpleSelector
or even the top-level Syntax
interface.
Subscribing to an interface type will allow you to receive all instances of that type, which can be useful in certain scenarios.
Within a particular class, the more specifically-typed subscription will be delivered before the more generally-typed subscriptions. For example, in a class with subscriptions to ClassSelector
, SimpleSelector
and Syntax
, the methods will always be invoked in that exact order.
See the Subscribable Syntax Units section below for the definitive list of all subscribable AST objects.
Besides @Rework
and @Validate
, there is one another annotation that can be used to make a subscription method.
@Observe
can be used in place of @Rework
when your intention is to simply utilize information from the AST object and you do not intend to make any changes. In terms of execution order, @Observe
and @Rework
are equivalent. Currently the only difference @Observe
makes is providing a better description of what the method intends to do.
Omakase provides a powerful mechanism for extending the standard CSS syntax. You can easily augment CSS with your own:
- Custom functions
- Custom at-rules
- Custom selectors
- Custom declarations
Using the @Refine
annotation, you can subscribe to any Refinable
syntax unit:
@Refine
public void refine(RawFunction function, Grammar grammar, Broadcaster broadcaster) {
}
@Refine
methods must specify three parameters. The first is the unit to refine. The second is of type Grammar
, which should be used to access internal parsers and tokens. The third is of type Broadcaster
, which should be used to broadcast refined syntax units.
Note that after a subscription method handles a unit it will not be sent to subsequent refiners, so plugins should be registered in appropriate order.
You can optionally specify a string to the @Refine
annotation, and only units that match that name will be delivered. Here is an example of a custom function:
@Refine("myFunction")
public void refine(RawFunction function, Grammar grammar, Broadcaster broadcaster) {
String args = function.args().trim();
// parse arguments, e.g., lookup a value, perform calculations...
// ...
// turn our parsed string into actual terms
Source source = new Source(parsedArgs);
grammar.parser().termSequenceParser().parse(source, grammar, broadcaster);
}
You can utilize the Source
class as a parsing utility, and nearly all of the library parsing functionality can be used standalone. This includes parsing rules, declarations, selectors, and even specific selectors like a class selector. Utilize the methods on the Grammar
instance and the Parsers
class.
For a full example of a RawFunction
refiner see UrlPlugin
. For a full example of an AtRule
refiner see Conditionals
plugin and related classes. For more detailed examples see the test samples.
Note that generally speaking, by simply utilizing an internal parser, all parsed units will be automatically broadcasted to the given broadcaster. This means that a custom function could simply parse a string for terms and operators using the term sequence parser and all encountered terms and operators will be automatically added to the declaration that the custom function is in, no further work required. To avoid this, just use your own broadcaster instance instead of passing through the one given to you.
As mentioned above, most of the time you want to include the StandardValidation
or AutoRefine
plugins to ensure that every AST object is refined and delivered to subscription methods. The alternative is to conditionally refine only the units that are necessary.
The easiest way to do this is with AutoRefine
, where you can specify to only refine selectors, declarations, etc:
// skip refinement of selectors
AutoRefine.only(Match.FUNCTIONS, Match.DECLARATIONS, Match.AT_RULES);
You can take this further with a custom @Refine
method that checks the raw content and refines if appropriate:
@Refine
public void observe(Selector selector, Grammar grammar, Broadcaster broadcaster) {
if (selector.raw().get().content().contains(".foo")) {
SelectorPlugin.delegateRefinement(selector, grammar, broadcaster);
}
}
You can delegate refinement to the standard plugin (SelectorPlugin
, DeclarationPlugin
, MediaPlugin
).
Using these methods you can eliminate unnecessary parsing for large sets of CSS in performance sensitive environments.
The default ErrorManager
is DefaultErrorManager
, which will rethrow some errors immediately and log others at the end of parsing.
You can alternatively specify your own ErrorManager
implementation and provide it during parser setup:
Omakase.source(input).use(myCustomErrorManager).process();
Omakase allows you to hook into the writing process and override the output of any particular AST unit. This feature allows you to:
- Conditionally stop the output of the unit.
- Append or prepend something before or after the unit.
- Conditional apply logic or append content based on an annotation from a CSS comment associated with the unit.
However, it is not recommended to change the actual content of the unit using a custom writer, as this will bypass all rework and validation rules.
The first step is to create a new class that implements the CustomWriter
interface. This interface is parameterized with the type of unit that it is overriding:
public class MyCustomWriter implements CustomWriter<Selector> {
@Override
public boolean write(Selector selector, StyleWriter writer, StyleAppendable appendable) throws IOException {
appendable.append("/*CUSTOM OUTPUT*/"); // arbitrary content
writer.writeInner(selector, appendable, false);
return true;
}
}
Inside of the #write
method you can append any content to the output by using the given StyleAppendable
as seen above. If you would like to append the default output of the unit as well then call the StyleWriter#writeInner
method, passing false for the last parameter. This method should return true if it has handled the unit, or false to allow subsequent custom writers or the default writer to handle it instead.
Afterwards, register this custom writer with the StyleWriter
instance:
StyleWriter writer = StyleWriter.compressed();
writer.addCustomWriter(Selector.class, new MyCustomWriter());
Omakase.source(".class{color:red}").use(writer).process();
AST objects are automatically associated with all comments that logically precede them. You can access the list of comments by calling the Syntax#comments
method on the syntax unit. In other words, comments are linked to the AST object that directly follows them (with the exception of orphaned comments, as explained below). Take this example:
/*0*/
/*1*/.class /*2*/ /*3*/.class/*4*/.class /*5*/, /*6*/ p#id /*7*/ { /*8*/
/*9*/ border: /*10*/ 1px/*11*/ solid red /*12*/;/*13*/ /*14*/
/*15*/ margin: 1px; /*16*/
/*17*/
}
/*18*//*19*/
- 0, 1 - linked with the
Selector
starting with the content.class...
- 2, 3 - linked with the
ClassSelector
segment that follows them - 4 - linked with the
ClassSelector
that follows it - 5 - an orphaned comment on the
Selector
- 6 - linked to the
Selector
starting with contentp#
... - 7 - an orphaned comment attached to the
Selector
- 8, 9 - linked to the
Declaration
starting with contentborder
... - 10 - linked to the term (
NumericalValue
) with content1px
- 11 - linked to the term (
KeywordValue
) with contentsolid
- 12 - an orphaned comment attached to the
Declaration
- 13, 14, 15 - linked to the
Declaration
starting with contentmargin
... - 16, 17 - orphaned comments linked to the
Rule
- 18, 19 - orphaned comments linked to the
Stylesheet
Note that without refinement, the comments on the inner segments of the Selector
s and Declaration
s will not be known.
Comments can be used for annotation directives. For example:
.class {
width: 20px;
margin-left: 20px;
/* @noflip */ float: left;
}
In this example, the @noflip
annotation is used to provide information to the DirectionFlip
plugin regarding not flipping that particular declaration value.
CSS comment annotations start with @
+ the name of the annotation. They can also have arguments. There can be at most one annotation per CSS comment block, although multiple annotation comment blocks are allowed. For example:
.class {
/* @markerAnnotation */ color: red;
/* @lang-switch ja eng */ font-family: Arial, sans-serif;
/* @custom1 */ /* @custom2 */ display: block;
}
No other content is allowed in the comment with the annotation. If there's any content before the start of the annotation the annotation will not be recognized, with the exception of bang comments:
.class {
width: 20px;
margin-left: 20px;
/*! @noflip */ float: left;
}
Any content that follows the annotation will be considered arguments for the annotation.
Any number of custom annotations can be utilized by your plugins. To check for an annotation, there are convenience methods available on every syntax unit:
public class MyPlugin implements Plugin {
@Rework
public void rework(Declaration declaration) {
// read /* @markerAnnotation */
if (declaration.hasAnnotation("markerAnnotation")) {
// do something
}
// read /* @browser ie7 */
Optional<CssAnnotation> annotation = declaration.annotation("browser");
if (annotation.isPresent()) {
System.out.println(annotation.get().rawArgs());
}
// read /* @bug b-123456, b-123457 */
Optional<CssAnnotation> annotation = declaration.annotation("bug");
if (annotation.isPresent()) {
ImmutableList<String> bugNumbers = annotation.get().commaSeparatedArgs();
}
// read /* @bug b-123456 b-123457 */
Optional<CssAnnotation> annotation = declaration.annotation("bug");
if (annotation.isPresent()) {
ImmutableList<String> bugNumbers = annotation.get().spaceSeparatedArgs();
}
// read /* @details author=nathan, since=2.0 */
Optional<CssAnnotation> annotation = declaration.annotation("details");
if (annotation.isPresent()) {
ImmutableMap<String, String> map = annotation.get().keyValueArgs('=');
String author = map.get("author");
}
// print all annotations
for (CssAnnotation annotation : declaration.annotations()) {
System.out.println(annotation.name());
System.out.println(annotation.rawArgs());
}
}
}
The CssAnnotation
class contains many powerful methods to parse various arg formats including space-delimited,
comma-delimited, key-value pairs and enum constants.
If you happen to have your hands on a specific Comment
instance, it has convenience methods as well.
When an annotation is placed before a rule, it is associated with first selector instance in the rule, not the rule or the simple selector, as explained above. However for convenience, all of the has*
and get*
annotation methods will also check or include results from the first selector when called on a rule instance.
The term orphaned comment refers to a comment that does not logically precede any particular AST unit. There are four places where orphaned comments can be found, which are at the end of a selector, at the end of a declaration (before the semi-colon), at the end of a rule, and at the end of a stylesheet. Here are some examples:
.class1 .class2 > a:hover /* an orphaned comment */, #id a:hover /*another orphaned comment*/ {
color: red;
font-size: 1.3em /*an orphaned comment*/;
/* an orphaned comment */
}
/* an orphaned comment */
Use the #orphanedComments
method on a Selector
, Declaration
, Rule
or Stylesheet
to retrieve them.
Following is the list of all supported syntax types that you can subscribe to in @Rework
, @Validate
and @Observe
annotated methods. Keep in mind that many syntax units require refinement before they will be delivered. More information on this is available in the Usage section above.
Name Description Enablement / Dependency Type ---------------------------- ------------------------------------------------------- ------------------------- --------------- 01: Statement rule or at-rule Automatic interface 02: Syntax top level interface for all units Under certain conditions* interface 03: RawFunction a raw function before refinement Declaration refinement class 04: Rule (no description) Automatic class 05: Stylesheet (no description) Automatic class 06: AtRule (no description) Automatic class 07: FontDescriptor font descriptor within @font-face AtRule refinement class 08: MediaQueryList full media query string AtRule refinement class 09: FunctionValue general interface for function terms Declaration refinement interface 10: Term a single segment of a property value Declaration refinement interface 11: Declaration (no description) Automatic class 12: GenericFunctionValue unknown function value Declaration refinement class 13: HexColorValue individual hex color value Declaration refinement class 14: KeywordValue individual keyword value Declaration refinement class 15: LinearGradientFunctionValue linear gradient function Declaration refinement class 16: NumericalValue individual numerical value Declaration refinement class 17: PropertyValue interface for all property values Declaration refinement class 18: StringValue individual string value Declaration refinement class 19: UnicodeRangeValue unicode range value Declaration refinement class 20: UrlFunctionValue url function Declaration refinement class 21: ConditionalAtRuleBlock conditionals AtRule refinement class 22: UnquotedIEFilter proprietary microsoft filter Declaration refinement class 23: SelectorPart group interface for all selector segments Selector refinement interface 24: SimpleSelector parent interface for simple selectors Selector refinement interface 25: AttributeSelector attribute selector segment Selector refinement class 26: ClassSelector class selector segment Selector refinement class 27: IdSelector id selector segment Selector refinement class 28: PseudoClassSelector pseudo class selector segment Selector refinement class 29: PseudoElementSelector pseudo element selector segment Selector refinement class 30: Selector (no description) Automatic class 31: TypeSelector type/element selector segment Selector refinement class 32: UniversalSelector universal selector segment Selector refinement class Generated by PrintSubscribableSyntaxTable.java
Notes:
- A subscription to
Syntax
will depend on which concrete syntax classes are enabled. To get every syntax unit then utilizenew StandardValidaion()
orAutoRefine#everything
. - Some orphaned comments will only be delivered if selectors and declarations are refined.
This project comes with an interactive shell, which allows you to quickly see what Omakase will output when given specific input CSS, all in real-time.
To get started, you must first run the Omakase setup script. Under the Omakase project directory run this command from the terminal:
script/setup.sh
This will enable the omakase
command from within this project folder. See the Scripts section below for more information. Once this is done, you can start up the interactive shell by typing in the terminal:
omakase --interactive
This will display usage information that looks something like this:
Omakase Interactive Shell
enter ! on a new line to finish
enter !c for continuous mode (ctrl+c to exit)
enter !verbose for verbose output
enter !inline for inline output
enter !compressed for compressed output
enter !prefix-current for default prefixing
enter !prefix-all for all prefixing
enter !prefix-prune to enable prefix pruning
enter !prefix-rearrange to enable prefix rearranging
enter !prefix-off to remove prefixing support
enter !subl to use the sublime text editor (subl)
enter !mate to use the textmate editor (mate)
Type your input CSS, with !
on a blank line demarking the end of the input. For example:
.test {
color: red;
}
!
This will output something like this:
----------result----------
.test {color:red}
For more involved scenarios, you might prefer to use the built in support for Sublime Text and Atom.
Sublime Text requires the subl
command to be installed on your PATH:
https://www.sublimetext.com/docs/3/osx_command_line.html
You can test that this is working by simply typing subl
in the terminal. It should open Sublime Text. If not, make sure ~/bin (or wherever you linked to) is on your path.
Atom requires the atom
command. You can install this by going to Menu -> Install Shell Commands. Again you can test this if the atom
command in terminal opens up Atom.
Before checking anything in, setup your IDE to conform to project standards. See and follow the instructions in the readme.md files inside of the contributing/intellij
or contributing/eclipse
folders.
As of right now the (strongly) preferred IDE for contribution is IntelliJ IDEA. This is mainly because the existing source code and style closely conforms to the IntelliJ settings included in the project. If you use eclipse or something else then be sure to following the existing coding conventions manually if need be.
The project relies on the following technologies:
- git (duh)
- java 8 (make sure both the IDE and maven are setup to use it)
- maven 3+
run mvn clean install
to get things going from the command line. It should build and run tests successfully. Afterwards you can import the maven project into your IDE (as an existing maven project) and go from there.
Non-test dependencies include Google's Guava library. Dependencies shouldn't really increase beyond that as one of the goals is simplicity and self-containment.
Currently tests are built with junit 4 and fest assertions. Junit should be self explanatory, but if you haven't used junit 4+ then keep in mind that it uses java annotations instead of inheritance and method naming conventions.
Fest may be new to you, but it's quite simple and easy to get the hang of. If you have used hamcrest before then it's something like that. Basically it's a library of matchers and assertions. The actual assertions are fluent and look something like this:
assertThat(someValue).isTrue();
assertThat(someCollection).hasSize(3);
assertThat(someCollection).containsExactly(value1, value2, value3);
This makes the tests more readable and also provides much more useful error messages out the box.
The important takeaway is that all unit tests must be written using fest assertions and not the junit/hamcrest ones. Just follow the patterns established in the existing tests. Particularly make sure you import the correct classes.
There are several enums such as Keyword.java
and Browser.java
that contain values that will inevitably need to be updated. Most of these are stored in data files under src/test/resources/data/
.
You can use a script to regenerate the java source files after updating, or you can directly run the main method on the classes.
The omakase CLI is a powerful tool for building the project, regenerating enum source files, running performance tests, and more.
To get started, under the project directory run this command from the shell:
script/setup.sh
This will setup links to the omakase CLI script. Now you can run the omakase
command from within the project root:
~/dev/omakase > omakase
Usage: omakase [options]
Options:
-b (--build) build the project
-h (--help) print this help message
-i (--interactive, --shell) interactive shell
-l (--local-only) only regenerate local data, no prefix data (used with -u option)
-p (--perf) <args> performance test
-s (--syntax, --sub) print the subscribable syntax table
-u (--update) regenerate data enum, data class and prefixes source files
-v (--prefixed-def) print what is auto-prefixed by Prefixer.defaultBrowserSupport()
-w (--prefixed-all) print all properties, at-rules, etc...that are supported by Prefixer
For example, updating the prefix info:
omakase --update
Printing the subscribable syntax table:
omakase --syntax
Running the performance test:
omakase -p
Omakase is a CSS parser built from the ground up. Unlike other open-source CSS parsers it is not built on a parser generator such as JavaCC or Antlr. Instead, it relies on many small and simple java objects that know how to consume various parts of CSS syntax, which also allows for easy extenstion of the syntax and grammar by consumers.
The project requires Java 8, git and maven. The general architecture of the project can be summarized as follows:
- Parsers - Small, individual parser objects that process CSS source code.
- AST Objects - Simple representations of various CSS syntax units.
- Plugins - Observers that can subscribe to any AST object for refinement, rework or validation.
- Broadcasters and Emitter - The bridge between parsers and plugins.
- Writers - Outputs parsed CSS code.
- ErrorHandlers - Manages errors encountered when parsing CSS source code.
Key Classes Token
Tokens
Source
Parser
BaseParserFactory
, Grammar
Parsers are simple java objects that know how to parse specific aspects of CSS syntax. Parsers do not maintain any state.
Parsers employ a 2-level parsing strategy. The first level will comb through the CSS source and extract the at-rules, selectors and declarations (and no more). Any errors at this grammatical level will be caught immediately (for example, a missing curly bracket to close a rule). However any errors at a more specific level (e.g., within a selector or within a declaration) will not be caught until that particular object is refined.
The second level occurs individually per instance, e.g., each particular Selector
or Declaration
instance.
It's important to understand that the second level may or may not be executed or may only be executed on certain instances.
This process allows us to be more specific about what actually gets parsed. For example, on a first parsing pass we may want to parse and validate everything, but on a second pass we may not care about fully parsing selectors anymore.
When a Parser
has successfully parsed some content, it will construct the appropriate AST object and give it to the Broadcaster
. Ultimately, the Broadcaster
will pass the AST object to the registered subscription methods for that particular AST object type.
Key Classes Syntax
Refinable
Groubable
SyntaxCollection
Selector
Declaration
AST (Abstract Syntax Tree) objects are simple data objects representing various aspects o CSS syntax, e.g., selectors, declarations, etc... AST objects generally have getters and setters for various values. AST objects are also responsible for writing their own content out when a StyleWriter
asks for it.
AST objects generally contain little to no validation logic. Most validation is written in the form of a Plugin
that subscribes to the unit it is going to validate.
Key Classes Plugin
DependentPlugin
A Plugin
subscribes to one or more AST objects (one per method) to perform rework or validation. Methods on the plugin are annotated with @Rework
or @Validate
annotations as appropriate. These methods are known as subscription methods. Each subscription method is subscribed to one particular AST object type via its parameter. This parameter is how we know which methods to invoke when various AST objects are sent to be broadcasted.
Plugins are registered during parser setup via Omakase#use
. Plugins can and often do have dependencies on each other, which can be registered by implementing the DependentPlugin
interface.
The general Omakase philosophy is that much of the internal logic as well as all of the consumer logic is organized into a set of plugins.
Key Classes Broadcaster
Emitter
AnnotationScanner
Subscription
A Broadcaster
is something that handles broadcasts of various AST objects. You can think of this as the observer pattern, or the event listener pattern, where the AST object itself is the event, the plugins are the listeners, and the broadcasters are responsible for receiving the AST objects from the parsers and delivering it to all registered subscriptions.
However, broadcasters do no necessarily deliver the AST objects right away. The Broadcaster
interface utilizes a chain pattern. That is, broadcasters can be wrapped inside of each other, relaying broadcasters to the next broadcaster down the chain. Near the bottom of the chain is usually the EmittingBroadcaster
, which is the one responsible for using an Emitter
to actually invoke the subscription methods.
Key Classes Writable
StyleWriter
StyleAppendable
Writers are fairly simple... they are responsible for taking the parsed SyntaxTree
and writing it out as a CSS source code string.
Key Classes ErrorManager
Error managers are responsible for dealing with errors during processing, including parser errors or errors generated from a validator plugin.
To view statistics on dependencies, run:
mvn project-info-reports:dependencies
open target/site/dependencies.html