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);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
},
|
|
};
|
|
};
|