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