Files
root d523f0f600 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>
2026-01-14 10:38:22 +00:00

341 lines
11 KiB
JavaScript

'use strict';
const List = require('../utils/List.cjs');
const SyntaxError = require('./SyntaxError.cjs');
const index = require('../tokenizer/index.cjs');
const sequence = require('./sequence.cjs');
const OffsetToLocation = require('../tokenizer/OffsetToLocation.cjs');
const TokenStream = require('../tokenizer/TokenStream.cjs');
const utils = require('../tokenizer/utils.cjs');
const types = require('../tokenizer/types.cjs');
const names = require('../tokenizer/names.cjs');
const NOOP = () => {};
const EXCLAMATIONMARK = 0x0021; // U+0021 EXCLAMATION MARK (!)
const NUMBERSIGN = 0x0023; // U+0023 NUMBER SIGN (#)
const SEMICOLON = 0x003B; // U+003B SEMICOLON (;)
const LEFTCURLYBRACKET = 0x007B; // U+007B LEFT CURLY BRACKET ({)
const NULL = 0;
function createParseContext(name) {
return function() {
return this[name]();
};
}
function fetchParseValues(dict) {
const result = Object.create(null);
for (const name of Object.keys(dict)) {
const item = dict[name];
const fn = item.parse || item;
if (fn) {
result[name] = fn;
}
}
return result;
}
function processConfig(config) {
const parseConfig = {
context: Object.create(null),
features: Object.assign(Object.create(null), config.features),
scope: Object.assign(Object.create(null), config.scope),
atrule: fetchParseValues(config.atrule),
pseudo: fetchParseValues(config.pseudo),
node: fetchParseValues(config.node)
};
for (const [name, context] of Object.entries(config.parseContext)) {
switch (typeof context) {
case 'function':
parseConfig.context[name] = context;
break;
case 'string':
parseConfig.context[name] = createParseContext(context);
break;
}
}
return {
config: parseConfig,
...parseConfig,
...parseConfig.node
};
}
function createParser(config) {
let source = '';
let filename = '<unknown>';
let needPositions = false;
let onParseError = NOOP;
let onParseErrorThrow = false;
const locationMap = new OffsetToLocation.OffsetToLocation();
const parser = Object.assign(new TokenStream.TokenStream(), processConfig(config || {}), {
parseAtrulePrelude: true,
parseRulePrelude: true,
parseValue: true,
parseCustomProperty: false,
readSequence: sequence.readSequence,
consumeUntilBalanceEnd: () => 0,
consumeUntilLeftCurlyBracket(code) {
return code === LEFTCURLYBRACKET ? 1 : 0;
},
consumeUntilLeftCurlyBracketOrSemicolon(code) {
return code === LEFTCURLYBRACKET || code === SEMICOLON ? 1 : 0;
},
consumeUntilExclamationMarkOrSemicolon(code) {
return code === EXCLAMATIONMARK || code === SEMICOLON ? 1 : 0;
},
consumeUntilSemicolonIncluded(code) {
return code === SEMICOLON ? 2 : 0;
},
createList() {
return new List.List();
},
createSingleNodeList(node) {
return new List.List().appendData(node);
},
getFirstListNode(list) {
return list && list.first;
},
getLastListNode(list) {
return list && list.last;
},
parseWithFallback(consumer, fallback) {
const startIndex = this.tokenIndex;
try {
return consumer.call(this);
} catch (e) {
if (onParseErrorThrow) {
throw e;
}
this.skip(startIndex - this.tokenIndex);
const fallbackNode = fallback.call(this);
onParseErrorThrow = true;
onParseError(e, fallbackNode);
onParseErrorThrow = false;
return fallbackNode;
}
},
lookupNonWSType(offset) {
let type;
do {
type = this.lookupType(offset++);
if (type !== types.WhiteSpace && type !== types.Comment) {
return type;
}
} while (type !== NULL);
return NULL;
},
charCodeAt(offset) {
return offset >= 0 && offset < source.length ? source.charCodeAt(offset) : 0;
},
substring(offsetStart, offsetEnd) {
return source.substring(offsetStart, offsetEnd);
},
substrToCursor(start) {
return this.source.substring(start, this.tokenStart);
},
cmpChar(offset, charCode) {
return utils.cmpChar(source, offset, charCode);
},
cmpStr(offsetStart, offsetEnd, str) {
return utils.cmpStr(source, offsetStart, offsetEnd, str);
},
consume(tokenType) {
const start = this.tokenStart;
this.eat(tokenType);
return this.substrToCursor(start);
},
consumeFunctionName() {
const name = source.substring(this.tokenStart, this.tokenEnd - 1);
this.eat(types.Function);
return name;
},
consumeNumber(type) {
const number = source.substring(this.tokenStart, utils.consumeNumber(source, this.tokenStart));
this.eat(type);
return number;
},
eat(tokenType) {
if (this.tokenType !== tokenType) {
const tokenName = names[tokenType].slice(0, -6).replace(/-/g, ' ').replace(/^./, m => m.toUpperCase());
let message = `${/[[\](){}]/.test(tokenName) ? `"${tokenName}"` : tokenName} is expected`;
let offset = this.tokenStart;
// tweak message and offset
switch (tokenType) {
case types.Ident:
// when identifier is expected but there is a function or url
if (this.tokenType === types.Function || this.tokenType === types.Url) {
offset = this.tokenEnd - 1;
message = 'Identifier is expected but function found';
} else {
message = 'Identifier is expected';
}
break;
case types.Hash:
if (this.isDelim(NUMBERSIGN)) {
this.next();
offset++;
message = 'Name is expected';
}
break;
case types.Percentage:
if (this.tokenType === types.Number) {
offset = this.tokenEnd;
message = 'Percent sign is expected';
}
break;
}
this.error(message, offset);
}
this.next();
},
eatIdent(name) {
if (this.tokenType !== types.Ident || this.lookupValue(0, name) === false) {
this.error(`Identifier "${name}" is expected`);
}
this.next();
},
eatDelim(code) {
if (!this.isDelim(code)) {
this.error(`Delim "${String.fromCharCode(code)}" is expected`);
}
this.next();
},
getLocation(start, end) {
if (needPositions) {
return locationMap.getLocationRange(
start,
end,
filename
);
}
return null;
},
getLocationFromList(list) {
if (needPositions) {
const head = this.getFirstListNode(list);
const tail = this.getLastListNode(list);
return locationMap.getLocationRange(
head !== null ? head.loc.start.offset - locationMap.startOffset : this.tokenStart,
tail !== null ? tail.loc.end.offset - locationMap.startOffset : this.tokenStart,
filename
);
}
return null;
},
error(message, offset) {
const location = typeof offset !== 'undefined' && offset < source.length
? locationMap.getLocation(offset)
: this.eof
? locationMap.getLocation(utils.findWhiteSpaceStart(source, source.length - 1))
: locationMap.getLocation(this.tokenStart);
throw new SyntaxError.SyntaxError(
message || 'Unexpected input',
source,
location.offset,
location.line,
location.column,
locationMap.startLine,
locationMap.startColumn
);
}
});
const parse = function(source_, options) {
source = source_;
options = options || {};
parser.setSource(source, index.tokenize);
locationMap.setSource(
source,
options.offset,
options.line,
options.column
);
filename = options.filename || '<unknown>';
needPositions = Boolean(options.positions);
onParseError = typeof options.onParseError === 'function' ? options.onParseError : NOOP;
onParseErrorThrow = false;
parser.parseAtrulePrelude = 'parseAtrulePrelude' in options ? Boolean(options.parseAtrulePrelude) : true;
parser.parseRulePrelude = 'parseRulePrelude' in options ? Boolean(options.parseRulePrelude) : true;
parser.parseValue = 'parseValue' in options ? Boolean(options.parseValue) : true;
parser.parseCustomProperty = 'parseCustomProperty' in options ? Boolean(options.parseCustomProperty) : false;
const { context = 'default', onComment } = options;
if (context in parser.context === false) {
throw new Error('Unknown context `' + context + '`');
}
if (typeof onComment === 'function') {
parser.forEachToken((type, start, end) => {
if (type === types.Comment) {
const loc = parser.getLocation(start, end);
const value = utils.cmpStr(source, end - 2, end, '*/')
? source.slice(start + 2, end - 2)
: source.slice(start + 2, end);
onComment(value, loc);
}
});
}
const ast = parser.context[context].call(parser, options);
if (!parser.eof) {
parser.error();
}
return ast;
};
return Object.assign(parse, {
SyntaxError: SyntaxError.SyntaxError,
config: parser.config
});
}
exports.createParser = createParser;