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>
426 lines
12 KiB
JavaScript
Executable File
426 lines
12 KiB
JavaScript
Executable File
import { List, generate, walk } from 'css-tree';
|
|
|
|
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',
|
|
'margin-left': 'left',
|
|
|
|
'padding-top': 'top',
|
|
'padding-right': 'right',
|
|
'padding-bottom': 'bottom',
|
|
'padding-left': 'left',
|
|
|
|
'border-top-color': 'top',
|
|
'border-right-color': 'right',
|
|
'border-bottom-color': 'bottom',
|
|
'border-left-color': 'left',
|
|
'border-top-width': 'top',
|
|
'border-right-width': 'right',
|
|
'border-bottom-width': 'bottom',
|
|
'border-left-width': 'left',
|
|
'border-top-style': 'top',
|
|
'border-right-style': 'right',
|
|
'border-bottom-style': 'bottom',
|
|
'border-left-style': 'left'
|
|
};
|
|
const MAIN_PROPERTY = {
|
|
'margin': 'margin',
|
|
'margin-top': 'margin',
|
|
'margin-right': 'margin',
|
|
'margin-bottom': 'margin',
|
|
'margin-left': 'margin',
|
|
|
|
'padding': 'padding',
|
|
'padding-top': 'padding',
|
|
'padding-right': 'padding',
|
|
'padding-bottom': 'padding',
|
|
'padding-left': 'padding',
|
|
|
|
'border-color': 'border-color',
|
|
'border-top-color': 'border-color',
|
|
'border-right-color': 'border-color',
|
|
'border-bottom-color': 'border-color',
|
|
'border-left-color': 'border-color',
|
|
'border-width': 'border-width',
|
|
'border-top-width': 'border-width',
|
|
'border-right-width': 'border-width',
|
|
'border-bottom-width': 'border-width',
|
|
'border-left-width': 'border-width',
|
|
'border-style': 'border-style',
|
|
'border-top-style': 'border-style',
|
|
'border-right-style': 'border-style',
|
|
'border-bottom-style': 'border-style',
|
|
'border-left-style': 'border-style'
|
|
};
|
|
|
|
class TRBL {
|
|
constructor(name) {
|
|
this.name = name;
|
|
this.loc = null;
|
|
this.iehack = undefined;
|
|
this.sides = {
|
|
'top': null,
|
|
'right': null,
|
|
'bottom': null,
|
|
'left': null
|
|
};
|
|
}
|
|
|
|
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;
|
|
|
|
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':
|
|
|
|
// 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 'Function':
|
|
if (child.name === 'var') {
|
|
return true;
|
|
}
|
|
|
|
special = child.name;
|
|
break;
|
|
|
|
default:
|
|
return true; // bad value
|
|
}
|
|
|
|
values.push({
|
|
node: child,
|
|
special,
|
|
important: declaration.important
|
|
});
|
|
});
|
|
|
|
if (hasBadValues || values.length > count) {
|
|
return false;
|
|
}
|
|
|
|
if (typeof this.iehack === 'string' && this.iehack !== iehack) {
|
|
return false;
|
|
}
|
|
|
|
this.iehack = iehack; // move outside
|
|
|
|
return values;
|
|
}
|
|
|
|
canOverride(side, value) {
|
|
const currentValue = this.sides[side];
|
|
|
|
return !currentValue || (value.important && !currentValue.important);
|
|
}
|
|
|
|
add(name, declaration) {
|
|
function attemptToAdd() {
|
|
const sides = this.sides;
|
|
const side = SIDE[name];
|
|
|
|
if (side) {
|
|
if (side in sides === false) {
|
|
return false;
|
|
}
|
|
|
|
const values = this.getValueSequence(declaration, 1);
|
|
|
|
if (!values || !values.length) {
|
|
return false;
|
|
}
|
|
|
|
// can mix only if specials are equal
|
|
for (const key in sides) {
|
|
if (sides[key] !== null && sides[key].special !== values[0].special) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!this.canOverride(side, values[0])) {
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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)
|
|
];
|
|
|
|
if (stringValues[LEFT] === stringValues[RIGHT]) {
|
|
values.pop();
|
|
if (stringValues[BOTTOM] === stringValues[TOP]) {
|
|
values.pop();
|
|
if (stringValues[RIGHT] === stringValues[TOP]) {
|
|
values.pop();
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < values.length; i++) {
|
|
result.appendData(values[i].node);
|
|
}
|
|
|
|
if (this.iehack) {
|
|
result.appendData({
|
|
type: 'Identifier',
|
|
loc: null,
|
|
name: this.iehack
|
|
});
|
|
}
|
|
|
|
return {
|
|
type: 'Value',
|
|
loc: null,
|
|
children: result
|
|
};
|
|
}
|
|
|
|
getDeclaration() {
|
|
return {
|
|
type: 'Declaration',
|
|
loc: this.loc,
|
|
important: this.sides.top.important,
|
|
property: this.name,
|
|
value: this.getValue()
|
|
};
|
|
}
|
|
}
|
|
|
|
function processRule(rule, shorts, shortDeclarations, lastShortSelector) {
|
|
const declarations = rule.block.children;
|
|
const selector = rule.prelude.children.first.id;
|
|
|
|
rule.block.children.forEachRight(function(declaration, item) {
|
|
const property = declaration.property;
|
|
|
|
if (!MAIN_PROPERTY.hasOwnProperty(property)) {
|
|
return;
|
|
}
|
|
|
|
const key = MAIN_PROPERTY[property];
|
|
let shorthand;
|
|
let operation;
|
|
|
|
if (!lastShortSelector || selector === lastShortSelector) {
|
|
if (key in shorts) {
|
|
operation = REMOVE;
|
|
shorthand = shorts[key];
|
|
}
|
|
}
|
|
|
|
if (!shorthand || !shorthand.add(property, declaration)) {
|
|
operation = REPLACE;
|
|
shorthand = new TRBL(key);
|
|
|
|
// if can't parse value ignore it and break shorthand children
|
|
if (!shorthand.add(property, declaration)) {
|
|
lastShortSelector = null;
|
|
return;
|
|
}
|
|
}
|
|
|
|
shorts[key] = shorthand;
|
|
shortDeclarations.push({
|
|
operation,
|
|
block: declarations,
|
|
item,
|
|
shorthand
|
|
});
|
|
|
|
lastShortSelector = selector;
|
|
});
|
|
|
|
return lastShortSelector;
|
|
}
|
|
|
|
function processShorthands(shortDeclarations, markDeclaration) {
|
|
shortDeclarations.forEach(function(item) {
|
|
const shorthand = item.shorthand;
|
|
|
|
if (!shorthand.isOkToMinimize()) {
|
|
return;
|
|
}
|
|
|
|
if (item.operation === REPLACE) {
|
|
item.item.data = markDeclaration(shorthand.getDeclaration());
|
|
} else {
|
|
item.block.remove(item.item);
|
|
}
|
|
});
|
|
}
|
|
|
|
export default function restructBlock(ast, indexer) {
|
|
const stylesheetMap = {};
|
|
const shortDeclarations = [];
|
|
|
|
walk(ast, {
|
|
visit: 'Rule',
|
|
reverse: true,
|
|
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 = {
|
|
lastShortSelector: null
|
|
};
|
|
stylesheetMap[stylesheet.id] = ruleMap;
|
|
} else {
|
|
ruleMap = stylesheetMap[stylesheet.id];
|
|
}
|
|
|
|
if (ruleMap.hasOwnProperty(ruleId)) {
|
|
shorts = ruleMap[ruleId];
|
|
} else {
|
|
shorts = {};
|
|
ruleMap[ruleId] = shorts;
|
|
}
|
|
|
|
ruleMap.lastShortSelector = processRule.call(this, node, shorts, shortDeclarations, ruleMap.lastShortSelector);
|
|
}
|
|
});
|
|
|
|
processShorthands(shortDeclarations, indexer.declaration);
|
|
};
|