compress.js•5.15 kB
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
    };
};