227 lines
6.0 KiB
JavaScript
227 lines
6.0 KiB
JavaScript
'use strict';
|
|
|
|
const { elems } = require('./_collections');
|
|
|
|
/**
|
|
* @typedef {import('../lib/types').XastElement} XastElement
|
|
*/
|
|
|
|
exports.name = 'removeXlink';
|
|
exports.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 {XastElement} node
|
|
* @param {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.
|
|
*
|
|
* The XLink namespace is deprecated in SVG 2.
|
|
*
|
|
* @type {import('./plugins-types').Plugin<'removeXlink'>}
|
|
* @see https://developer.mozilla.org/docs/Web/SVG/Attribute/xlink:href
|
|
*/
|
|
exports.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 so 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 {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);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
},
|
|
};
|
|
};
|