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>
228 lines
6.3 KiB
JavaScript
228 lines
6.3 KiB
JavaScript
import { elems } from './_collections.js';
|
|
|
|
/**
|
|
* @typedef RemoveXlinkParams
|
|
* @property {boolean=} includeLegacy
|
|
* By default this plugin ignores legacy elements that were deprecated or
|
|
* removed in SVG 2. Set to true to force performing operations on those too.
|
|
*/
|
|
|
|
export const name = 'removeXlink';
|
|
export const description =
|
|
'remove xlink namespace and replaces attributes with the SVG 2 equivalent where applicable';
|
|
|
|
/** URI indicating the Xlink namespace. */
|
|
const XLINK_NAMESPACE = 'http://www.w3.org/1999/xlink';
|
|
|
|
/**
|
|
* Map of `xlink:show` values to the SVG 2 `target` attribute values.
|
|
*
|
|
* @type {Record<string, string>}
|
|
* @see https://developer.mozilla.org/docs/Web/SVG/Attribute/xlink:show#usage_notes
|
|
*/
|
|
const SHOW_TO_TARGET = {
|
|
new: '_blank',
|
|
replace: '_self',
|
|
};
|
|
|
|
/**
|
|
* Elements that use xlink:href, but were deprecated in SVG 2 and therefore
|
|
* don't support the SVG 2 href attribute.
|
|
*
|
|
* @type {Set<string>}
|
|
* @see https://developer.mozilla.org/docs/Web/SVG/Attribute/xlink:href
|
|
* @see https://developer.mozilla.org/docs/Web/SVG/Attribute/href
|
|
*/
|
|
const LEGACY_ELEMENTS = new Set([
|
|
'cursor',
|
|
'filter',
|
|
'font-face-uri',
|
|
'glyphRef',
|
|
'tref',
|
|
]);
|
|
|
|
/**
|
|
* @param {import('../lib/types.js').XastElement} node
|
|
* @param {ReadonlyArray<string>} prefixes
|
|
* @param {string} attr
|
|
* @returns {string[]}
|
|
*/
|
|
const findPrefixedAttrs = (node, prefixes, attr) => {
|
|
return prefixes
|
|
.map((prefix) => `${prefix}:${attr}`)
|
|
.filter((attr) => node.attributes[attr] != null);
|
|
};
|
|
|
|
/**
|
|
* Removes XLink namespace prefixes and converts references to XLink attributes
|
|
* to the native SVG equivalent.
|
|
*
|
|
* XLink namespace is deprecated in SVG 2.
|
|
*
|
|
* @type {import('../lib/types.js').Plugin<RemoveXlinkParams>}
|
|
* @see https://developer.mozilla.org/docs/Web/SVG/Attribute/xlink:href
|
|
*/
|
|
export const fn = (_, params) => {
|
|
const { includeLegacy } = params;
|
|
|
|
/**
|
|
* XLink namespace prefixes that are currently in the stack.
|
|
*
|
|
* @type {string[]}
|
|
*/
|
|
const xlinkPrefixes = [];
|
|
|
|
/**
|
|
* Namespace prefixes that exist in {@link xlinkPrefixes} but were overridden
|
|
* in a child element to point to another namespace, and is not treated as an
|
|
* XLink attribute.
|
|
*
|
|
* @type {string[]}
|
|
*/
|
|
const overriddenPrefixes = [];
|
|
|
|
/**
|
|
* Namespace prefixes that were used in one of the {@link LEGACY_ELEMENTS}.
|
|
*
|
|
* @type {string[]}
|
|
*/
|
|
const usedInLegacyElement = [];
|
|
|
|
return {
|
|
element: {
|
|
enter: (node) => {
|
|
for (const [key, value] of Object.entries(node.attributes)) {
|
|
if (key.startsWith('xmlns:')) {
|
|
const prefix = key.split(':', 2)[1];
|
|
|
|
if (value === XLINK_NAMESPACE) {
|
|
xlinkPrefixes.push(prefix);
|
|
continue;
|
|
}
|
|
|
|
if (xlinkPrefixes.includes(prefix)) {
|
|
overriddenPrefixes.push(prefix);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
overriddenPrefixes.some((prefix) => xlinkPrefixes.includes(prefix))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const showAttrs = findPrefixedAttrs(node, xlinkPrefixes, 'show');
|
|
let showHandled = node.attributes.target != null;
|
|
for (let i = showAttrs.length - 1; i >= 0; i--) {
|
|
const attr = showAttrs[i];
|
|
const value = node.attributes[attr];
|
|
const mapping = SHOW_TO_TARGET[value];
|
|
|
|
if (showHandled || mapping == null) {
|
|
delete node.attributes[attr];
|
|
continue;
|
|
}
|
|
|
|
if (mapping !== elems[node.name]?.defaults?.target) {
|
|
node.attributes.target = mapping;
|
|
}
|
|
|
|
delete node.attributes[attr];
|
|
showHandled = true;
|
|
}
|
|
|
|
const titleAttrs = findPrefixedAttrs(node, xlinkPrefixes, 'title');
|
|
for (let i = titleAttrs.length - 1; i >= 0; i--) {
|
|
const attr = titleAttrs[i];
|
|
const value = node.attributes[attr];
|
|
const hasTitle = node.children.filter(
|
|
(child) => child.type === 'element' && child.name === 'title',
|
|
);
|
|
|
|
if (hasTitle.length > 0) {
|
|
delete node.attributes[attr];
|
|
continue;
|
|
}
|
|
|
|
/** @type {import('../lib/types.js').XastElement} */
|
|
const titleTag = {
|
|
type: 'element',
|
|
name: 'title',
|
|
attributes: {},
|
|
children: [
|
|
{
|
|
type: 'text',
|
|
value,
|
|
},
|
|
],
|
|
};
|
|
|
|
Object.defineProperty(titleTag, 'parentNode', {
|
|
writable: true,
|
|
value: node,
|
|
});
|
|
|
|
node.children.unshift(titleTag);
|
|
delete node.attributes[attr];
|
|
}
|
|
|
|
const hrefAttrs = findPrefixedAttrs(node, xlinkPrefixes, 'href');
|
|
|
|
if (
|
|
hrefAttrs.length > 0 &&
|
|
LEGACY_ELEMENTS.has(node.name) &&
|
|
!includeLegacy
|
|
) {
|
|
hrefAttrs
|
|
.map((attr) => attr.split(':', 1)[0])
|
|
.forEach((prefix) => usedInLegacyElement.push(prefix));
|
|
return;
|
|
}
|
|
|
|
for (let i = hrefAttrs.length - 1; i >= 0; i--) {
|
|
const attr = hrefAttrs[i];
|
|
const value = node.attributes[attr];
|
|
|
|
if (node.attributes.href != null) {
|
|
delete node.attributes[attr];
|
|
continue;
|
|
}
|
|
|
|
node.attributes.href = value;
|
|
delete node.attributes[attr];
|
|
}
|
|
},
|
|
exit: (node) => {
|
|
for (const [key, value] of Object.entries(node.attributes)) {
|
|
const [prefix, attr] = key.split(':', 2);
|
|
|
|
if (
|
|
xlinkPrefixes.includes(prefix) &&
|
|
!overriddenPrefixes.includes(prefix) &&
|
|
!usedInLegacyElement.includes(prefix) &&
|
|
!includeLegacy
|
|
) {
|
|
delete node.attributes[key];
|
|
continue;
|
|
}
|
|
|
|
if (key.startsWith('xmlns:') && !usedInLegacyElement.includes(attr)) {
|
|
if (value === XLINK_NAMESPACE) {
|
|
const index = xlinkPrefixes.indexOf(attr);
|
|
xlinkPrefixes.splice(index, 1);
|
|
delete node.attributes[key];
|
|
continue;
|
|
}
|
|
|
|
if (overriddenPrefixes.includes(prefix)) {
|
|
const index = overriddenPrefixes.indexOf(attr);
|
|
overriddenPrefixes.splice(index, 1);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
},
|
|
};
|
|
};
|