-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(removeXlink): new plugin to map xlink:href to href
Co-authored-by: Seth Falco <seth@falco.fun>
- Loading branch information
Showing
14 changed files
with
394 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
--- | ||
title: Remove XLink | ||
svgo: | ||
pluginId: removeXlink | ||
parameters: | ||
includeLegacy: | ||
description: If to update references to XLink in elements that don't support the SVG 2 href attribute, like <code><filter></code> and <code><tref></code>. | ||
default: false | ||
--- | ||
|
||
Removes XLink namespace prefixes and converts references to XLink attributes to the native SVG equivalent by performing the following operations: | ||
|
||
* Convert `*:href` to [`href`](https://developer.mozilla.org/docs/Web/SVG/Attribute/href). | ||
* Convert `*:show` to [`target`](https://developer.mozilla.org/docs/Web/SVG/Attribute/target). | ||
* Convert `*:title` to [`<title>`](https://developer.mozilla.org/docs/Web/SVG/Element/title). | ||
* Drop all other references to the XLink namespace. | ||
* Remove XLink namespace declarations. | ||
|
||
In most cases this will remove all references to XLink, but if legacy elements that are deprecated or removed in SVG 2 are found, the references are preserved as those elements do not support the SVG 2 `href` attribute. You can set `includeLegacy` to `true` to apply the plugin in this case too. | ||
|
||
The following support `xlink:href` but not the SVG 2 `href` attribute: | ||
|
||
* [`<cursor>`](https://developer.mozilla.org/docs/Web/SVG/Element/cursor) | ||
* [`<filter>`](https://developer.mozilla.org/docs/Web/SVG/Element/filter) | ||
* [`<font-face-uri>`](https://developer.mozilla.org/docs/Web/SVG/Element/font-face-uri) | ||
* [`<glyphRef>`](https://developer.mozilla.org/docs/Web/SVG/Element/glyphRef) | ||
* [`<tref>`](https://developer.mozilla.org/docs/Web/SVG/Element/tref) | ||
|
||
It's recommended to use this plugin if you intend to inline SVGs into an HTML document, the `includeLegacy` can be safely used in this case too. HTML does not support explicit namespaces, so namespace prefixes are ignored by the browser anyway. | ||
|
||
:::danger | ||
|
||
This replaces XLink with features that are only supported in the SVGO 2 spec, and so breaks compatibility with the SVG 1.1. | ||
|
||
::: | ||
|
||
## Usage | ||
|
||
<PluginUsage/> | ||
|
||
## Demo | ||
|
||
<PluginDemo/> | ||
|
||
## Implementation | ||
|
||
* https://github.com/svg/svgo/blob/main/plugins/removeXlink.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
'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 {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 = [ | ||
'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 overriden | ||
* 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.includes(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); | ||
} | ||
} | ||
} | ||
}, | ||
}, | ||
}; | ||
}; |
Oops, something went wrong.