CSS selector minification is a missed opportunity of saved bytes. Currently, Google uses it but not much beyond that.
The concept is change .box
to .b
and <div class="box">
to <div class="b">
.
There is room for issues with JavaScript so that should be treated as a nice-to-have and be conservatively avoided.
To convert HTML and CSS, it would be a 2 step process:
- Parse CSS into its AST
/* DEV: This is bad OOCSS but it is for example purposes */
.hello.world .arrow {
background-color: blue;
}
to
// This is more AST pseudocode
{
type: 'CSSRule',
selector: '.hello.world .arrow',
properties: [{
name: 'background-color',
value: 'blue'
}]
}
- Break up and selectors (e.g. ids, classes)
Be sure this focuses exclusively on and not optimizing the selectors themself (e.g.
* #id -> #id
). That is the responsibility of CSS optimizers (e.g. http://bem.info/tools/optimizers/csso/). Do one thing and do it well.
// DEV: This step might be overkill with the format
{
type: 'CSSRule',
selector: '.hello.world .arrow',
// Selectors: Array of selectors groups
// Selector group: Array of selector properties
selectors: [
[{type: 'class', value: 'hello'}, {type: 'class', value: 'world'}],
[{type: 'class', value: 'arrow'}]
],
properties: [{
name: 'background-color',
value: 'blue'
}]
}
- Transform and save selectors
var defn = {
type: 'CSSRule',
selector: '.h.w .a',
// Selectors: Array of selectors groups
// Selector group: Array of selector properties
selectors: [
[{type: 'class', value: 'h'}, {type: 'class', value: 'w'}],
[{type: 'class', value: 'a'}]
],
properties: [{
name: 'background-color',
value: 'blue'
}]
};
// DEV: These are selectors as previously used
var map = [
{type: 'class', original: 'hello', value: 'h'},
{type: 'class', original: 'world', value: 'w'},
{type: 'class', original: 'arrow', value: 'a'}
];
- Recompile CSS
.h.w .a {
background-color: blue;
}
Library fragmentation (each of these should be its own node module)
- CSS to AST parser, probably can use rework or similar
- CSS selector parser (step 2)
- CSS selector minifier (step 3) -- meant to work with AST pieces only
- CSS AST lexer, probably use rework again
- Module that combines all of them (as a vanilla node module)
- Convert HTML to AST or DOM
<div class="hello world">
<div class="arrow">></div>
</div>
to
{
type: HTMLDivElement,
attributes: {
className: 'hello world'
},
childNodes: [{
type: HTMLDivElement,
attributes: {
className: 'arrow'
},
// childNodes: TextNode: value: '>'
}]
}
- Break down classes
{
type: HTMLDivElement,
classes: ['hello', 'world'],
attributes: {
className: 'hello world'
},
childNodes: [{
type: HTMLDivElement,
classes: ['arrow'],
attributes: {
className: 'arrow'
},
// childNodes: TextNode: value: '>'
}]
}
- Convert against a given map
var map = [
{type: 'class', original: 'hello', value: 'h'},
{type: 'class', original: 'world', value: 'w'},
{type: 'class', original: 'arrow', value: 'a'}
];
generates
{
type: HTMLDivElement,
classes: ['h', 'w']
attributes: {
className: 'h w'
},
childNodes: [{
type: HTMLDivElement,
classes: ['a'],
attributes: {
className: 'a'
},
// childNodes: TextNode: value: '>'
}]
}
- Compile AST/DOM into HTML
<div class="h w">
<div class="a">></div>
</div>
Library fragmentation (each of these should be its own node module)
- HTML to AST parser
- HTML class parser (step 2)
- HTML class transformer (step 3) -- meant to work with AST pieces only
- HTML AST lexer
- Module that combines all of them (as a vanilla node module)
If you haven't guessed, this would in theory be more of the same. However, this step can be potentially dangerous so an opt-in approach is best suited (e.g. whitelist).
We can provide an identity function that signfies as a special marker in the AST to switch over its selectors.
function k(selector) {
return selector;
}
// We will write
var $arrow = $('.arrow');
// as (signifying an opt-in)
var $arrow = $(k('.arrow'));
// In development, runs as
var $arrow = $(k('.arrow'));
// In production, this *compiles* to
var $arrow = $('.a');
The selector would be broken up with the same module as the CSS selector breakup.
This can feed back into step 1 by providing a list of opted-in classes. However, this can be potentially frustrating if you forget to opt-in an already opted-in selector on new code.