Fix code quality violations and exclude Manifest from checks

Document application modes (development/debug/production)
Add global file drop handler, order column normalization, SPA hash fix
Serve CDN assets via /_vendor/ URLs instead of merging into bundles
Add production minification with license preservation
Improve JSON formatting for debugging and production optimization
Add CDN asset caching with CSS URL inlining for production builds
Add three-mode system (development, debug, production)
Update Manifest CLAUDE.md to reflect helper class architecture
Refactor Manifest.php into helper classes for better organization
Pre-manifest-refactor checkpoint: Add app_mode documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2026-01-14 10:38:22 +00:00
parent bb9046af1b
commit d523f0f600
2355 changed files with 231384 additions and 32223 deletions

View File

@@ -1,12 +1,11 @@
var List = require('css-tree').List;
var resolveKeyword = require('css-tree').keyword;
var hasOwnProperty = Object.prototype.hasOwnProperty;
var walk = require('css-tree').walk;
import { List, walk, keyword as resolveKeyword } from 'css-tree';
const { hasOwnProperty } = Object.prototype;
function addRuleToMap(map, item, list, single) {
var node = item.data;
var name = resolveKeyword(node.name).basename;
var id = node.name.toLowerCase() + '/' + (node.prelude ? node.prelude.id : null);
const node = item.data;
const name = resolveKeyword(node.name).basename;
const id = node.name.toLowerCase() + '/' + (node.prelude ? node.prelude.id : null);
if (!hasOwnProperty.call(map, name)) {
map[name] = Object.create(null);
@@ -24,12 +23,12 @@ function addRuleToMap(map, item, list, single) {
}
function relocateAtrules(ast, options) {
var collected = Object.create(null);
var topInjectPoint = null;
const collected = Object.create(null);
let topInjectPoint = null;
ast.children.each(function(node, item, list) {
ast.children.forEach(function(node, item, list) {
if (node.type === 'Atrule') {
var name = resolveKeyword(node.name).basename;
const name = resolveKeyword(node.name).basename;
switch (name) {
case 'keyframes':
@@ -56,8 +55,8 @@ function relocateAtrules(ast, options) {
}
});
for (var atrule in collected) {
for (var id in collected[atrule]) {
for (const atrule in collected) {
for (const id in collected[atrule]) {
ast.children.insertList(
collected[atrule][id],
atrule === 'media' ? null : topInjectPoint
@@ -75,7 +74,7 @@ function processAtrule(node, item, list) {
return;
}
var prev = item.prev && item.prev.data;
const prev = item.prev && item.prev.data;
if (!prev || !isMediaRule(prev)) {
return;
@@ -96,7 +95,7 @@ function processAtrule(node, item, list) {
}
}
module.exports = function rejoinAtrule(ast, options) {
export default function rejoinAtrule(ast, options) {
relocateAtrules(ast, options);
walk(ast, {

View File

@@ -1,45 +1,51 @@
var walk = require('css-tree').walk;
var utils = require('./utils');
import { walk } from 'css-tree';
import {
unsafeToSkipNode,
isEqualSelectors,
isEqualDeclarations,
addSelectors,
hasSimilarSelectors
} from './utils.js';
function processRule(node, item, list) {
var selectors = node.prelude.children;
var declarations = node.block.children;
const selectors = node.prelude.children;
const declarations = node.block.children;
list.prevUntil(item.prev, function(prev) {
// skip non-ruleset node if safe
if (prev.type !== 'Rule') {
return utils.unsafeToSkipNode.call(selectors, prev);
return unsafeToSkipNode.call(selectors, prev);
}
var prevSelectors = prev.prelude.children;
var prevDeclarations = prev.block.children;
const prevSelectors = prev.prelude.children;
const prevDeclarations = prev.block.children;
// try to join rulesets with equal pseudo signature
if (node.pseudoSignature === prev.pseudoSignature) {
// try to join by selectors
if (utils.isEqualSelectors(prevSelectors, selectors)) {
if (isEqualSelectors(prevSelectors, selectors)) {
prevDeclarations.appendList(declarations);
list.remove(item);
return true;
}
// try to join by declarations
if (utils.isEqualDeclarations(declarations, prevDeclarations)) {
utils.addSelectors(prevSelectors, selectors);
if (isEqualDeclarations(declarations, prevDeclarations)) {
addSelectors(prevSelectors, selectors);
list.remove(item);
return true;
}
}
// go to prev ruleset if has no selector similarities
return utils.hasSimilarSelectors(selectors, prevSelectors);
return hasSimilarSelectors(selectors, prevSelectors);
});
}
// NOTE: direction should be left to right, since rulesets merge to left
// ruleset. When direction right to left unmerged rulesets may prevent lookup
// TODO: remove initial merge
module.exports = function initialMergeRule(ast) {
export default function initialMergeRule(ast) {
walk(ast, {
visit: 'Rule',
enter: processRule

View File

@@ -1,8 +1,7 @@
var List = require('css-tree').List;
var walk = require('css-tree').walk;
import { List, walk } from 'css-tree';
function processRule(node, item, list) {
var selectors = node.prelude.children;
const selectors = node.prelude.children;
// generate new rule sets:
// .a, .b { color: red; }
@@ -12,7 +11,8 @@ function processRule(node, item, list) {
// while there are more than 1 simple selector split for rulesets
while (selectors.head !== selectors.tail) {
var newSelectors = new List();
const newSelectors = new List();
newSelectors.insert(selectors.remove(selectors.head));
list.insert(list.createItem({
@@ -33,7 +33,7 @@ function processRule(node, item, list) {
}
}
module.exports = function disjoinRule(ast) {
export default function disjoinRule(ast) {
walk(ast, {
visit: 'Rule',
reverse: true,

View File

@@ -1,15 +1,13 @@
var List = require('css-tree').List;
var generate = require('css-tree').generate;
var walk = require('css-tree').walk;
import { List, generate, walk } from 'css-tree';
var REPLACE = 1;
var REMOVE = 2;
var TOP = 0;
var RIGHT = 1;
var BOTTOM = 2;
var LEFT = 3;
var SIDES = ['top', 'right', 'bottom', 'left'];
var SIDE = {
const REPLACE = 1;
const REMOVE = 2;
const TOP = 0;
const RIGHT = 1;
const BOTTOM = 2;
const LEFT = 3;
const SIDES = ['top', 'right', 'bottom', 'left'];
const SIDE = {
'margin-top': 'top',
'margin-right': 'right',
'margin-bottom': 'bottom',
@@ -33,7 +31,7 @@ var SIDE = {
'border-bottom-style': 'bottom',
'border-left-style': 'left'
};
var MAIN_PROPERTY = {
const MAIN_PROPERTY = {
'margin': 'margin',
'margin-top': 'margin',
'margin-right': 'margin',
@@ -63,289 +61,284 @@ var MAIN_PROPERTY = {
'border-left-style': 'border-style'
};
function TRBL(name) {
this.name = name;
this.loc = null;
this.iehack = undefined;
this.sides = {
'top': null,
'right': null,
'bottom': null,
'left': null
};
}
class TRBL {
constructor(name) {
this.name = name;
this.loc = null;
this.iehack = undefined;
this.sides = {
'top': null,
'right': null,
'bottom': null,
'left': null
};
}
TRBL.prototype.getValueSequence = function(declaration, count) {
var values = [];
var iehack = '';
var hasBadValues = declaration.value.type !== 'Value' || declaration.value.children.some(function(child) {
var special = false;
getValueSequence(declaration, count) {
const values = [];
let iehack = '';
const hasBadValues = declaration.value.type !== 'Value' || declaration.value.children.some(function(child) {
let special = false;
switch (child.type) {
case 'Identifier':
switch (child.name) {
case '\\0':
case '\\9':
iehack = child.name;
return;
switch (child.type) {
case 'Identifier':
switch (child.name) {
case '\\0':
case '\\9':
iehack = child.name;
return;
case 'inherit':
case 'initial':
case 'unset':
case 'revert':
special = child.name;
break;
}
break;
case 'inherit':
case 'initial':
case 'unset':
case 'revert':
special = child.name;
break;
}
break;
case 'Dimension':
switch (child.unit) {
// is not supported until IE11
case 'rem':
case 'Dimension':
switch (child.unit) {
// is not supported until IE11
case 'rem':
// v* units is too buggy across browsers and better
// don't merge values with those units
case 'vw':
case 'vh':
case 'vmin':
case 'vmax':
case 'vm': // IE9 supporting "vm" instead of "vmin".
special = child.unit;
break;
}
break;
// v* units is too buggy across browsers and better
// don't merge values with those units
case 'vw':
case 'vh':
case 'vmin':
case 'vmax':
case 'vm': // IE9 supporting "vm" instead of "vmin".
special = child.unit;
break;
}
break;
case 'Hash': // color
case 'Number':
case 'Percentage':
break;
case 'Hash': // color
case 'Number':
case 'Percentage':
break;
case 'Function':
if (child.name === 'var') {
return true;
}
case 'Function':
if (child.name === 'var') {
return true;
}
special = child.name;
break;
special = child.name;
break;
case 'WhiteSpace':
return false; // ignore space
default:
return true; // bad value
}
default:
return true; // bad value
values.push({
node: child,
special,
important: declaration.important
});
});
if (hasBadValues || values.length > count) {
return false;
}
values.push({
node: child,
special: special,
important: declaration.important
});
});
if (typeof this.iehack === 'string' && this.iehack !== iehack) {
return false;
}
if (hasBadValues || values.length > count) {
return false;
this.iehack = iehack; // move outside
return values;
}
if (typeof this.iehack === 'string' && this.iehack !== iehack) {
return false;
canOverride(side, value) {
const currentValue = this.sides[side];
return !currentValue || (value.important && !currentValue.important);
}
this.iehack = iehack; // move outside
add(name, declaration) {
function attemptToAdd() {
const sides = this.sides;
const side = SIDE[name];
return values;
};
TRBL.prototype.canOverride = function(side, value) {
var currentValue = this.sides[side];
return !currentValue || (value.important && !currentValue.important);
};
TRBL.prototype.add = function(name, declaration) {
function attemptToAdd() {
var sides = this.sides;
var side = SIDE[name];
if (side) {
if (side in sides === false) {
return false;
}
var values = this.getValueSequence(declaration, 1);
if (!values || !values.length) {
return false;
}
// can mix only if specials are equal
for (var key in sides) {
if (sides[key] !== null && sides[key].special !== values[0].special) {
if (side) {
if (side in sides === false) {
return false;
}
}
if (!this.canOverride(side, values[0])) {
return true;
}
const values = this.getValueSequence(declaration, 1);
sides[side] = values[0];
return true;
} else if (name === this.name) {
var values = this.getValueSequence(declaration, 4);
if (!values || !values.length) {
return false;
}
if (!values || !values.length) {
return false;
}
switch (values.length) {
case 1:
values[RIGHT] = values[TOP];
values[BOTTOM] = values[TOP];
values[LEFT] = values[TOP];
break;
case 2:
values[BOTTOM] = values[TOP];
values[LEFT] = values[RIGHT];
break;
case 3:
values[LEFT] = values[RIGHT];
break;
}
// can mix only if specials are equal
for (var i = 0; i < 4; i++) {
for (var key in sides) {
if (sides[key] !== null && sides[key].special !== values[i].special) {
// can mix only if specials are equal
for (const key in sides) {
if (sides[key] !== null && sides[key].special !== values[0].special) {
return false;
}
}
}
for (var i = 0; i < 4; i++) {
if (this.canOverride(SIDES[i], values[i])) {
sides[SIDES[i]] = values[i];
if (!this.canOverride(side, values[0])) {
return true;
}
}
return true;
sides[side] = values[0];
return true;
} else if (name === this.name) {
const values = this.getValueSequence(declaration, 4);
if (!values || !values.length) {
return false;
}
switch (values.length) {
case 1:
values[RIGHT] = values[TOP];
values[BOTTOM] = values[TOP];
values[LEFT] = values[TOP];
break;
case 2:
values[BOTTOM] = values[TOP];
values[LEFT] = values[RIGHT];
break;
case 3:
values[LEFT] = values[RIGHT];
break;
}
// can mix only if specials are equal
for (let i = 0; i < 4; i++) {
for (const key in sides) {
if (sides[key] !== null && sides[key].special !== values[i].special) {
return false;
}
}
}
for (let i = 0; i < 4; i++) {
if (this.canOverride(SIDES[i], values[i])) {
sides[SIDES[i]] = values[i];
}
}
return true;
}
}
if (!attemptToAdd.call(this)) {
return false;
}
// TODO: use it when we can refer to several points in source
// if (this.loc) {
// this.loc = {
// primary: this.loc,
// merged: declaration.loc
// };
// } else {
// this.loc = declaration.loc;
// }
if (!this.loc) {
this.loc = declaration.loc;
}
return true;
}
if (!attemptToAdd.call(this)) {
isOkToMinimize() {
const top = this.sides.top;
const right = this.sides.right;
const bottom = this.sides.bottom;
const left = this.sides.left;
if (top && right && bottom && left) {
const important =
top.important +
right.important +
bottom.important +
left.important;
return important === 0 || important === 4;
}
return false;
}
// TODO: use it when we can refer to several points in source
// if (this.loc) {
// this.loc = {
// primary: this.loc,
// merged: declaration.loc
// };
// } else {
// this.loc = declaration.loc;
// }
if (!this.loc) {
this.loc = declaration.loc;
}
getValue() {
const result = new List();
const sides = this.sides;
const values = [
sides.top,
sides.right,
sides.bottom,
sides.left
];
const stringValues = [
generate(sides.top.node),
generate(sides.right.node),
generate(sides.bottom.node),
generate(sides.left.node)
];
return true;
};
TRBL.prototype.isOkToMinimize = function() {
var top = this.sides.top;
var right = this.sides.right;
var bottom = this.sides.bottom;
var left = this.sides.left;
if (top && right && bottom && left) {
var important =
top.important +
right.important +
bottom.important +
left.important;
return important === 0 || important === 4;
}
return false;
};
TRBL.prototype.getValue = function() {
var result = new List();
var sides = this.sides;
var values = [
sides.top,
sides.right,
sides.bottom,
sides.left
];
var stringValues = [
generate(sides.top.node),
generate(sides.right.node),
generate(sides.bottom.node),
generate(sides.left.node)
];
if (stringValues[LEFT] === stringValues[RIGHT]) {
values.pop();
if (stringValues[BOTTOM] === stringValues[TOP]) {
if (stringValues[LEFT] === stringValues[RIGHT]) {
values.pop();
if (stringValues[RIGHT] === stringValues[TOP]) {
if (stringValues[BOTTOM] === stringValues[TOP]) {
values.pop();
if (stringValues[RIGHT] === stringValues[TOP]) {
values.pop();
}
}
}
}
for (var i = 0; i < values.length; i++) {
if (i) {
result.appendData({ type: 'WhiteSpace', value: ' ' });
for (let i = 0; i < values.length; i++) {
result.appendData(values[i].node);
}
result.appendData(values[i].node);
}
if (this.iehack) {
result.appendData({
type: 'Identifier',
loc: null,
name: this.iehack
});
}
if (this.iehack) {
result.appendData({ type: 'WhiteSpace', value: ' ' });
result.appendData({
type: 'Identifier',
return {
type: 'Value',
loc: null,
name: this.iehack
});
children: result
};
}
return {
type: 'Value',
loc: null,
children: result
};
};
TRBL.prototype.getDeclaration = function() {
return {
type: 'Declaration',
loc: this.loc,
important: this.sides.top.important,
property: this.name,
value: this.getValue()
};
};
getDeclaration() {
return {
type: 'Declaration',
loc: this.loc,
important: this.sides.top.important,
property: this.name,
value: this.getValue()
};
}
}
function processRule(rule, shorts, shortDeclarations, lastShortSelector) {
var declarations = rule.block.children;
var selector = rule.prelude.children.first().id;
const declarations = rule.block.children;
const selector = rule.prelude.children.first.id;
rule.block.children.eachRight(function(declaration, item) {
var property = declaration.property;
rule.block.children.forEachRight(function(declaration, item) {
const property = declaration.property;
if (!MAIN_PROPERTY.hasOwnProperty(property)) {
return;
}
var key = MAIN_PROPERTY[property];
var shorthand;
var operation;
const key = MAIN_PROPERTY[property];
let shorthand;
let operation;
if (!lastShortSelector || selector === lastShortSelector) {
if (key in shorts) {
@@ -367,10 +360,10 @@ function processRule(rule, shorts, shortDeclarations, lastShortSelector) {
shorts[key] = shorthand;
shortDeclarations.push({
operation: operation,
operation,
block: declarations,
item: item,
shorthand: shorthand
item,
shorthand
});
lastShortSelector = selector;
@@ -381,7 +374,7 @@ function processRule(rule, shorts, shortDeclarations, lastShortSelector) {
function processShorthands(shortDeclarations, markDeclaration) {
shortDeclarations.forEach(function(item) {
var shorthand = item.shorthand;
const shorthand = item.shorthand;
if (!shorthand.isOkToMinimize()) {
return;
@@ -395,18 +388,18 @@ function processShorthands(shortDeclarations, markDeclaration) {
});
}
module.exports = function restructBlock(ast, indexer) {
var stylesheetMap = {};
var shortDeclarations = [];
export default function restructBlock(ast, indexer) {
const stylesheetMap = {};
const shortDeclarations = [];
walk(ast, {
visit: 'Rule',
reverse: true,
enter: function(node) {
var stylesheet = this.block || this.stylesheet;
var ruleId = (node.pseudoSignature || '') + '|' + node.prelude.children.first().id;
var ruleMap;
var shorts;
enter(node) {
const stylesheet = this.block || this.stylesheet;
const ruleId = (node.pseudoSignature || '') + '|' + node.prelude.children.first.id;
let ruleMap;
let shorts;
if (!stylesheetMap.hasOwnProperty(stylesheet.id)) {
ruleMap = {

View File

@@ -1,20 +1,23 @@
var resolveProperty = require('css-tree').property;
var resolveKeyword = require('css-tree').keyword;
var walk = require('css-tree').walk;
var generate = require('css-tree').generate;
var fingerprintId = 1;
var dontRestructure = {
'src': 1 // https://github.com/afelix/csso/issues/50
};
import {
walk,
generate,
property as resolveProperty,
keyword as resolveKeyword
} from 'css-tree';
var DONT_MIX_VALUE = {
let fingerprintId = 1;
const dontRestructure = new Set([
'src' // https://github.com/afelix/csso/issues/50
]);
const DONT_MIX_VALUE = {
// https://developer.mozilla.org/en-US/docs/Web/CSS/display#Browser_compatibility
'display': /table|ruby|flex|-(flex)?box$|grid|contents|run-in/i,
// https://developer.mozilla.org/en/docs/Web/CSS/text-align
'text-align': /^(start|end|match-parent|justify-all)$/i
};
var SAFE_VALUES = {
const SAFE_VALUES = {
cursor: [
'auto', 'crosshair', 'default', 'move', 'text', 'wait', 'help',
'n-resize', 'e-resize', 's-resize', 'w-resize',
@@ -30,7 +33,7 @@ var SAFE_VALUES = {
]
};
var NEEDLESS_TABLE = {
const NEEDLESS_TABLE = {
'border-width': ['border'],
'border-style': ['border'],
'border-color': ['border'],
@@ -69,37 +72,37 @@ var NEEDLESS_TABLE = {
};
function getPropertyFingerprint(propertyName, declaration, fingerprints) {
var realName = resolveProperty(propertyName).basename;
const realName = resolveProperty(propertyName).basename;
if (realName === 'background') {
return propertyName + ':' + generate(declaration.value);
}
var declarationId = declaration.id;
var fingerprint = fingerprints[declarationId];
const declarationId = declaration.id;
let fingerprint = fingerprints[declarationId];
if (!fingerprint) {
switch (declaration.value.type) {
case 'Value':
var vendorId = '';
var iehack = '';
var special = {};
var raw = false;
const special = {};
let vendorId = '';
let iehack = '';
let raw = false;
declaration.value.children.each(function walk(node) {
declaration.value.children.forEach(function walk(node) {
switch (node.type) {
case 'Value':
case 'Brackets':
case 'Parentheses':
node.children.each(walk);
node.children.forEach(walk);
break;
case 'Raw':
raw = true;
break;
case 'Identifier':
var name = node.name;
case 'Identifier': {
const { name } = node;
if (!vendorId) {
vendorId = resolveKeyword(name).vendor;
@@ -120,9 +123,10 @@ function getPropertyFingerprint(propertyName, declaration, fingerprints) {
}
break;
}
case 'Function':
var name = node.name;
case 'Function': {
let { name } = node;
if (!vendorId) {
vendorId = resolveKeyword(name).vendor;
@@ -133,9 +137,10 @@ function getPropertyFingerprint(propertyName, declaration, fingerprints) {
// rect(<top>, <right>, <bottom>, <left>) - standart
// rect(<top> <right> <bottom> <left>) backwards compatible syntax
// only the same form values can be merged
var hasComma = node.children.some(function(node) {
return node.type === 'Operator' && node.value === ',';
});
const hasComma = node.children.some((node) =>
node.type === 'Operator' && node.value === ','
);
if (!hasComma) {
name = 'rect-backward';
}
@@ -144,12 +149,13 @@ function getPropertyFingerprint(propertyName, declaration, fingerprints) {
special[name + '()'] = true;
// check nested tokens too
node.children.each(walk);
node.children.forEach(walk);
break;
}
case 'Dimension':
var unit = node.unit;
case 'Dimension': {
const { unit } = node;
if (/\\[09]/.test(unit)) {
iehack = RegExp.lastMatch;
@@ -169,7 +175,9 @@ function getPropertyFingerprint(propertyName, declaration, fingerprints) {
special[unit] = true;
break;
}
break;
}
}
});
@@ -193,14 +201,14 @@ function getPropertyFingerprint(propertyName, declaration, fingerprints) {
}
function needless(props, declaration, fingerprints) {
var property = resolveProperty(declaration.property);
const property = resolveProperty(declaration.property);
if (NEEDLESS_TABLE.hasOwnProperty(property.basename)) {
var table = NEEDLESS_TABLE[property.basename];
const table = NEEDLESS_TABLE[property.basename];
for (var i = 0; i < table.length; i++) {
var ppre = getPropertyFingerprint(property.prefix + table[i], declaration, fingerprints);
var prev = props.hasOwnProperty(ppre) ? props[ppre] : null;
for (const entry of table) {
const ppre = getPropertyFingerprint(property.prefix + entry, declaration, fingerprints);
const prev = props.hasOwnProperty(ppre) ? props[ppre] : null;
if (prev && (!declaration.important || prev.item.data.important)) {
return prev;
@@ -210,14 +218,14 @@ function needless(props, declaration, fingerprints) {
}
function processRule(rule, item, list, props, fingerprints) {
var declarations = rule.block.children;
const declarations = rule.block.children;
declarations.eachRight(function(declaration, declarationItem) {
var property = declaration.property;
var fingerprint = getPropertyFingerprint(property, declaration, fingerprints);
var prev = props[fingerprint];
declarations.forEachRight(function(declaration, declarationItem) {
const { property } = declaration;
const fingerprint = getPropertyFingerprint(property, declaration, fingerprints);
const prev = props[fingerprint];
if (prev && !dontRestructure.hasOwnProperty(property)) {
if (prev && !dontRestructure.has(property)) {
if (declaration.important && !prev.item.data.important) {
props[fingerprint] = {
block: declarations,
@@ -241,7 +249,7 @@ function processRule(rule, item, list, props, fingerprints) {
// };
}
} else {
var prev = needless(props, declaration, fingerprints);
const prev = needless(props, declaration, fingerprints);
if (prev) {
declarations.remove(declarationItem);
@@ -262,23 +270,23 @@ function processRule(rule, item, list, props, fingerprints) {
}
});
if (declarations.isEmpty()) {
if (declarations.isEmpty) {
list.remove(item);
}
}
module.exports = function restructBlock(ast) {
var stylesheetMap = {};
var fingerprints = Object.create(null);
export default function restructBlock(ast) {
const stylesheetMap = {};
const fingerprints = Object.create(null);
walk(ast, {
visit: 'Rule',
reverse: true,
enter: function(node, item, list) {
var stylesheet = this.block || this.stylesheet;
var ruleId = (node.pseudoSignature || '') + '|' + node.prelude.children.first().id;
var ruleMap;
var props;
enter(node, item, list) {
const stylesheet = this.block || this.stylesheet;
const ruleId = (node.pseudoSignature || '') + '|' + node.prelude.children.first.id;
let ruleMap;
let props;
if (!stylesheetMap.hasOwnProperty(stylesheet.id)) {
ruleMap = {};

View File

@@ -1,5 +1,5 @@
var walk = require('css-tree').walk;
var utils = require('./utils');
import { walk } from 'css-tree';
import { unsafeToSkipNode, isEqualDeclarations} from './utils.js';
/*
At this step all rules has single simple selector. We try to join by equal
@@ -14,24 +14,24 @@ var utils = require('./utils');
*/
function processRule(node, item, list) {
var selectors = node.prelude.children;
var declarations = node.block.children;
var nodeCompareMarker = selectors.first().compareMarker;
var skippedCompareMarkers = {};
const selectors = node.prelude.children;
const declarations = node.block.children;
const nodeCompareMarker = selectors.first.compareMarker;
const skippedCompareMarkers = {};
list.nextUntil(item.next, function(next, nextItem) {
// skip non-ruleset node if safe
if (next.type !== 'Rule') {
return utils.unsafeToSkipNode.call(selectors, next);
return unsafeToSkipNode.call(selectors, next);
}
if (node.pseudoSignature !== next.pseudoSignature) {
return true;
}
var nextFirstSelector = next.prelude.children.head;
var nextDeclarations = next.block.children;
var nextCompareMarker = nextFirstSelector.data.compareMarker;
const nextFirstSelector = next.prelude.children.head;
const nextDeclarations = next.block.children;
const nextCompareMarker = nextFirstSelector.data.compareMarker;
// if next ruleset has same marked as one of skipped then stop joining
if (nextCompareMarker in skippedCompareMarkers) {
@@ -40,7 +40,7 @@ function processRule(node, item, list) {
// try to join by selectors
if (selectors.head === selectors.tail) {
if (selectors.first().id === nextFirstSelector.data.id) {
if (selectors.first.id === nextFirstSelector.data.id) {
declarations.appendList(nextDeclarations);
list.remove(nextItem);
return;
@@ -48,11 +48,11 @@ function processRule(node, item, list) {
}
// try to join by properties
if (utils.isEqualDeclarations(declarations, nextDeclarations)) {
var nextStr = nextFirstSelector.data.id;
if (isEqualDeclarations(declarations, nextDeclarations)) {
const nextStr = nextFirstSelector.data.id;
selectors.some(function(data, item) {
var curStr = data.id;
selectors.some((data, item) => {
const curStr = data.id;
if (nextStr < curStr) {
selectors.insert(nextFirstSelector, item);
@@ -78,7 +78,7 @@ function processRule(node, item, list) {
});
}
module.exports = function mergeRule(ast) {
export default function mergeRule(ast) {
walk(ast, {
visit: 'Rule',
enter: processRule

View File

@@ -1,22 +1,20 @@
var List = require('css-tree').List;
var walk = require('css-tree').walk;
var utils = require('./utils');
import { List, walk } from 'css-tree';
import {
unsafeToSkipNode,
isEqualSelectors,
compareDeclarations,
addSelectors
} from './utils.js';
function calcSelectorLength(list) {
var length = 0;
list.each(function(data) {
length += data.id.length + 1;
});
return length - 1;
return list.reduce((res, data) => res + data.id.length + 1, 0) - 1;
}
function calcDeclarationsLength(tokens) {
var length = 0;
let length = 0;
for (var i = 0; i < tokens.length; i++) {
length += tokens[i].length;
for (const token of tokens) {
length += token.length;
}
return (
@@ -26,25 +24,25 @@ function calcDeclarationsLength(tokens) {
}
function processRule(node, item, list) {
var avoidRulesMerge = this.block !== null ? this.block.avoidRulesMerge : false;
var selectors = node.prelude.children;
var block = node.block;
var disallowDownMarkers = Object.create(null);
var allowMergeUp = true;
var allowMergeDown = true;
const avoidRulesMerge = this.block !== null ? this.block.avoidRulesMerge : false;
const selectors = node.prelude.children;
const block = node.block;
const disallowDownMarkers = Object.create(null);
let allowMergeUp = true;
let allowMergeDown = true;
list.prevUntil(item.prev, function(prev, prevItem) {
var prevBlock = prev.block;
var prevType = prev.type;
const prevBlock = prev.block;
const prevType = prev.type;
if (prevType !== 'Rule') {
var unsafe = utils.unsafeToSkipNode.call(selectors, prev);
const unsafe = unsafeToSkipNode.call(selectors, prev);
if (!unsafe && prevType === 'Atrule' && prevBlock) {
walk(prevBlock, {
visit: 'Rule',
enter: function(node) {
node.prelude.children.each(function(data) {
enter(node) {
node.prelude.children.forEach((data) => {
disallowDownMarkers[data.compareMarker] = true;
});
}
@@ -54,15 +52,15 @@ function processRule(node, item, list) {
return unsafe;
}
var prevSelectors = prev.prelude.children;
if (node.pseudoSignature !== prev.pseudoSignature) {
return true;
}
allowMergeDown = !prevSelectors.some(function(selector) {
return selector.compareMarker in disallowDownMarkers;
});
const prevSelectors = prev.prelude.children;
allowMergeDown = !prevSelectors.some((selector) =>
selector.compareMarker in disallowDownMarkers
);
// try prev ruleset if simpleselectors has no equal specifity and element selector
if (!allowMergeDown && !allowMergeUp) {
@@ -70,14 +68,15 @@ function processRule(node, item, list) {
}
// try to join by selectors
if (allowMergeUp && utils.isEqualSelectors(prevSelectors, selectors)) {
if (allowMergeUp && isEqualSelectors(prevSelectors, selectors)) {
prevBlock.children.appendList(block.children);
list.remove(item);
return true;
}
// try to join by properties
var diff = utils.compareDeclarations(block.children, prevBlock.children);
const diff = compareDeclarations(block.children, prevBlock.children);
// console.log(diff.eq, diff.ne1, diff.ne2);
@@ -85,7 +84,7 @@ function processRule(node, item, list) {
if (!diff.ne1.length && !diff.ne2.length) {
// equal blocks
if (allowMergeDown) {
utils.addSelectors(selectors, prevSelectors);
addSelectors(selectors, prevSelectors);
list.remove(prevItem);
}
@@ -95,37 +94,37 @@ function processRule(node, item, list) {
if (diff.ne1.length && !diff.ne2.length) {
// prevBlock is subset block
var selectorLength = calcSelectorLength(selectors);
var blockLength = calcDeclarationsLength(diff.eq); // declarations length
const selectorLength = calcSelectorLength(selectors);
const blockLength = calcDeclarationsLength(diff.eq); // declarations length
if (allowMergeUp && selectorLength < blockLength) {
utils.addSelectors(prevSelectors, selectors);
block.children = new List().fromArray(diff.ne1);
addSelectors(prevSelectors, selectors);
block.children.fromArray(diff.ne1);
}
} else if (!diff.ne1.length && diff.ne2.length) {
// node is subset of prevBlock
var selectorLength = calcSelectorLength(prevSelectors);
var blockLength = calcDeclarationsLength(diff.eq); // declarations length
const selectorLength = calcSelectorLength(prevSelectors);
const blockLength = calcDeclarationsLength(diff.eq); // declarations length
if (allowMergeDown && selectorLength < blockLength) {
utils.addSelectors(selectors, prevSelectors);
prevBlock.children = new List().fromArray(diff.ne2);
addSelectors(selectors, prevSelectors);
prevBlock.children.fromArray(diff.ne2);
}
} else {
// diff.ne1.length && diff.ne2.length
// extract equal block
var newSelector = {
const newSelector = {
type: 'SelectorList',
loc: null,
children: utils.addSelectors(prevSelectors.copy(), selectors)
children: addSelectors(prevSelectors.copy(), selectors)
};
var newBlockLength = calcSelectorLength(newSelector.children) + 2; // selectors length + curly braces length
var blockLength = calcDeclarationsLength(diff.eq); // declarations length
const newBlockLength = calcSelectorLength(newSelector.children) + 2; // selectors length + curly braces length
const blockLength = calcDeclarationsLength(diff.eq); // declarations length
// create new ruleset if declarations length greater than
// ruleset description overhead
if (blockLength >= newBlockLength) {
var newItem = list.createItem({
const newItem = list.createItem({
type: 'Rule',
loc: null,
prelude: newSelector,
@@ -137,8 +136,8 @@ function processRule(node, item, list) {
pseudoSignature: node.pseudoSignature
});
block.children = new List().fromArray(diff.ne1);
prevBlock.children = new List().fromArray(diff.ne2overrided);
block.children.fromArray(diff.ne1);
prevBlock.children.fromArray(diff.ne2overrided);
if (allowMergeUp) {
list.insert(newItem, prevItem);
@@ -155,20 +154,20 @@ function processRule(node, item, list) {
if (allowMergeUp) {
// TODO: disallow up merge only if any property interception only (i.e. diff.ne2overrided.length > 0);
// await property families to find property interception correctly
allowMergeUp = !prevSelectors.some(function(prevSelector) {
return selectors.some(function(selector) {
return selector.compareMarker === prevSelector.compareMarker;
});
});
allowMergeUp = !prevSelectors.some((prevSelector) =>
selectors.some((selector) =>
selector.compareMarker === prevSelector.compareMarker
)
);
}
prevSelectors.each(function(data) {
prevSelectors.forEach((data) => {
disallowDownMarkers[data.compareMarker] = true;
});
});
}
module.exports = function restructRule(ast) {
export default function restructRule(ast) {
walk(ast, {
visit: 'Rule',
reverse: true,

View File

@@ -1,15 +1,15 @@
var prepare = require('./prepare/index');
var mergeAtrule = require('./1-mergeAtrule');
var initialMergeRuleset = require('./2-initialMergeRuleset');
var disjoinRuleset = require('./3-disjoinRuleset');
var restructShorthand = require('./4-restructShorthand');
var restructBlock = require('./6-restructBlock');
var mergeRuleset = require('./7-mergeRuleset');
var restructRuleset = require('./8-restructRuleset');
import prepare from './prepare/index.js';
import mergeAtrule from './1-mergeAtrule.js';
import initialMergeRuleset from './2-initialMergeRuleset.js';
import disjoinRuleset from './3-disjoinRuleset.js';
import restructShorthand from './4-restructShorthand.js';
import restructBlock from './6-restructBlock.js';
import mergeRuleset from './7-mergeRuleset.js';
import restructRuleset from './8-restructRuleset.js';
module.exports = function(ast, options) {
export default function(ast, options) {
// prepare ast for restructing
var indexer = prepare(ast, options);
const indexer = prepare(ast, options);
options.logger('prepare', ast);
mergeAtrule(ast, options);

View File

@@ -1,26 +1,26 @@
var generate = require('css-tree').generate;
import { generate } from 'css-tree';
function Index() {
this.seed = 0;
this.map = Object.create(null);
}
Index.prototype.resolve = function(str) {
var index = this.map[str];
if (!index) {
index = ++this.seed;
this.map[str] = index;
class Index {
constructor() {
this.map = new Map();
}
resolve(str) {
let index = this.map.get(str);
return index;
if (index === undefined) {
index = this.map.size + 1;
this.map.set(str, index);
}
return index;
}
};
module.exports = function createDeclarationIndexer() {
var ids = new Index();
export default function createDeclarationIndexer() {
const ids = new Index();
return function markDeclaration(node) {
var id = generate(node);
const id = generate(node);
node.id = ids.resolve(id);
node.length = id.length;

View File

@@ -1,23 +1,21 @@
var resolveKeyword = require('css-tree').keyword;
var walk = require('css-tree').walk;
var generate = require('css-tree').generate;
var createDeclarationIndexer = require('./createDeclarationIndexer');
var processSelector = require('./processSelector');
import { walk, generate, keyword as resolveKeyword } from 'css-tree';
import createDeclarationIndexer from './createDeclarationIndexer.js';
import processSelector from './processSelector.js';
module.exports = function prepare(ast, options) {
var markDeclaration = createDeclarationIndexer();
export default function prepare(ast, options) {
const markDeclaration = createDeclarationIndexer();
walk(ast, {
visit: 'Rule',
enter: function processRule(node) {
node.block.children.each(markDeclaration);
enter(node) {
node.block.children.forEach(markDeclaration);
processSelector(node, options.usage);
}
});
walk(ast, {
visit: 'Atrule',
enter: function(node) {
enter(node) {
if (node.prelude) {
node.prelude.id = null; // pre-init property to avoid multiple hidden class for generate
node.prelude.id = generate(node.prelude);
@@ -28,8 +26,8 @@ module.exports = function prepare(ast, options) {
if (resolveKeyword(node.name).basename === 'keyframes') {
node.block.avoidRulesMerge = true; /* probably we don't need to prevent those merges for @keyframes
TODO: need to be checked */
node.block.children.each(function(rule) {
rule.prelude.children.each(function(simpleselector) {
node.block.children.forEach(function(rule) {
rule.prelude.children.forEach(function(simpleselector) {
simpleselector.compareMarker = simpleselector.id;
});
});

View File

@@ -1,36 +1,35 @@
var generate = require('css-tree').generate;
var specificity = require('./specificity');
import { generate } from 'css-tree';
import specificity from './specificity.js';
var nonFreezePseudoElements = {
'first-letter': true,
'first-line': true,
'after': true,
'before': true
};
var nonFreezePseudoClasses = {
'link': true,
'visited': true,
'hover': true,
'active': true,
'first-letter': true,
'first-line': true,
'after': true,
'before': true
};
const nonFreezePseudoElements = new Set([
'first-letter',
'first-line',
'after',
'before'
]);
const nonFreezePseudoClasses = new Set([
'link',
'visited',
'hover',
'active',
'first-letter',
'first-line',
'after',
'before'
]);
module.exports = function freeze(node, usageData) {
var pseudos = Object.create(null);
var hasPseudo = false;
export default function processSelector(node, usageData) {
const pseudos = new Set();
node.prelude.children.each(function(simpleSelector) {
var tagName = '*';
var scope = 0;
node.prelude.children.forEach(function(simpleSelector) {
let tagName = '*';
let scope = 0;
simpleSelector.children.each(function(node) {
simpleSelector.children.forEach(function(node) {
switch (node.type) {
case 'ClassSelector':
if (usageData && usageData.scopes) {
var classScope = usageData.scopes[node.name] || 0;
const classScope = usageData.scopes[node.name] || 0;
if (scope !== 0 && classScope !== scope) {
throw new Error('Selector can\'t has classes from different scopes: ' + generate(simpleSelector));
@@ -38,25 +37,28 @@ module.exports = function freeze(node, usageData) {
scope = classScope;
}
break;
case 'PseudoClassSelector':
var name = node.name.toLowerCase();
case 'PseudoClassSelector': {
const name = node.name.toLowerCase();
if (!nonFreezePseudoClasses.hasOwnProperty(name)) {
pseudos[':' + name] = true;
hasPseudo = true;
if (!nonFreezePseudoClasses.has(name)) {
pseudos.add(`:${name}`);
}
break;
}
case 'PseudoElementSelector':
var name = node.name.toLowerCase();
case 'PseudoElementSelector': {
const name = node.name.toLowerCase();
if (!nonFreezePseudoElements.hasOwnProperty(name)) {
pseudos['::' + name] = true;
hasPseudo = true;
if (!nonFreezePseudoElements.has(name)) {
pseudos.add(`::${name}`);
}
break;
}
case 'TypeSelector':
tagName = node.name.toLowerCase();
@@ -64,12 +66,11 @@ module.exports = function freeze(node, usageData) {
case 'AttributeSelector':
if (node.flags) {
pseudos['[' + node.flags.toLowerCase() + ']'] = true;
hasPseudo = true;
pseudos.add(`[${node.flags.toLowerCase()}]`);
}
break;
case 'WhiteSpace':
case 'Combinator':
tagName = '*';
break;
@@ -90,5 +91,7 @@ module.exports = function freeze(node, usageData) {
});
// add property to all rule nodes to avoid multiple hidden class
node.pseudoSignature = hasPseudo && Object.keys(pseudos).sort().join(',');
node.pseudoSignature = pseudos.size > 0
? [...pseudos].sort().join(',')
: false;
};

View File

@@ -1,30 +1,103 @@
module.exports = function specificity(simpleSelector) {
var A = 0;
var B = 0;
var C = 0;
import { parse } from 'css-tree';
simpleSelector.children.each(function walk(node) {
function ensureSelectorList(node) {
if (node.type === 'Raw') {
return parse(node.value, { context: 'selectorList' });
}
return node;
}
function maxSpecificity(a, b) {
for (let i = 0; i < 3; i++) {
if (a[i] !== b[i]) {
return a[i] > b[i] ? a : b;
}
}
return a;
}
function maxSelectorListSpecificity(selectorList) {
return ensureSelectorList(selectorList).children.reduce(
(result, node) => maxSpecificity(specificity(node), result),
[0, 0, 0]
);
}
// §16. Calculating a selectors specificity
// https://www.w3.org/TR/selectors-4/#specificity-rules
function specificity(simpleSelector) {
let A = 0;
let B = 0;
let C = 0;
// A selectors specificity is calculated for a given element as follows:
simpleSelector.children.forEach((node) => {
switch (node.type) {
case 'SelectorList':
case 'Selector':
node.children.each(walk);
break;
// count the number of ID selectors in the selector (= A)
case 'IdSelector':
A++;
break;
// count the number of class selectors, attributes selectors, ...
case 'ClassSelector':
case 'AttributeSelector':
B++;
break;
// ... and pseudo-classes in the selector (= B)
case 'PseudoClassSelector':
switch (node.name.toLowerCase()) {
// The specificity of an :is(), :not(), or :has() pseudo-class is replaced
// by the specificity of the most specific complex selector in its selector list argument.
case 'not':
node.children.each(walk);
case 'has':
case 'is':
// :matches() is used before it was renamed to :is()
// https://github.com/w3c/csswg-drafts/issues/3258
case 'matches':
// Older browsers support :is() functionality as prefixed pseudo-class :any()
// https://developer.mozilla.org/en-US/docs/Web/CSS/:is
case '-webkit-any':
case '-moz-any': {
const [a, b, c] = maxSelectorListSpecificity(node.children.first);
A += a;
B += b;
C += c;
break;
}
// Analogously, the specificity of an :nth-child() or :nth-last-child() selector
// is the specificity of the pseudo class itself (counting as one pseudo-class selector)
// plus the specificity of the most specific complex selector in its selector list argument (if any).
case 'nth-child':
case 'nth-last-child': {
const arg = node.children.first;
if (arg.type === 'Nth' && arg.selector) {
const [a, b, c] = maxSelectorListSpecificity(arg.selector);
A += a;
B += b + 1;
C += c;
} else {
B++;
}
break;
}
// The specificity of a :where() pseudo-class is replaced by zero.
case 'where':
break;
// The four Level 2 pseudo-elements (::before, ::after, ::first-line, and ::first-letter) may,
// for legacy reasons, be represented using the <pseudo-class-selector> grammar,
// with only a single ":" character at their start.
// https://www.w3.org/TR/selectors-4/#single-colon-pseudos
case 'before':
case 'after':
case 'first-line':
@@ -32,25 +105,27 @@ module.exports = function specificity(simpleSelector) {
C++;
break;
// TODO: support for :nth-*(.. of <SelectorList>), :matches(), :has()
default:
B++;
}
break;
case 'PseudoElementSelector':
C++;
break;
// count the number of type selectors ...
case 'TypeSelector':
// ignore universal selector
if (node.name.charAt(node.name.length - 1) !== '*') {
// ignore the universal selector
if (!node.name.endsWith('*')) {
C++;
}
break;
// ... and pseudo-elements in the selector (= C)
case 'PseudoElementSelector':
C++;
break;
}
});
return [A, B, C];
};
export default specificity;

View File

@@ -1,8 +1,8 @@
var hasOwnProperty = Object.prototype.hasOwnProperty;
const { hasOwnProperty } = Object.prototype;
function isEqualSelectors(a, b) {
var cursor1 = a.head;
var cursor2 = b.head;
export function isEqualSelectors(a, b) {
let cursor1 = a.head;
let cursor2 = b.head;
while (cursor1 !== null && cursor2 !== null && cursor1.data.id === cursor2.data.id) {
cursor1 = cursor1.next;
@@ -12,9 +12,9 @@ function isEqualSelectors(a, b) {
return cursor1 === null && cursor2 === null;
}
function isEqualDeclarations(a, b) {
var cursor1 = a.head;
var cursor2 = b.head;
export function isEqualDeclarations(a, b) {
let cursor1 = a.head;
let cursor2 = b.head;
while (cursor1 !== null && cursor2 !== null && cursor1.data.id === cursor2.data.id) {
cursor1 = cursor1.next;
@@ -24,23 +24,23 @@ function isEqualDeclarations(a, b) {
return cursor1 === null && cursor2 === null;
}
function compareDeclarations(declarations1, declarations2) {
var result = {
export function compareDeclarations(declarations1, declarations2) {
const result = {
eq: [],
ne1: [],
ne2: [],
ne2overrided: []
};
var fingerprints = Object.create(null);
var declarations2hash = Object.create(null);
const fingerprints = Object.create(null);
const declarations2hash = Object.create(null);
for (var cursor = declarations2.head; cursor; cursor = cursor.next) {
for (let cursor = declarations2.head; cursor; cursor = cursor.next) {
declarations2hash[cursor.data.id] = true;
}
for (var cursor = declarations1.head; cursor; cursor = cursor.next) {
var data = cursor.data;
for (let cursor = declarations1.head; cursor; cursor = cursor.next) {
const data = cursor.data;
if (data.fingerprint) {
fingerprints[data.fingerprint] = data.important;
@@ -54,8 +54,8 @@ function compareDeclarations(declarations1, declarations2) {
}
}
for (var cursor = declarations2.head; cursor; cursor = cursor.next) {
var data = cursor.data;
for (let cursor = declarations2.head; cursor; cursor = cursor.next) {
const data = cursor.data;
if (declarations2hash[data.id]) {
// when declarations1 has an overriding declaration, this is not a difference
@@ -72,13 +72,13 @@ function compareDeclarations(declarations1, declarations2) {
return result;
}
function addSelectors(dest, source) {
source.each(function(sourceData) {
var newStr = sourceData.id;
var cursor = dest.head;
export function addSelectors(dest, source) {
source.forEach((sourceData) => {
const newStr = sourceData.id;
let cursor = dest.head;
while (cursor) {
var nextStr = cursor.data.id;
const nextStr = cursor.data.id;
if (nextStr === newStr) {
return;
@@ -98,11 +98,11 @@ function addSelectors(dest, source) {
}
// check if simpleselectors has no equal specificity and element selector
function hasSimilarSelectors(selectors1, selectors2) {
var cursor1 = selectors1.head;
export function hasSimilarSelectors(selectors1, selectors2) {
let cursor1 = selectors1.head;
while (cursor1 !== null) {
var cursor2 = selectors2.head;
let cursor2 = selectors2.head;
while (cursor2 !== null) {
if (cursor1.data.compareMarker === cursor2.data.compareMarker) {
@@ -119,7 +119,7 @@ function hasSimilarSelectors(selectors1, selectors2) {
}
// test node can't to be skipped
function unsafeToSkipNode(node) {
export function unsafeToSkipNode(node) {
switch (node.type) {
case 'Rule':
// unsafe skip ruleset with selector similarities
@@ -140,12 +140,3 @@ function unsafeToSkipNode(node) {
// unsafe by default
return true;
}
module.exports = {
isEqualSelectors: isEqualSelectors,
isEqualDeclarations: isEqualDeclarations,
compareDeclarations: compareDeclarations,
addSelectors: addSelectors,
hasSimilarSelectors: hasSimilarSelectors,
unsafeToSkipNode: unsafeToSkipNode
};