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,116 +1,60 @@
var Tokenizer = require('./tokenizer');
var TAB = 9;
var N = 10;
var F = 12;
var R = 13;
var SPACE = 32;
var EXCLAMATIONMARK = 33; // !
var NUMBERSIGN = 35; // #
var AMPERSAND = 38; // &
var APOSTROPHE = 39; // '
var LEFTPARENTHESIS = 40; // (
var RIGHTPARENTHESIS = 41; // )
var ASTERISK = 42; // *
var PLUSSIGN = 43; // +
var COMMA = 44; // ,
var HYPERMINUS = 45; // -
var LESSTHANSIGN = 60; // <
var GREATERTHANSIGN = 62; // >
var QUESTIONMARK = 63; // ?
var COMMERCIALAT = 64; // @
var LEFTSQUAREBRACKET = 91; // [
var RIGHTSQUAREBRACKET = 93; // ]
var LEFTCURLYBRACKET = 123; // {
var VERTICALLINE = 124; // |
var RIGHTCURLYBRACKET = 125; // }
var INFINITY = 8734; //
var NAME_CHAR = createCharMap(function(ch) {
return /[a-zA-Z0-9\-]/.test(ch);
});
var COMBINATOR_PRECEDENCE = {
import { Scanner } from './scanner.js';
const TAB = 9;
const N = 10;
const F = 12;
const R = 13;
const SPACE = 32;
const EXCLAMATIONMARK = 33; // !
const NUMBERSIGN = 35; // #
const AMPERSAND = 38; // &
const APOSTROPHE = 39; // '
const LEFTPARENTHESIS = 40; // (
const RIGHTPARENTHESIS = 41; // )
const ASTERISK = 42; // *
const PLUSSIGN = 43; // +
const COMMA = 44; // ,
const HYPERMINUS = 45; // -
const LESSTHANSIGN = 60; // <
const GREATERTHANSIGN = 62; // >
const QUESTIONMARK = 63; // ?
const COMMERCIALAT = 64; // @
const LEFTSQUAREBRACKET = 91; // [
const RIGHTSQUAREBRACKET = 93; // ]
const LEFTCURLYBRACKET = 123; // {
const VERTICALLINE = 124; // |
const RIGHTCURLYBRACKET = 125; // }
const INFINITY = 8734; // ∞
const COMBINATOR_PRECEDENCE = {
' ': 1,
'&&': 2,
'||': 3,
'|': 4
};
function createCharMap(fn) {
var array = typeof Uint32Array === 'function' ? new Uint32Array(128) : new Array(128);
for (var i = 0; i < 128; i++) {
array[i] = fn(String.fromCharCode(i)) ? 1 : 0;
}
return array;
}
function readMultiplierRange(scanner) {
let min = null;
let max = null;
function scanSpaces(tokenizer) {
return tokenizer.substringToPos(
tokenizer.findWsEnd(tokenizer.pos)
);
}
scanner.eat(LEFTCURLYBRACKET);
scanner.skipWs();
function scanWord(tokenizer) {
var end = tokenizer.pos;
min = scanner.scanNumber(scanner);
scanner.skipWs();
for (; end < tokenizer.str.length; end++) {
var code = tokenizer.str.charCodeAt(end);
if (code >= 128 || NAME_CHAR[code] === 0) {
break;
}
}
if (scanner.charCode() === COMMA) {
scanner.pos++;
scanner.skipWs();
if (tokenizer.pos === end) {
tokenizer.error('Expect a keyword');
}
return tokenizer.substringToPos(end);
}
function scanNumber(tokenizer) {
var end = tokenizer.pos;
for (; end < tokenizer.str.length; end++) {
var code = tokenizer.str.charCodeAt(end);
if (code < 48 || code > 57) {
break;
}
}
if (tokenizer.pos === end) {
tokenizer.error('Expect a number');
}
return tokenizer.substringToPos(end);
}
function scanString(tokenizer) {
var end = tokenizer.str.indexOf('\'', tokenizer.pos + 1);
if (end === -1) {
tokenizer.pos = tokenizer.str.length;
tokenizer.error('Expect an apostrophe');
}
return tokenizer.substringToPos(end + 1);
}
function readMultiplierRange(tokenizer) {
var min = null;
var max = null;
tokenizer.eat(LEFTCURLYBRACKET);
min = scanNumber(tokenizer);
if (tokenizer.charCode() === COMMA) {
tokenizer.pos++;
if (tokenizer.charCode() !== RIGHTCURLYBRACKET) {
max = scanNumber(tokenizer);
if (scanner.charCode() !== RIGHTCURLYBRACKET) {
max = scanner.scanNumber(scanner);
scanner.skipWs();
}
} else {
max = min;
}
tokenizer.eat(RIGHTCURLYBRACKET);
scanner.eat(RIGHTCURLYBRACKET);
return {
min: Number(min),
@@ -118,13 +62,13 @@ function readMultiplierRange(tokenizer) {
};
}
function readMultiplier(tokenizer) {
var range = null;
var comma = false;
function readMultiplier(scanner) {
let range = null;
let comma = false;
switch (tokenizer.charCode()) {
switch (scanner.charCode()) {
case ASTERISK:
tokenizer.pos++;
scanner.pos++;
range = {
min: 0,
@@ -134,7 +78,7 @@ function readMultiplier(tokenizer) {
break;
case PLUSSIGN:
tokenizer.pos++;
scanner.pos++;
range = {
min: 1,
@@ -144,7 +88,7 @@ function readMultiplier(tokenizer) {
break;
case QUESTIONMARK:
tokenizer.pos++;
scanner.pos++;
range = {
min: 0,
@@ -154,12 +98,22 @@ function readMultiplier(tokenizer) {
break;
case NUMBERSIGN:
tokenizer.pos++;
scanner.pos++;
comma = true;
if (tokenizer.charCode() === LEFTCURLYBRACKET) {
range = readMultiplierRange(tokenizer);
if (scanner.charCode() === LEFTCURLYBRACKET) {
range = readMultiplierRange(scanner);
} else if (scanner.charCode() === QUESTIONMARK) {
// https://www.w3.org/TR/css-values-4/#component-multipliers
// > the # and ? multipliers may be stacked as #?
// In this case just treat "#?" as a single multiplier
// { min: 0, max: 0, comma: true }
scanner.pos++;
range = {
min: 0,
max: 0
};
} else {
range = {
min: 1,
@@ -170,7 +124,7 @@ function readMultiplier(tokenizer) {
break;
case LEFTCURLYBRACKET:
range = readMultiplierRange(tokenizer);
range = readMultiplierRange(scanner);
break;
default:
@@ -179,51 +133,66 @@ function readMultiplier(tokenizer) {
return {
type: 'Multiplier',
comma: comma,
comma,
min: range.min,
max: range.max,
term: null
};
}
function maybeMultiplied(tokenizer, node) {
var multiplier = readMultiplier(tokenizer);
function maybeMultiplied(scanner, node) {
const multiplier = readMultiplier(scanner);
if (multiplier !== null) {
multiplier.term = node;
// https://www.w3.org/TR/css-values-4/#component-multipliers
// > The + and # multipliers may be stacked as +#;
// Represent "+#" as nested multipliers:
// { ...<multiplier #>,
// term: {
// ...<multipler +>,
// term: node
// }
// }
if (scanner.charCode() === NUMBERSIGN &&
scanner.charCodeAt(scanner.pos - 1) === PLUSSIGN) {
return maybeMultiplied(scanner, multiplier);
}
return multiplier;
}
return node;
}
function maybeToken(tokenizer) {
var ch = tokenizer.peek();
function maybeToken(scanner) {
const ch = scanner.peek();
if (ch === '') {
return null;
}
return {
return maybeMultiplied(scanner, {
type: 'Token',
value: ch
};
});
}
function readProperty(tokenizer) {
var name;
function readProperty(scanner) {
let name;
tokenizer.eat(LESSTHANSIGN);
tokenizer.eat(APOSTROPHE);
scanner.eat(LESSTHANSIGN);
scanner.eat(APOSTROPHE);
name = scanWord(tokenizer);
name = scanner.scanWord();
tokenizer.eat(APOSTROPHE);
tokenizer.eat(GREATERTHANSIGN);
scanner.eat(APOSTROPHE);
scanner.eat(GREATERTHANSIGN);
return maybeMultiplied(tokenizer, {
return maybeMultiplied(scanner, {
type: 'Property',
name: name
name
});
}
@@ -234,101 +203,118 @@ function readProperty(tokenizer) {
// range notation—[min,max]—within the angle brackets, after the identifying keyword,
// indicating a closed range between (and including) min and max.
// For example, <integer [0, 10]> indicates an integer between 0 and 10, inclusive.
function readTypeRange(tokenizer) {
function readTypeRange(scanner) {
// use null for Infinity to make AST format JSON serializable/deserializable
var min = null; // -Infinity
var max = null; // Infinity
var sign = 1;
let min = null; // -Infinity
let max = null; // Infinity
let sign = 1;
tokenizer.eat(LEFTSQUAREBRACKET);
scanner.eat(LEFTSQUAREBRACKET);
if (tokenizer.charCode() === HYPERMINUS) {
tokenizer.peek();
if (scanner.charCode() === HYPERMINUS) {
scanner.peek();
sign = -1;
}
if (sign == -1 && tokenizer.charCode() === INFINITY) {
tokenizer.peek();
if (sign == -1 && scanner.charCode() === INFINITY) {
scanner.peek();
} else {
min = sign * Number(scanNumber(tokenizer));
min = sign * Number(scanner.scanNumber(scanner));
if (scanner.isNameCharCode()) {
min += scanner.scanWord();
}
}
scanSpaces(tokenizer);
tokenizer.eat(COMMA);
scanSpaces(tokenizer);
scanner.skipWs();
scanner.eat(COMMA);
scanner.skipWs();
if (tokenizer.charCode() === INFINITY) {
tokenizer.peek();
if (scanner.charCode() === INFINITY) {
scanner.peek();
} else {
sign = 1;
if (tokenizer.charCode() === HYPERMINUS) {
tokenizer.peek();
if (scanner.charCode() === HYPERMINUS) {
scanner.peek();
sign = -1;
}
max = sign * Number(scanNumber(tokenizer));
max = sign * Number(scanner.scanNumber(scanner));
if (scanner.isNameCharCode()) {
max += scanner.scanWord();
}
}
tokenizer.eat(RIGHTSQUAREBRACKET);
// If no range is indicated, either by using the bracketed range notation
// or in the property description, then [−∞,∞] is assumed.
if (min === null && max === null) {
return null;
}
scanner.eat(RIGHTSQUAREBRACKET);
return {
type: 'Range',
min: min,
max: max
min,
max
};
}
function readType(tokenizer) {
var name;
var opts = null;
function readType(scanner) {
let name;
let opts = null;
tokenizer.eat(LESSTHANSIGN);
name = scanWord(tokenizer);
scanner.eat(LESSTHANSIGN);
name = scanner.scanWord();
if (tokenizer.charCode() === LEFTPARENTHESIS &&
tokenizer.nextCharCode() === RIGHTPARENTHESIS) {
tokenizer.pos += 2;
// https://drafts.csswg.org/css-values-5/#boolean
if (name === 'boolean-expr') {
scanner.eat(LEFTSQUAREBRACKET);
const implicitGroup = readImplicitGroup(scanner, RIGHTSQUAREBRACKET);
scanner.eat(RIGHTSQUAREBRACKET);
scanner.eat(GREATERTHANSIGN);
return maybeMultiplied(scanner, {
type: 'Boolean',
term: implicitGroup.terms.length === 1
? implicitGroup.terms[0]
: implicitGroup
});
}
if (scanner.charCode() === LEFTPARENTHESIS &&
scanner.nextCharCode() === RIGHTPARENTHESIS) {
scanner.pos += 2;
name += '()';
}
if (tokenizer.charCodeAt(tokenizer.findWsEnd(tokenizer.pos)) === LEFTSQUAREBRACKET) {
scanSpaces(tokenizer);
opts = readTypeRange(tokenizer);
if (scanner.charCodeAt(scanner.findWsEnd(scanner.pos)) === LEFTSQUAREBRACKET) {
scanner.skipWs();
opts = readTypeRange(scanner);
}
tokenizer.eat(GREATERTHANSIGN);
scanner.eat(GREATERTHANSIGN);
return maybeMultiplied(tokenizer, {
return maybeMultiplied(scanner, {
type: 'Type',
name: name,
opts: opts
name,
opts
});
}
function readKeywordOrFunction(tokenizer) {
var name;
function readKeywordOrFunction(scanner) {
const name = scanner.scanWord();
name = scanWord(tokenizer);
if (tokenizer.charCode() === LEFTPARENTHESIS) {
tokenizer.pos++;
if (scanner.charCode() === LEFTPARENTHESIS) {
scanner.pos++;
return {
type: 'Function',
name: name
name
};
}
return maybeMultiplied(tokenizer, {
return maybeMultiplied(scanner, {
type: 'Keyword',
name: name
name
});
}
@@ -336,21 +322,27 @@ function regroupTerms(terms, combinators) {
function createGroup(terms, combinator) {
return {
type: 'Group',
terms: terms,
combinator: combinator,
terms,
combinator,
disallowEmpty: false,
explicit: false
};
}
combinators = Object.keys(combinators).sort(function(a, b) {
return COMBINATOR_PRECEDENCE[a] - COMBINATOR_PRECEDENCE[b];
});
let combinator;
combinators = Object.keys(combinators)
.sort((a, b) => COMBINATOR_PRECEDENCE[a] - COMBINATOR_PRECEDENCE[b]);
while (combinators.length > 0) {
var combinator = combinators.shift();
for (var i = 0, subgroupStart = 0; i < terms.length; i++) {
var term = terms[i];
combinator = combinators.shift();
let i = 0;
let subgroupStart = 0;
for (; i < terms.length; i++) {
const term = terms[i];
if (term.type === 'Combinator') {
if (term.value === combinator) {
if (subgroupStart === -1) {
@@ -384,20 +376,20 @@ function regroupTerms(terms, combinators) {
return combinator;
}
function readImplicitGroup(tokenizer) {
var terms = [];
var combinators = {};
var token;
var prevToken = null;
var prevTokenPos = tokenizer.pos;
function readImplicitGroup(scanner, stopCharCode) {
const combinators = Object.create(null);
const terms = [];
let token;
let prevToken = null;
let prevTokenPos = scanner.pos;
while (token = peek(tokenizer)) {
while (scanner.charCode() !== stopCharCode && (token = peek(scanner, stopCharCode))) {
if (token.type !== 'Spaces') {
if (token.type === 'Combinator') {
// check for combinator in group beginning and double combinator sequence
if (prevToken === null || prevToken.type === 'Combinator') {
tokenizer.pos = prevTokenPos;
tokenizer.error('Unexpected combinator');
scanner.pos = prevTokenPos;
scanner.error('Unexpected combinator');
}
combinators[token.value] = true;
@@ -411,48 +403,44 @@ function readImplicitGroup(tokenizer) {
terms.push(token);
prevToken = token;
prevTokenPos = tokenizer.pos;
prevTokenPos = scanner.pos;
}
}
// check for combinator in group ending
if (prevToken !== null && prevToken.type === 'Combinator') {
tokenizer.pos -= prevTokenPos;
tokenizer.error('Unexpected combinator');
scanner.pos -= prevTokenPos;
scanner.error('Unexpected combinator');
}
return {
type: 'Group',
terms: terms,
terms,
combinator: regroupTerms(terms, combinators) || ' ',
disallowEmpty: false,
explicit: false
};
}
function readGroup(tokenizer) {
var result;
function readGroup(scanner, stopCharCode) {
let result;
tokenizer.eat(LEFTSQUAREBRACKET);
result = readImplicitGroup(tokenizer);
tokenizer.eat(RIGHTSQUAREBRACKET);
scanner.eat(LEFTSQUAREBRACKET);
result = readImplicitGroup(scanner, stopCharCode);
scanner.eat(RIGHTSQUAREBRACKET);
result.explicit = true;
if (tokenizer.charCode() === EXCLAMATIONMARK) {
tokenizer.pos++;
if (scanner.charCode() === EXCLAMATIONMARK) {
scanner.pos++;
result.disallowEmpty = true;
}
return result;
}
function peek(tokenizer) {
var code = tokenizer.charCode();
if (code < 128 && NAME_CHAR[code] === 1) {
return readKeywordOrFunction(tokenizer);
}
function peek(scanner, stopCharCode) {
let code = scanner.charCode();
switch (code) {
case RIGHTSQUAREBRACKET:
@@ -460,26 +448,24 @@ function peek(tokenizer) {
break;
case LEFTSQUAREBRACKET:
return maybeMultiplied(tokenizer, readGroup(tokenizer));
return maybeMultiplied(scanner, readGroup(scanner, stopCharCode));
case LESSTHANSIGN:
return tokenizer.nextCharCode() === APOSTROPHE
? readProperty(tokenizer)
: readType(tokenizer);
return scanner.nextCharCode() === APOSTROPHE
? readProperty(scanner)
: readType(scanner);
case VERTICALLINE:
return {
type: 'Combinator',
value: tokenizer.substringToPos(
tokenizer.nextCharCode() === VERTICALLINE
? tokenizer.pos + 2
: tokenizer.pos + 1
value: scanner.substringToPos(
scanner.pos + (scanner.nextCharCode() === VERTICALLINE ? 2 : 1)
)
};
case AMPERSAND:
tokenizer.pos++;
tokenizer.eat(AMPERSAND);
scanner.pos++;
scanner.eat(AMPERSAND);
return {
type: 'Combinator',
@@ -487,15 +473,15 @@ function peek(tokenizer) {
};
case COMMA:
tokenizer.pos++;
scanner.pos++;
return {
type: 'Comma'
};
case APOSTROPHE:
return maybeMultiplied(tokenizer, {
return maybeMultiplied(scanner, {
type: 'String',
value: scanString(tokenizer)
value: scanner.scanString()
});
case SPACE:
@@ -505,21 +491,21 @@ function peek(tokenizer) {
case F:
return {
type: 'Spaces',
value: scanSpaces(tokenizer)
value: scanner.scanSpaces()
};
case COMMERCIALAT:
code = tokenizer.nextCharCode();
code = scanner.nextCharCode();
if (code < 128 && NAME_CHAR[code] === 1) {
tokenizer.pos++;
if (scanner.isNameCharCode(code)) {
scanner.pos++;
return {
type: 'AtKeyword',
name: scanWord(tokenizer)
name: scanner.scanWord()
};
}
return maybeToken(tokenizer);
return maybeToken(scanner);
case ASTERISK:
case PLUSSIGN:
@@ -532,37 +518,35 @@ function peek(tokenizer) {
case LEFTCURLYBRACKET:
// LEFTCURLYBRACKET is allowed since mdn/data uses it w/o quoting
// check next char isn't a number, because it's likely a disjoined multiplier
code = tokenizer.nextCharCode();
code = scanner.nextCharCode();
if (code < 48 || code > 57) {
return maybeToken(tokenizer);
return maybeToken(scanner);
}
break;
default:
return maybeToken(tokenizer);
if (scanner.isNameCharCode(code)) {
return readKeywordOrFunction(scanner);
}
return maybeToken(scanner);
}
}
function parse(source) {
var tokenizer = new Tokenizer(source);
var result = readImplicitGroup(tokenizer);
export function parse(source) {
const scanner = new Scanner(source);
const result = readImplicitGroup(scanner);
if (tokenizer.pos !== source.length) {
tokenizer.error('Unexpected input');
if (scanner.pos !== source.length) {
scanner.error('Unexpected input');
}
// reduce redundant groups with single group term
if (result.terms.length === 1 && result.terms[0].type === 'Group') {
result = result.terms[0];
return result.terms[0];
}
return result;
}
// warm up parse to elimitate code branches that never execute
// fix soft deoptimizations (insufficient type feedback)
parse('[a&&<b>#|<\'c\'>*||e() f{2} /,(% g#{1,2} h{2,})]!');
module.exports = parse;
};