197 lines
5 KiB
JavaScript
197 lines
5 KiB
JavaScript
|
import { List, clone, walk } from 'css-tree';
|
||
|
import { buildIndex } from './usage.js';
|
||
|
import clean from './clean/index.js';
|
||
|
import replace from './replace/index.js';
|
||
|
import restructure from './restructure/index.js';
|
||
|
|
||
|
function readChunk(input, specialComments) {
|
||
|
const children = new List();
|
||
|
let nonSpaceTokenInBuffer = false;
|
||
|
let protectedComment;
|
||
|
|
||
|
input.nextUntil(input.head, (node, item, list) => {
|
||
|
if (node.type === 'Comment') {
|
||
|
if (!specialComments || node.value.charAt(0) !== '!') {
|
||
|
list.remove(item);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (nonSpaceTokenInBuffer || protectedComment) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
list.remove(item);
|
||
|
protectedComment = node;
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (node.type !== 'WhiteSpace') {
|
||
|
nonSpaceTokenInBuffer = true;
|
||
|
}
|
||
|
|
||
|
children.insert(list.remove(item));
|
||
|
});
|
||
|
|
||
|
return {
|
||
|
comment: protectedComment,
|
||
|
stylesheet: {
|
||
|
type: 'StyleSheet',
|
||
|
loc: null,
|
||
|
children
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function compressChunk(ast, firstAtrulesAllowed, num, options) {
|
||
|
options.logger(`Compress block #${num}`, null, true);
|
||
|
|
||
|
let seed = 1;
|
||
|
|
||
|
if (ast.type === 'StyleSheet') {
|
||
|
ast.firstAtrulesAllowed = firstAtrulesAllowed;
|
||
|
ast.id = seed++;
|
||
|
}
|
||
|
|
||
|
walk(ast, {
|
||
|
visit: 'Atrule',
|
||
|
enter(node) {
|
||
|
if (node.block !== null) {
|
||
|
node.block.id = seed++;
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
options.logger('init', ast);
|
||
|
|
||
|
// remove redundant
|
||
|
clean(ast, options);
|
||
|
options.logger('clean', ast);
|
||
|
|
||
|
// replace nodes for shortened forms
|
||
|
replace(ast, options);
|
||
|
options.logger('replace', ast);
|
||
|
|
||
|
// structure optimisations
|
||
|
if (options.restructuring) {
|
||
|
restructure(ast, options);
|
||
|
}
|
||
|
|
||
|
return ast;
|
||
|
}
|
||
|
|
||
|
function getCommentsOption(options) {
|
||
|
let comments = 'comments' in options ? options.comments : 'exclamation';
|
||
|
|
||
|
if (typeof comments === 'boolean') {
|
||
|
comments = comments ? 'exclamation' : false;
|
||
|
} else if (comments !== 'exclamation' && comments !== 'first-exclamation') {
|
||
|
comments = false;
|
||
|
}
|
||
|
|
||
|
return comments;
|
||
|
}
|
||
|
|
||
|
function getRestructureOption(options) {
|
||
|
if ('restructure' in options) {
|
||
|
return options.restructure;
|
||
|
}
|
||
|
|
||
|
return 'restructuring' in options ? options.restructuring : true;
|
||
|
}
|
||
|
|
||
|
function wrapBlock(block) {
|
||
|
return new List().appendData({
|
||
|
type: 'Rule',
|
||
|
loc: null,
|
||
|
prelude: {
|
||
|
type: 'SelectorList',
|
||
|
loc: null,
|
||
|
children: new List().appendData({
|
||
|
type: 'Selector',
|
||
|
loc: null,
|
||
|
children: new List().appendData({
|
||
|
type: 'TypeSelector',
|
||
|
loc: null,
|
||
|
name: 'x'
|
||
|
})
|
||
|
})
|
||
|
},
|
||
|
block
|
||
|
});
|
||
|
}
|
||
|
|
||
|
export default function compress(ast, options) {
|
||
|
ast = ast || { type: 'StyleSheet', loc: null, children: new List() };
|
||
|
options = options || {};
|
||
|
|
||
|
const compressOptions = {
|
||
|
logger: typeof options.logger === 'function' ? options.logger : function() {},
|
||
|
restructuring: getRestructureOption(options),
|
||
|
forceMediaMerge: Boolean(options.forceMediaMerge),
|
||
|
usage: options.usage ? buildIndex(options.usage) : false
|
||
|
};
|
||
|
const output = new List();
|
||
|
let specialComments = getCommentsOption(options);
|
||
|
let firstAtrulesAllowed = true;
|
||
|
let input;
|
||
|
let chunk;
|
||
|
let chunkNum = 1;
|
||
|
let chunkChildren;
|
||
|
|
||
|
if (options.clone) {
|
||
|
ast = clone(ast);
|
||
|
}
|
||
|
|
||
|
if (ast.type === 'StyleSheet') {
|
||
|
input = ast.children;
|
||
|
ast.children = output;
|
||
|
} else {
|
||
|
input = wrapBlock(ast);
|
||
|
}
|
||
|
|
||
|
do {
|
||
|
chunk = readChunk(input, Boolean(specialComments));
|
||
|
compressChunk(chunk.stylesheet, firstAtrulesAllowed, chunkNum++, compressOptions);
|
||
|
chunkChildren = chunk.stylesheet.children;
|
||
|
|
||
|
if (chunk.comment) {
|
||
|
// add \n before comment if there is another content in output
|
||
|
if (!output.isEmpty) {
|
||
|
output.insert(List.createItem({
|
||
|
type: 'Raw',
|
||
|
value: '\n'
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
output.insert(List.createItem(chunk.comment));
|
||
|
|
||
|
// add \n after comment if chunk is not empty
|
||
|
if (!chunkChildren.isEmpty) {
|
||
|
output.insert(List.createItem({
|
||
|
type: 'Raw',
|
||
|
value: '\n'
|
||
|
}));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (firstAtrulesAllowed && !chunkChildren.isEmpty) {
|
||
|
const lastRule = chunkChildren.last;
|
||
|
|
||
|
if (lastRule.type !== 'Atrule' ||
|
||
|
(lastRule.name !== 'import' && lastRule.name !== 'charset')) {
|
||
|
firstAtrulesAllowed = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (specialComments !== 'exclamation') {
|
||
|
specialComments = false;
|
||
|
}
|
||
|
|
||
|
output.appendList(chunkChildren);
|
||
|
} while (!input.isEmpty);
|
||
|
|
||
|
return {
|
||
|
ast
|
||
|
};
|
||
|
};
|