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>
146 lines
4.2 KiB
JavaScript
Executable File
146 lines
4.2 KiB
JavaScript
Executable File
import { attrsGroups } from './_collections.js';
|
|
|
|
/**
|
|
* @typedef ConvertStyleToAttrsParams
|
|
* @property {boolean=} keepImportant
|
|
*/
|
|
|
|
export const name = 'convertStyleToAttrs';
|
|
export const description = 'converts style to attributes';
|
|
|
|
/**
|
|
* @param {...string} args
|
|
* @returns {string}
|
|
*/
|
|
const g = (...args) => {
|
|
return '(?:' + args.join('|') + ')';
|
|
};
|
|
|
|
const stylingProps = attrsGroups.presentation;
|
|
const rEscape = '\\\\(?:[0-9a-f]{1,6}\\s?|\\r\\n|.)'; // Like \" or \2051. Code points consume one space.
|
|
|
|
/** Pattern to match attribute name like: 'fill' */
|
|
const rAttr = '\\s*(' + g('[^:;\\\\]', rEscape) + '*?)\\s*';
|
|
|
|
/** Pattern to match string in single quotes like: 'foo' */
|
|
const rSingleQuotes = "'(?:[^'\\n\\r\\\\]|" + rEscape + ")*?(?:'|$)";
|
|
|
|
/** Pattern to match string in double quotes like: "foo" */
|
|
const rQuotes = '"(?:[^"\\n\\r\\\\]|' + rEscape + ')*?(?:"|$)';
|
|
|
|
const rQuotedString = new RegExp('^' + g(rSingleQuotes, rQuotes) + '$');
|
|
// Parentheses, E.g.: url(data:image/png;base64,iVBO...).
|
|
// ':' and ';' inside of it should be treated as is. (Just like in strings.)
|
|
const rParenthesis =
|
|
'\\(' + g('[^\'"()\\\\]+', rEscape, rSingleQuotes, rQuotes) + '*?' + '\\)';
|
|
// The value. It can have strings and parentheses (see above). Fallbacks to anything in case of unexpected input.
|
|
const rValue =
|
|
'\\s*(' +
|
|
g(
|
|
'[^!\'"();\\\\]+?',
|
|
rEscape,
|
|
rSingleQuotes,
|
|
rQuotes,
|
|
rParenthesis,
|
|
'[^;]*?',
|
|
) +
|
|
'*?' +
|
|
')';
|
|
// End of declaration. Spaces outside of capturing groups help to do natural trimming.
|
|
const rDeclEnd = '\\s*(?:;\\s*|$)';
|
|
// Important rule
|
|
const rImportant = '(\\s*!important(?![-(\\w]))?';
|
|
// Final RegExp to parse CSS declarations.
|
|
const regDeclarationBlock = new RegExp(
|
|
rAttr + ':' + rValue + rImportant + rDeclEnd,
|
|
'ig',
|
|
);
|
|
// Comments expression. Honors escape sequences and strings.
|
|
const regStripComments = new RegExp(
|
|
g(rEscape, rSingleQuotes, rQuotes, '/\\*[^]*?\\*/'),
|
|
'ig',
|
|
);
|
|
|
|
/**
|
|
* Convert style in attributes. Cleanups comments and illegal declarations (without colon) as a side effect.
|
|
*
|
|
* @example
|
|
* <g style="fill:#000; color: #fff;">
|
|
* ⬇
|
|
* <g fill="#000" color="#fff">
|
|
*
|
|
* @example
|
|
* <g style="fill:#000; color: #fff; -webkit-blah: blah">
|
|
* ⬇
|
|
* <g fill="#000" color="#fff" style="-webkit-blah: blah">
|
|
*
|
|
* @author Kir Belevich
|
|
*
|
|
* @type {import('../lib/types.js').Plugin<ConvertStyleToAttrsParams>}
|
|
*/
|
|
export const fn = (_root, params) => {
|
|
const { keepImportant = false } = params;
|
|
return {
|
|
element: {
|
|
enter: (node) => {
|
|
if (node.attributes.style != null) {
|
|
// ['opacity: 1', 'color: #000']
|
|
let styles = [];
|
|
/** @type {Record<string, string>} */
|
|
const newAttributes = {};
|
|
|
|
// Strip CSS comments preserving escape sequences and strings.
|
|
const styleValue = node.attributes.style.replace(
|
|
regStripComments,
|
|
(match) => {
|
|
return match[0] == '/'
|
|
? ''
|
|
: match[0] == '\\' && /[-g-z]/i.test(match[1])
|
|
? match[1]
|
|
: match;
|
|
},
|
|
);
|
|
|
|
regDeclarationBlock.lastIndex = 0;
|
|
for (var rule; (rule = regDeclarationBlock.exec(styleValue)); ) {
|
|
if (!keepImportant || !rule[3]) {
|
|
styles.push([rule[1], rule[2]]);
|
|
}
|
|
}
|
|
|
|
if (styles.length) {
|
|
styles = styles.filter(function (style) {
|
|
if (style[0]) {
|
|
const prop = style[0].toLowerCase();
|
|
let val = style[1];
|
|
|
|
if (rQuotedString.test(val)) {
|
|
val = val.slice(1, -1);
|
|
}
|
|
|
|
if (stylingProps.has(prop)) {
|
|
newAttributes[prop] = val;
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
Object.assign(node.attributes, newAttributes);
|
|
|
|
if (styles.length) {
|
|
node.attributes.style = styles
|
|
.map((declaration) => declaration.join(':'))
|
|
.join(';');
|
|
} else {
|
|
delete node.attributes.style;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
},
|
|
};
|
|
};
|