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,25 +1,26 @@
'use strict';
const csstree = require('css-tree');
const { referencesProps } = require('./_collections.js');
import * as csstree from 'css-tree';
import { referencesProps } from './_collections.js';
/**
* @typedef {import('../lib/types').XastElement} XastElement
* @typedef {import('../lib/types').PluginInfo} PluginInfo
* @typedef PrefixIdsParams
* @property {boolean | string | ((node: import('../lib/types.js').XastElement, info: import('../lib/types.js').PluginInfo) => string)=} prefix
* @property {string=} delim
* @property {boolean=} prefixIds
* @property {boolean=} prefixClassNames
*/
exports.type = 'visitor';
exports.name = 'prefixIds';
exports.active = false;
exports.description = 'prefix IDs';
export const name = 'prefixIds';
export const description = 'prefix IDs';
/**
* extract basename from path
* @type {(path: string) => string}
* Extract basename from path.
*
* @param {string} path
* @returns {string}
*/
const getBasename = (path) => {
// extract everything after latest slash or backslash
const matched = path.match(/[/\\]?([^/\\]+)$/);
const matched = /[/\\]?([^/\\]+)$/.exec(path);
if (matched) {
return matched[1];
}
@@ -27,15 +28,18 @@ const getBasename = (path) => {
};
/**
* escapes a string for being used as ID
* @type {(string: string) => string}
* Escapes a string for being used as ID.
*
* @param {string} str
* @returns {string}
*/
const escapeIdentifierName = (str) => {
return str.replace(/[. ]/g, '_');
};
/**
* @type {(string: string) => string}
* @param {string} string
* @returns {string}
*/
const unquote = (string) => {
if (
@@ -48,59 +52,100 @@ const unquote = (string) => {
};
/**
* prefix an ID
* @type {(prefix: string, name: string) => string}
* Prefix the given string, unless it already starts with the generated prefix.
*
* @param {(id: string) => string} prefixGenerator Function to generate a prefix.
* @param {string} body An arbitrary string.
* @returns {string} The given string with a prefix prepended to it.
*/
const prefixId = (prefix, value) => {
if (value.startsWith(prefix)) {
return value;
const prefixId = (prefixGenerator, body) => {
const prefix = prefixGenerator(body);
if (body.startsWith(prefix)) {
return body;
}
return prefix + value;
return prefix + body;
};
/**
* prefix an #ID
* @type {(prefix: string, name: string) => string | null}
* Insert the prefix in a reference string. A reference string is already
* prefixed with #, so the prefix is inserted after the first character.
*
* @param {(id: string) => string} prefixGenerator Function to generate a prefix.
* @param {string} reference An arbitrary string, should start with "#".
* @returns {?string} The given string with a prefix inserted, or null if the string did not start with "#".
*/
const prefixReference = (prefix, value) => {
if (value.startsWith('#')) {
return '#' + prefixId(prefix, value.slice(1));
const prefixReference = (prefixGenerator, reference) => {
if (reference.startsWith('#')) {
return '#' + prefixId(prefixGenerator, reference.slice(1));
}
return null;
};
/**
* Generates a prefix for the given string.
*
* @param {string} body An arbitrary string.
* @param {import('../lib/types.js').XastElement} node XML node that the identifier belongs to.
* @param {import('../lib/types.js').PluginInfo} info
* @param {((node: import('../lib/types.js').XastElement, info: import('../lib/types.js').PluginInfo) => string) | string | boolean | undefined} prefixGenerator Some way of obtaining a prefix.
* @param {string} delim Content to insert between the prefix and original value.
* @param {Map<string, string>} history Map of previously generated prefixes to IDs.
* @returns {string} A generated prefix.
*/
const generatePrefix = (body, node, info, prefixGenerator, delim, history) => {
if (typeof prefixGenerator === 'function') {
let prefix = history.get(body);
if (prefix != null) {
return prefix;
}
prefix = prefixGenerator(node, info) + delim;
history.set(body, prefix);
return prefix;
}
if (typeof prefixGenerator === 'string') {
return prefixGenerator + delim;
}
if (prefixGenerator === false) {
return '';
}
if (info.path != null && info.path.length > 0) {
return escapeIdentifierName(getBasename(info.path)) + delim;
}
return 'prefix' + delim;
};
/**
* Prefixes identifiers
*
* @author strarsis <strarsis@gmail.com>
*
* @type {import('../lib/types').Plugin<{
* prefix?: boolean | string | ((node: XastElement, info: PluginInfo) => string),
* delim?: string,
* prefixIds?: boolean,
* prefixClassNames?: boolean,
* }>}
* @type {import('../lib/types.js').Plugin<PrefixIdsParams>}
*/
exports.fn = (_root, params, info) => {
const { delim = '__', prefixIds = true, prefixClassNames = true } = params;
export const fn = (_root, params, info) => {
const {
delim = '__',
prefix,
prefixIds = true,
prefixClassNames = true,
} = params;
/** @type {Map<string, string>} */
const prefixMap = new Map();
return {
element: {
enter: (node) => {
/**
* prefix, from file name or option
* @type {string}
* @param {string} id A node identifier or class.
* @returns {string} Given string with a prefix inserted, or null if the string did not start with "#".
*/
let prefix = 'prefix' + delim;
if (typeof params.prefix === 'function') {
prefix = params.prefix(node, info) + delim;
} else if (typeof params.prefix === 'string') {
prefix = params.prefix + delim;
} else if (params.prefix === false) {
prefix = '';
} else if (info.path != null && info.path.length > 0) {
prefix = escapeIdentifierName(getBasename(info.path)) + delim;
}
const prefixGenerator = (id) =>
generatePrefix(id, node, info, prefix, delim, prefixMap);
// prefix id/class selectors and url() references in styles
if (node.name === 'style') {
@@ -109,60 +154,44 @@ exports.fn = (_root, params, info) => {
return;
}
// parse styles
let cssText = '';
if (
node.children[0].type === 'text' ||
node.children[0].type === 'cdata'
) {
cssText = node.children[0].value;
}
/**
* @type {null | csstree.CssNode}
*/
let cssAst = null;
try {
cssAst = csstree.parse(cssText, {
parseValue: true,
parseCustomProperty: false,
});
} catch {
return;
}
for (const child of node.children) {
if (child.type !== 'text' && child.type !== 'cdata') {
continue;
}
csstree.walk(cssAst, (node) => {
// #ID, .class selectors
if (
(prefixIds && node.type === 'IdSelector') ||
(prefixClassNames && node.type === 'ClassSelector')
) {
node.name = prefixId(prefix, node.name);
const cssText = child.value;
/** @type {?csstree.CssNode} */
let cssAst;
try {
cssAst = csstree.parse(cssText, {
parseValue: true,
parseCustomProperty: false,
});
} catch {
return;
}
// url(...) references
if (
node.type === 'Url' &&
node.value.value &&
node.value.value.length > 0
) {
const prefixed = prefixReference(
prefix,
unquote(node.value.value)
);
if (prefixed != null) {
node.value.value = prefixed;
}
}
});
// update styles
if (
node.children[0].type === 'text' ||
node.children[0].type === 'cdata'
) {
node.children[0].value = csstree.generate(cssAst);
csstree.walk(cssAst, (node) => {
if (
(prefixIds && node.type === 'IdSelector') ||
(prefixClassNames && node.type === 'ClassSelector')
) {
node.name = prefixId(prefixGenerator, node.name);
return;
}
if (node.type === 'Url' && node.value.length > 0) {
const prefixed = prefixReference(
prefixGenerator,
unquote(node.value),
);
if (prefixed != null) {
node.value = prefixed;
}
}
});
child.value = csstree.generate(cssAst);
}
return;
}
// prefix an ID attribute value
@@ -171,7 +200,7 @@ exports.fn = (_root, params, info) => {
node.attributes.id != null &&
node.attributes.id.length !== 0
) {
node.attributes.id = prefixId(prefix, node.attributes.id);
node.attributes.id = prefixId(prefixGenerator, node.attributes.id);
}
// prefix a class attribute value
@@ -182,39 +211,42 @@ exports.fn = (_root, params, info) => {
) {
node.attributes.class = node.attributes.class
.split(/\s+/)
.map((name) => prefixId(prefix, name))
.map((name) => prefixId(prefixGenerator, name))
.join(' ');
}
// prefix a href attribute value
// prefix an href attribute value
// xlink:href is deprecated, must be still supported
for (const name of ['href', 'xlink:href']) {
if (
node.attributes[name] != null &&
node.attributes[name].length !== 0
) {
const prefixed = prefixReference(prefix, node.attributes[name]);
const prefixed = prefixReference(
prefixGenerator,
node.attributes[name],
);
if (prefixed != null) {
node.attributes[name] = prefixed;
}
}
}
// prefix an URL attribute value
// prefix a URL attribute value
for (const name of referencesProps) {
if (
node.attributes[name] != null &&
node.attributes[name].length !== 0
) {
node.attributes[name] = node.attributes[name].replace(
/url\((.*?)\)/gi,
(match, url) => {
const prefixed = prefixReference(prefix, url);
/\burl\((["'])?(#.+?)\1\)/gi,
(match, _, url) => {
const prefixed = prefixReference(prefixGenerator, url);
if (prefixed == null) {
return match;
}
return `url(${prefixed})`;
}
},
);
}
}
@@ -228,7 +260,7 @@ exports.fn = (_root, params, info) => {
const parts = node.attributes[name].split(/\s*;\s+/).map((val) => {
if (val.endsWith('.end') || val.endsWith('.start')) {
const [id, postfix] = val.split('.');
return `${prefixId(prefix, id)}.${postfix}`;
return `${prefixId(prefixGenerator, id)}.${postfix}`;
}
return val;
});