css-mangle-webpack-plugin
Version:
This webpack plugin package is a CSS mangler that globally optimizes and shortens identifier names.
215 lines (176 loc) • 7.87 kB
text/typescript
import { StringUtil } from "../utils/string";
import { Mangler } from "./mangler";
import { ManglerAsset, ManglerAssetType } from "./mangler_asset";
import { ManglerContext, ManglerContextOptions } from "./mangler_context";
import { CSSQueryDeclaration, CSSVariableDeclaration, ManglerDeclaration } from "./mangler_declaration";
import { CSSQueryReference, CSSVariableReference, ManglerReference } from "./mangler_reference";
export abstract class ManglerTranspiler<T = Mangler> {
abstract createMangler(): Mangler;
abstract createContext(): ManglerContext<T>;
abstract transform(asset: ManglerAsset): string;
}
/** This class provides functions in general use for the foundation of this package. */
export abstract class DrivenManglerTranspiler<T = Mangler> extends ManglerTranspiler<T> {
context: ManglerContext<T>;
createMangler(): Mangler {
return new Mangler();
}
}
export interface CSSVariableManglerOptions extends ManglerContextOptions {
property: boolean;
literals: boolean;
}
export class CSSVariableManglerTranspiler extends DrivenManglerTranspiler {
constructor(public options: CSSVariableManglerOptions) {
super();
}
createManglerDeclaration(): ManglerDeclaration {
return new CSSVariableDeclaration();
}
createManglerReference(): ManglerReference {
return new CSSVariableReference(this.options);
}
createContext(): ManglerContext<Mangler> {
return new ManglerContext(this.options, this.createMangler());
}
transform(asset: ManglerAsset): string {
const context = this.context ??= this.createContext();
const t1 = this.createManglerDeclaration().transform(asset, context);
const t2 = this.createManglerReference().transform(((asset.syntaxText = t1), asset), context);
return t2;
}
}
export interface CSSQueryManglerContext {
classMangler: Mangler;
idMangler: Mangler;
}
export class CSSQueryManglerTranspiler extends DrivenManglerTranspiler<CSSQueryManglerContext> {
createManglerDeclaration(): CSSQueryDeclaration {
return new CSSQueryDeclaration();
}
createManglerReference(): CSSQueryReference {
return new CSSQueryReference();
}
createContext(): ManglerContext<CSSQueryManglerContext> {
return new ManglerContext({reversed: [], canUndeclared: false}, {
classMangler: this.createMangler(),
idMangler: this.createMangler()
});
}
transform(asset: ManglerAsset): string {
const context = this.context ??= this.createContext();
const t1 = this.createManglerDeclaration().transform(asset, context);
const t2 = this.createManglerReference().transform(((asset.syntaxText = t1), asset), context);
return t2;
}
}
export interface CSSMinificationManglerOptions {
rgbToHex: boolean;
comments: boolean;
escapeSequence: boolean;
}
export class CSSMinificationManglerTranspiler extends DrivenManglerTranspiler<undefined> {
constructor(public options: CSSMinificationManglerOptions) {
super();
}
createContext(): ManglerContext<undefined> {
return undefined;
}
transform(asset: ManglerAsset): string {
if (asset.syntaxType != ManglerAssetType.STYLE) {
return asset.syntaxText;
}
const t1 = this.options.rgbToHex ? this.transformLiterals(asset.syntaxText) : asset.syntaxText;
const t2 = this.options.comments ? this.transformComments(t1) : t1;
const t3 = this.options.escapeSequence ? this.transformEscapeSequence(t2) : t2;
return t3;
}
/** About rgb() */
transformLiterals(syntaxText: string): string {
const regexpInst = /rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*(,\s*(([0-1](\.\d+)?)|(\d{1,3}%))\s*)?\)/g;
const syntaxList = syntaxText.matchAll(regexpInst);
let replacedLength = 0;
for (const global of syntaxList) {
const rgbText = global[0].match(/(?<=\().+?(?=\))/g);
const rgbStrs = rgbText[0].replaceAll(" ", "").split(",");
const rgbNums = rgbStrs.map((str, index) => {
if (index == 3) { // is opacity
return str.endsWith("%")
? Math.round(parseFloat(str.replace("%", "")) / 100 * 255)
: Math.round(parseFloat(str) * 255);
}
return parseInt(str);
})
// No needs to explicitly define the value when an opacity is 100% as text.
.filter((val, i) => i != 3 || val < 255);
const rgbHexs = rgbNums.filter(v => v != null).map(val => val?.toString(16).padStart(2, "0")); // to Hexadecimal
const rgbHexStr = "#" + rgbHexs.join("");
const index = global.index + replacedLength;
const length = global[0].length;
const result = StringUtil.replaceRange(
syntaxText,
index,
index + length,
rgbHexStr
);
replacedLength += StringUtil.replacedLength(syntaxText, result);
syntaxText = result;
}
return syntaxText;
}
transformComments(syntaxText: string): string {
const regexpInst = /\n?\/\*[^]+?\*\//g;
const regexpList = syntaxText.matchAll(regexpInst);
let replacedLength = 0;
for (const global of regexpList) {
const inner = global[0];
const index = global.index + replacedLength;
const length = inner.length;
const result = StringUtil.replaceRange(
syntaxText,
index,
index + length,
"" // to empty string.
);
replacedLength += StringUtil.replacedLength(syntaxText, result);
syntaxText = result;
}
return syntaxText;
}
transformEscapeSequence(syntaxText: string): string {
// It matches patterns in CSS that should not be omitted,
// specifically areas where spaces must not be removed.
//
// i.e. The matched contents for this pattern is like safe-area
// from removal target for minifying CSS.
//
const ignoreRegexpInst = /".+?"|'.+?'|\/\*.+?\*\/|(?<=\w+\s*:\s*)\S.+?(?=[;}])|[^\s].+?(?=\s*[{,])/g; // Refer to safe-area.
const ignoreRegexpList = syntaxText.matchAll(ignoreRegexpInst);
const ignoreRanges: {start: number, end: number}[] = [];
for (const global of ignoreRegexpList) {
ignoreRanges.push({start: global.index, end: global.index + global[0].length});
}
/** It matches patterns that have the potential to be omitted. */
const regexpInst = /[\n\s]+/g;
const regexpList = syntaxText.matchAll(regexpInst);
let replacedLength = 0;
for (const global of regexpList) {
const inner = global[0];
const index = global.index + replacedLength;
const length = inner.length;
const inRange = ignoreRanges.find(r => r.start < global.index && r.end > global.index);
const isIgnore = inRange != null;
if (!isIgnore) {
const result = StringUtil.replaceRange(
syntaxText,
index,
index + length,
"" // to empty string.
);
replacedLength += StringUtil.replacedLength(syntaxText, result);
syntaxText = result;
}
}
return syntaxText;
}
}