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,7 +1,7 @@
var resolveKeyword = require('css-tree').keyword;
var compressKeyframes = require('./atrule/keyframes');
import { keyword as resolveKeyword } from 'css-tree';
import compressKeyframes from './atrule/keyframes.js';
module.exports = function(node) {
export default function(node) {
// compress @keyframe selectors
if (resolveKeyword(node.name).basename === 'keyframes') {
compressKeyframes(node);

View File

@@ -1,33 +1,28 @@
// Can unquote attribute detection
// Adopted implementation of Mathias Bynens
// https://github.com/mathiasbynens/mothereff.in/blob/master/unquoted-attributes/eff.js
var escapesRx = /\\([0-9A-Fa-f]{1,6})(\r\n|[ \t\n\f\r])?|\\./g;
var blockUnquoteRx = /^(-?\d|--)|[\u0000-\u002c\u002e\u002f\u003A-\u0040\u005B-\u005E\u0060\u007B-\u009f]/;
const blockUnquoteRx = /^(-?\d|--)|[\u0000-\u002c\u002e\u002f\u003A-\u0040\u005B-\u005E\u0060\u007B-\u009f]/;
function canUnquote(value) {
if (value === '' || value === '-') {
return;
return false;
}
// Escapes are valid, so replace them with a valid non-empty string
value = value.replace(escapesRx, 'a');
return !blockUnquoteRx.test(value);
}
module.exports = function(node) {
var attrValue = node.value;
export default function(node) {
const attrValue = node.value;
if (!attrValue || attrValue.type !== 'String') {
return;
}
var unquotedValue = attrValue.value.replace(/^(.)(.*)\1$/, '$2');
if (canUnquote(unquotedValue)) {
if (canUnquote(attrValue.value)) {
node.value = {
type: 'Identifier',
loc: attrValue.loc,
name: unquotedValue
name: attrValue.value
};
}
};

View File

@@ -1,43 +1,44 @@
var packNumber = require('./Number').pack;
var MATH_FUNCTIONS = {
'calc': true,
'min': true,
'max': true,
'clamp': true
};
var LENGTH_UNIT = {
import { packNumber } from './Number.js';
const MATH_FUNCTIONS = new Set([
'calc',
'min',
'max',
'clamp'
]);
const LENGTH_UNIT = new Set([
// absolute length units
'px': true,
'mm': true,
'cm': true,
'in': true,
'pt': true,
'pc': true,
'px',
'mm',
'cm',
'in',
'pt',
'pc',
// relative length units
'em': true,
'ex': true,
'ch': true,
'rem': true,
'em',
'ex',
'ch',
'rem',
// viewport-percentage lengths
'vh': true,
'vw': true,
'vmin': true,
'vmax': true,
'vm': true
};
'vh',
'vw',
'vmin',
'vmax',
'vm'
]);
module.exports = function compressDimension(node, item) {
var value = packNumber(node.value, item);
export default function compressDimension(node, item) {
const value = packNumber(node.value);
node.value = value;
if (value === '0' && this.declaration !== null && this.atrulePrelude === null) {
var unit = node.unit.toLowerCase();
const unit = node.unit.toLowerCase();
// only length values can be compressed
if (!LENGTH_UNIT.hasOwnProperty(unit)) {
if (!LENGTH_UNIT.has(unit)) {
return;
}
@@ -49,14 +50,14 @@ module.exports = function compressDimension(node, item) {
}
// issue #222: don't remove units inside calc
if (this.function && MATH_FUNCTIONS.hasOwnProperty(this.function.name)) {
if (this.function && MATH_FUNCTIONS.has(this.function.name)) {
return;
}
item.data = {
type: 'Number',
loc: node.loc,
value: value
value
};
}
};

View File

@@ -1,23 +1,23 @@
var OMIT_PLUSSIGN = /^(?:\+|(-))?0*(\d*)(?:\.0*|(\.\d*?)0*)?$/;
var KEEP_PLUSSIGN = /^([\+\-])?0*(\d*)(?:\.0*|(\.\d*?)0*)?$/;
var unsafeToRemovePlusSignAfter = {
Dimension: true,
Hash: true,
Identifier: true,
Number: true,
Raw: true,
UnicodeRange: true
};
const OMIT_PLUSSIGN = /^(?:\+|(-))?0*(\d*)(?:\.0*|(\.\d*?)0*)?$/;
const KEEP_PLUSSIGN = /^([\+\-])?0*(\d*)(?:\.0*|(\.\d*?)0*)?$/;
const unsafeToRemovePlusSignAfter = new Set([
'Dimension',
'Hash',
'Identifier',
'Number',
'Raw',
'UnicodeRange'
]);
function packNumber(value, item) {
export function packNumber(value, item) {
// omit plus sign only if no prev or prev is safe type
var regexp = item && item.prev !== null && unsafeToRemovePlusSignAfter.hasOwnProperty(item.prev.data.type)
const regexp = item && item.prev !== null && unsafeToRemovePlusSignAfter.has(item.prev.data.type)
? KEEP_PLUSSIGN
: OMIT_PLUSSIGN;
// 100 -> '100'
// 00100 -> '100'
// +100 -> '100' (only when safe, e.g. omitting plus sign for 1px+1px leads to single dimension instead of two)
// +100 -> '100'
// -100 -> '-100'
// 0.123 -> '.123'
// 0.12300 -> '.123'
@@ -29,11 +29,12 @@ function packNumber(value, item) {
if (value === '' || value === '-') {
value = '0';
}
// FIXME: is it solution simplier?
// value = String(Number(value)).replace(/^(-?)0+\./, '$1.');
return value;
}
module.exports = function(node, item) {
node.value = packNumber(node.value, item);
export function Number(node) {
node.value = packNumber(node.value);
};
module.exports.pack = packNumber;

View File

@@ -1,6 +1,7 @@
var lexer = require('css-tree').lexer;
var packNumber = require('./Number').pack;
var blacklist = new Set([
import { lexer } from 'css-tree';
import { packNumber } from './Number.js';
const blacklist = new Set([
// see https://github.com/jakubpawlowicz/clean-css/issues/957
'width',
'min-width',
@@ -16,8 +17,8 @@ var blacklist = new Set([
'-ms-flex'
]);
module.exports = function compressPercentage(node, item) {
node.value = packNumber(node.value, item);
export default function compressPercentage(node, item) {
node.value = packNumber(node.value);
if (node.value === '0' && this.declaration && !blacklist.has(this.declaration.property)) {
// try to convert a number

View File

@@ -1,12 +0,0 @@
module.exports = function(node) {
var value = node.value;
// remove escaped newlines, i.e.
// .a { content: "foo\
// bar"}
// ->
// .a { content: "foobar" }
value = value.replace(/\\(\r\n|\r|\n|\f)/g, '');
node.value = value;
};

33
node_modules/csso/lib/replace/Url.js generated vendored
View File

@@ -1,33 +1,4 @@
var UNICODE = '\\\\[0-9a-f]{1,6}(\\r\\n|[ \\n\\r\\t\\f])?';
var ESCAPE = '(' + UNICODE + '|\\\\[^\\n\\r\\f0-9a-fA-F])';
var NONPRINTABLE = '\u0000\u0008\u000b\u000e-\u001f\u007f';
var SAFE_URL = new RegExp('^(' + ESCAPE + '|[^\"\'\\(\\)\\\\\\s' + NONPRINTABLE + '])*$', 'i');
module.exports = function(node) {
var value = node.value;
if (value.type !== 'String') {
return;
}
var quote = value.value[0];
var url = value.value.substr(1, value.value.length - 2);
export default function(node) {
// convert `\\` to `/`
url = url.replace(/\\\\/g, '/');
// remove quotes when safe
// https://www.w3.org/TR/css-syntax-3/#url-unquoted-diagram
if (SAFE_URL.test(url)) {
node.value = {
type: 'Raw',
loc: node.value.loc,
value: url
};
} else {
// use double quotes if string has no double quotes
// otherwise use original quotes
// TODO: make better quote type selection
node.value.value = url.indexOf('"') === -1 ? '"' + url + '"' : quote + url + quote;
}
node.value = node.value.replace(/\\/g, '/');
};

View File

@@ -1,18 +1,24 @@
var resolveName = require('css-tree').property;
var handlers = {
'font': require('./property/font'),
'font-weight': require('./property/font-weight'),
'background': require('./property/background'),
'border': require('./property/border'),
'outline': require('./property/border')
import { property as resolveName } from 'css-tree';
import font from './property/font.js';
import fontWeight from './property/font-weight.js';
import background from './property/background.js';
import border from './property/border.js';
import outline from './property/border.js';
const handlers = {
'font': font,
'font-weight': fontWeight,
'background': background,
'border': border,
'outline': outline
};
module.exports = function compressValue(node) {
export default function compressValue(node) {
if (!this.declaration) {
return;
}
var property = resolveName(this.declaration.property);
const property = resolveName(this.declaration.property);
if (handlers.hasOwnProperty(property.basename)) {
handlers[property.basename](node);

View File

@@ -1,7 +1,7 @@
module.exports = function(node) {
node.block.children.each(function(rule) {
rule.prelude.children.each(function(simpleselector) {
simpleselector.children.each(function(data, item) {
export default function(node) {
node.block.children.forEach((rule) => {
rule.prelude.children.forEach((simpleselector) => {
simpleselector.children.forEach((data, item) => {
if (data.type === 'Percentage' && data.value === '100') {
item.data = {
type: 'TypeSelector',

View File

@@ -1,8 +1,8 @@
var lexer = require('css-tree').lexer;
var packNumber = require('./Number').pack;
import { lexer } from 'css-tree';
import { packNumber } from './Number.js';
// http://www.w3.org/TR/css3-color/#svg-color
var NAME_TO_HEX = {
const NAME_TO_HEX = {
'aliceblue': 'f0f8ff',
'antiquewhite': 'faebd7',
'aqua': '0ff',
@@ -153,7 +153,7 @@ var NAME_TO_HEX = {
'yellowgreen': '9acd32'
};
var HEX_TO_NAME = {
const HEX_TO_NAME = {
'800000': 'maroon',
'800080': 'purple',
'808000': 'olive',
@@ -214,15 +214,15 @@ function hueToRgb(p, q, t) {
}
function hslToRgb(h, s, l, a) {
var r;
var g;
var b;
let r;
let g;
let b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hueToRgb(p, q, h + 1 / 3);
g = hueToRgb(p, q, h);
@@ -239,17 +239,17 @@ function hslToRgb(h, s, l, a) {
function toHex(value) {
value = value.toString(16);
return value.length === 1 ? '0' + value : value;
}
function parseFunctionArgs(functionArgs, count, rgb) {
var cursor = functionArgs.head;
var args = [];
var wasValue = false;
let cursor = functionArgs.head;
let args = [];
let wasValue = false;
while (cursor !== null) {
var node = cursor.data;
var type = node.type;
const { type, value } = cursor.data;
switch (type) {
case 'Number':
@@ -260,20 +260,22 @@ function parseFunctionArgs(functionArgs, count, rgb) {
wasValue = true;
args.push({
type: type,
value: Number(node.value)
type,
value: Number(value)
});
break;
case 'Operator':
if (node.value === ',') {
if (value === ',') {
if (!wasValue) {
return;
}
wasValue = false;
} else if (wasValue || node.value !== '+') {
} else if (wasValue || value !== '+') {
return;
}
break;
default:
@@ -319,7 +321,7 @@ function parseFunctionArgs(functionArgs, count, rgb) {
}
return args.map(function(arg) {
var value = Math.max(0, arg.value);
let value = Math.max(0, arg.value);
switch (arg.type) {
case 'Number':
@@ -351,9 +353,9 @@ function parseFunctionArgs(functionArgs, count, rgb) {
});
}
function compressFunction(node, item, list) {
var functionName = node.name;
var args;
export function compressFunction(node, item) {
let functionName = node.name;
let args;
if (functionName === 'rgba' || functionName === 'hsla') {
args = parseFunctionArgs(node.children, 4, functionName === 'rgba');
@@ -364,7 +366,7 @@ function compressFunction(node, item, list) {
}
if (functionName === 'hsla') {
args = hslToRgb.apply(null, args);
args = hslToRgb(...args);
node.name = 'rgba';
}
@@ -373,7 +375,8 @@ function compressFunction(node, item, list) {
// always replace `rgba(0, 0, 0, 0)` to `transparent`
// otherwise avoid replacement in gradients since it may break color transition
// http://stackoverflow.com/questions/11829410/css3-gradient-rendering-issues-from-transparent-to-white
var scopeFunctionName = this.function && this.function.name;
const scopeFunctionName = this.function && this.function.name;
if ((args[0] === 0 && args[1] === 0 && args[2] === 0) ||
!/^(?:to|from|color-stop)$|gradient$/i.test(scopeFunctionName)) {
@@ -389,7 +392,7 @@ function compressFunction(node, item, list) {
if (args[3] !== 1) {
// replace argument values for normalized/interpolated
node.children.each(function(node, item, list) {
node.children.forEach((node, item, list) => {
if (node.type === 'Operator') {
if (node.value !== ',') {
list.remove(item);
@@ -400,7 +403,7 @@ function compressFunction(node, item, list) {
item.data = {
type: 'Number',
loc: node.loc,
value: packNumber(args.shift(), null)
value: packNumber(args.shift())
};
});
@@ -420,7 +423,7 @@ function compressFunction(node, item, list) {
}
// convert to rgb
args = hslToRgb.apply(null, args);
args = hslToRgb(...args);
functionName = 'rgb';
}
@@ -432,15 +435,6 @@ function compressFunction(node, item, list) {
return;
}
// check if color is not at the end and not followed by space
var next = item.next;
if (next && next.data.type !== 'WhiteSpace') {
list.insert(list.createItem({
type: 'WhiteSpace',
value: ' '
}), next);
}
item.data = {
type: 'Hash',
loc: node.loc,
@@ -451,16 +445,16 @@ function compressFunction(node, item, list) {
}
}
function compressIdent(node, item) {
export function compressIdent(node, item) {
if (this.declaration === null) {
return;
}
var color = node.name.toLowerCase();
let color = node.name.toLowerCase();
if (NAME_TO_HEX.hasOwnProperty(color) &&
lexer.matchDeclaration(this.declaration).isType(node, 'color')) {
var hex = NAME_TO_HEX[color];
const hex = NAME_TO_HEX[color];
if (hex.length + 1 <= color.length) {
// replace for shorter hex value
@@ -481,8 +475,8 @@ function compressIdent(node, item) {
}
}
function compressHex(node, item) {
var color = node.value.toLowerCase();
export function compressHex(node, item) {
let color = node.value.toLowerCase();
// #112233 -> #123
if (color.length === 6 &&
@@ -502,9 +496,3 @@ function compressHex(node, item) {
node.value = color;
}
}
module.exports = {
compressFunction: compressFunction,
compressIdent: compressIdent,
compressHex: compressHex
};

View File

@@ -1,21 +1,29 @@
var walk = require('css-tree').walk;
var handlers = {
Atrule: require('./Atrule'),
AttributeSelector: require('./AttributeSelector'),
Value: require('./Value'),
Dimension: require('./Dimension'),
Percentage: require('./Percentage'),
Number: require('./Number'),
String: require('./String'),
Url: require('./Url'),
Hash: require('./color').compressHex,
Identifier: require('./color').compressIdent,
Function: require('./color').compressFunction
import { walk } from 'css-tree';
import Atrule from './Atrule.js';
import AttributeSelector from './AttributeSelector.js';
import Value from './Value.js';
import Dimension from './Dimension.js';
import Percentage from './Percentage.js';
import { Number } from './Number.js';
import Url from './Url.js';
import { compressHex, compressIdent, compressFunction } from './color.js';
const handlers = {
Atrule,
AttributeSelector,
Value,
Dimension,
Percentage,
Number,
Url,
Hash: compressHex,
Identifier: compressIdent,
Function: compressFunction
};
module.exports = function(ast) {
export default function(ast) {
walk(ast, {
leave: function(node, item, list) {
leave(node, item, list) {
if (handlers.hasOwnProperty(node.type)) {
handlers[node.type].call(this, node, item, list);
}

View File

@@ -1,17 +1,7 @@
var List = require('css-tree').List;
module.exports = function compressBackground(node) {
function lastType() {
if (buffer.length) {
return buffer[buffer.length - 1].type;
}
}
import { List } from 'css-tree';
export default function compressBackground(node) {
function flush() {
if (lastType() === 'WhiteSpace') {
buffer.pop();
}
if (!buffer.length) {
buffer.unshift(
{
@@ -19,10 +9,6 @@ module.exports = function compressBackground(node) {
loc: null,
value: '0'
},
{
type: 'WhiteSpace',
value: ' '
},
{
type: 'Number',
loc: null,
@@ -36,10 +22,10 @@ module.exports = function compressBackground(node) {
buffer = [];
}
var newValue = [];
var buffer = [];
let newValue = [];
let buffer = [];
node.children.each(function(node) {
node.children.forEach((node) => {
if (node.type === 'Operator' && node.value === ',') {
flush();
newValue.push(node);
@@ -56,11 +42,6 @@ module.exports = function compressBackground(node) {
}
}
// don't add redundant spaces
if (node.type === 'WhiteSpace' && (!buffer.length || lastType() === 'WhiteSpace')) {
return;
}
buffer.push(node);
});

View File

@@ -1,20 +1,5 @@
function removeItemAndRedundantWhiteSpace(list, item) {
var prev = item.prev;
var next = item.next;
if (next !== null) {
if (next.data.type === 'WhiteSpace' && (prev === null || prev.data.type === 'WhiteSpace')) {
list.remove(next);
}
} else if (prev !== null && prev.data.type === 'WhiteSpace') {
list.remove(prev);
}
list.remove(item);
}
module.exports = function compressBorder(node) {
node.children.each(function(node, item, list) {
export default function compressBorder(node) {
node.children.forEach((node, item, list) => {
if (node.type === 'Identifier' && node.name.toLowerCase() === 'none') {
if (list.head === list.tail) {
// replace `none` for zero when `none` is a single term
@@ -24,7 +9,7 @@ module.exports = function compressBorder(node) {
value: '0'
};
} else {
removeItemAndRedundantWhiteSpace(list, item);
list.remove(item);
}
}
});

View File

@@ -1,5 +1,5 @@
module.exports = function compressFontWeight(node) {
var value = node.children.head.data;
export default function compressFontWeight(node) {
const value = node.children.head.data;
if (value.type === 'Identifier') {
switch (value.name) {

View File

@@ -1,7 +1,7 @@
module.exports = function compressFont(node) {
var list = node.children;
export default function compressFont(node) {
const list = node.children;
list.eachRight(function(node, item) {
list.forEachRight(function(node, item) {
if (node.type === 'Identifier') {
if (node.name === 'bold') {
item.data = {
@@ -10,33 +10,18 @@ module.exports = function compressFont(node) {
value: '700'
};
} else if (node.name === 'normal') {
var prev = item.prev;
const prev = item.prev;
if (prev && prev.data.type === 'Operator' && prev.data.value === '/') {
this.remove(prev);
}
this.remove(item);
} else if (node.name === 'medium') {
var next = item.next;
if (!next || next.data.type !== 'Operator') {
this.remove(item);
}
}
}
});
// remove redundant spaces
list.each(function(node, item) {
if (node.type === 'WhiteSpace') {
if (!item.prev || !item.next || item.next.data.type === 'WhiteSpace') {
this.remove(item);
}
}
});
if (list.isEmpty()) {
if (list.isEmpty) {
list.insert(list.createItem({
type: 'Identifier',
name: 'normal'