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>
274 lines
7.9 KiB
JavaScript
Executable File
274 lines
7.9 KiB
JavaScript
Executable File
import * as csstree from 'css-tree';
|
|
import { referencesProps } from './_collections.js';
|
|
|
|
/**
|
|
* @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
|
|
*/
|
|
|
|
export const name = 'prefixIds';
|
|
export const description = 'prefix IDs';
|
|
|
|
/**
|
|
* Extract basename from path.
|
|
*
|
|
* @param {string} path
|
|
* @returns {string}
|
|
*/
|
|
const getBasename = (path) => {
|
|
// extract everything after latest slash or backslash
|
|
const matched = /[/\\]?([^/\\]+)$/.exec(path);
|
|
if (matched) {
|
|
return matched[1];
|
|
}
|
|
return '';
|
|
};
|
|
|
|
/**
|
|
* Escapes a string for being used as ID.
|
|
*
|
|
* @param {string} str
|
|
* @returns {string}
|
|
*/
|
|
const escapeIdentifierName = (str) => {
|
|
return str.replace(/[. ]/g, '_');
|
|
};
|
|
|
|
/**
|
|
* @param {string} string
|
|
* @returns {string}
|
|
*/
|
|
const unquote = (string) => {
|
|
if (
|
|
(string.startsWith('"') && string.endsWith('"')) ||
|
|
(string.startsWith("'") && string.endsWith("'"))
|
|
) {
|
|
return string.slice(1, -1);
|
|
}
|
|
return 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 = (prefixGenerator, body) => {
|
|
const prefix = prefixGenerator(body);
|
|
if (body.startsWith(prefix)) {
|
|
return body;
|
|
}
|
|
return prefix + body;
|
|
};
|
|
|
|
/**
|
|
* 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 = (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.js').Plugin<PrefixIdsParams>}
|
|
*/
|
|
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) => {
|
|
/**
|
|
* @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 "#".
|
|
*/
|
|
const prefixGenerator = (id) =>
|
|
generatePrefix(id, node, info, prefix, delim, prefixMap);
|
|
|
|
// prefix id/class selectors and url() references in styles
|
|
if (node.name === 'style') {
|
|
// skip empty <style/> elements
|
|
if (node.children.length === 0) {
|
|
return;
|
|
}
|
|
|
|
for (const child of node.children) {
|
|
if (child.type !== 'text' && child.type !== 'cdata') {
|
|
continue;
|
|
}
|
|
|
|
const cssText = child.value;
|
|
/** @type {?csstree.CssNode} */
|
|
let cssAst;
|
|
try {
|
|
cssAst = csstree.parse(cssText, {
|
|
parseValue: true,
|
|
parseCustomProperty: false,
|
|
});
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// prefix an ID attribute value
|
|
if (
|
|
prefixIds &&
|
|
node.attributes.id != null &&
|
|
node.attributes.id.length !== 0
|
|
) {
|
|
node.attributes.id = prefixId(prefixGenerator, node.attributes.id);
|
|
}
|
|
|
|
// prefix a class attribute value
|
|
if (
|
|
prefixClassNames &&
|
|
node.attributes.class != null &&
|
|
node.attributes.class.length !== 0
|
|
) {
|
|
node.attributes.class = node.attributes.class
|
|
.split(/\s+/)
|
|
.map((name) => prefixId(prefixGenerator, name))
|
|
.join(' ');
|
|
}
|
|
|
|
// 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(
|
|
prefixGenerator,
|
|
node.attributes[name],
|
|
);
|
|
if (prefixed != null) {
|
|
node.attributes[name] = prefixed;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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(
|
|
/\burl\((["'])?(#.+?)\1\)/gi,
|
|
(match, _, url) => {
|
|
const prefixed = prefixReference(prefixGenerator, url);
|
|
if (prefixed == null) {
|
|
return match;
|
|
}
|
|
return `url(${prefixed})`;
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
// prefix begin/end attribute value
|
|
for (const name of ['begin', 'end']) {
|
|
if (
|
|
node.attributes[name] != null &&
|
|
node.attributes[name].length !== 0
|
|
) {
|
|
const parts = node.attributes[name].split(/\s*;\s+/).map((val) => {
|
|
if (val.endsWith('.end') || val.endsWith('.start')) {
|
|
const [id, postfix] = val.split('.');
|
|
return `${prefixId(prefixGenerator, id)}.${postfix}`;
|
|
}
|
|
return val;
|
|
});
|
|
node.attributes[name] = parts.join('; ');
|
|
}
|
|
}
|
|
},
|
|
},
|
|
};
|
|
};
|