Skip to content

Commit

Permalink
feat(removeXlink): new plugin to map xlink:href to href
Browse files Browse the repository at this point in the history
Co-authored-by: Seth Falco <seth@falco.fun>
  • Loading branch information
TrySound and SethFalco committed Nov 18, 2023
1 parent 9a20ad2 commit e0a32d2
Show file tree
Hide file tree
Showing 14 changed files with 334 additions and 11 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Our documentation is maintained in [MDX](https://mdxjs.com/), which is Markdown

To preview local changes, follow the steps to run the website locally in [svg/svgo.dev](https://github.com/svg/svgo.dev).

New plugins, plugin parameters, and notable any features should be paired with documentation in the [`docs/`](./docs/) directory and pushed in the same pull request.
New plugins, plugin parameters, and notable features should be paired with documentation in the [`docs/`](./docs/) directory and included in the same pull request.

## Funding

Expand Down
12 changes: 6 additions & 6 deletions docs/03-plugins/convert-colors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@ svgo:
description: If to convert all instances of a color to <code>currentColor</code>. This means to inherit the active foreground color, for example in HTML5 this would be the <a href="https://developer.mozilla.org/docs/Web/CSS/color" target="_blank"><code>color</code></a> property in CSS.
default: false
names2hex:
description: If to convert color names to the hex equivilent.
description: If to convert color names to the hex equivalent.
default: true
rgb2hex:
description: If to convert RGB colors to the hex equivilent, does ignores RGBA.
description: If to convert RGB colors to the hex equivalent, does ignores RGBA.
default: true
shorthex:
description: If to convert 6 character hex colors to the 3 character equivilents where possible.
description: If to convert 6 character hex colors to the 3 character equivalent where possible.
default: true
shortname:
description: If to convert hex colors to the color name, if the color name is shorter then the hex equivilent.
description: If to convert hex colors to the color name, if the color name is shorter then the hex equivalent.
default: true
---

Converts color references to the shortest equivilent.
Converts color references to the shortest equivalent.

Colors can be represented in various notations, the following table showcases some equivilent colors:
Colors can be represented in various notations, the following table showcases some equivalent colors:

| Name | rgb() | #rrggbb | #rgb |
|---|---|---|---|
Expand Down
35 changes: 35 additions & 0 deletions docs/03-plugins/remove-xlink.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: Remove XLink
svgo:
pluginId: removeXlink
---

Removes XLink namespace prefixes and converts references to XLink attributes to the native SVG equivalent.

Performs the following operations:

* Converts `*:href` to [`href`](https://developer.mozilla.org/docs/Web/SVG/Attribute/href).
* Converts `*:show` to [`target`](https://developer.mozilla.org/docs/Web/SVG/Attribute/target).
* Converts `*:title` to [`<title>`](https://developer.mozilla.org/docs/Web/SVG/Element/title).
* Drops all other references to the XLink namespace.
* Removes XLink namespace declarations.

It's recommended to use this plugin if you intend to inline SVGs into an HTML document. 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
8 changes: 7 additions & 1 deletion docs/03-plugins/remove-xmlns.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ svgo:

Removes the `xmlns` attribute from the top-most `<svg>` element in the document.

This optimization is encouraged if you plan to use your SVGs inline an HTML document. The HTML syntax does not support explicit namespaces, so these are ignored by the browser anyway.
It's recommended to use this plugin if you intend to inline SVGs into an HTML document. HTML does not support explicit namespaces, so these are ignored by the browser anyway.

:::tip

This plugin pairs well with the [Remove XLink](/docs/plugins/remove-xlink/) plugin. Remove XLink drops XLink namespaces and migrates references to them to the modern equivalent, supported by SVG 2 and inline an HTML document. When using this, it's recommended to enable Remove XLink too.

:::

:::caution

Expand Down
1 change: 1 addition & 0 deletions lib/builtin.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ exports.builtin = [
require('../plugins/removeUselessDefs.js'),
require('../plugins/removeUselessStrokeAndFill.js'),
require('../plugins/removeViewBox.js'),
require('../plugins/removeXlink.js'),
require('../plugins/removeXMLNS.js'),
require('../plugins/removeXMLProcInst.js'),
require('../plugins/reusePaths.js'),
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"packageManager": "yarn@2.4.3",
"name": "svgo",
"version": "3.0.3",
"version": "3.0.4",
"description": "Nodejs-based tool for optimizing SVG vector graphics files",
"license": "MIT",
"keywords": [
Expand Down
1 change: 1 addition & 0 deletions plugins/plugins-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export type BuiltinsWithOptionalParams = DefaultPlugins & {
removeRasterImages: void;
removeScriptElement: void;
removeStyleElement: void;
removeXlink: void;
removeXMLNS: void;
reusePaths: void;
};
Expand Down
2 changes: 1 addition & 1 deletion plugins/removeEditorsNSData.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ exports.fn = (_root, params) => {
return {
element: {
enter: (node, parentNode) => {
// collect namespace aliases from svg element
// collect namespace prefixes from svg element
if (node.name === 'svg') {
for (const [name, value] of Object.entries(node.attributes)) {
if (name.startsWith('xmlns:') && namespaces.includes(value)) {
Expand Down
1 change: 0 additions & 1 deletion plugins/removeXMLNS.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ exports.fn = () => {
enter: (node) => {
if (node.name === 'svg') {
delete node.attributes.xmlns;
delete node.attributes['xmlns:xlink'];
}
},
},
Expand Down
187 changes: 187 additions & 0 deletions plugins/removeXlink.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
'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',
};

/**
* @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('../lib/types').Plugin<void>}
* @see https://developer.mozilla.org/docs/Web/SVG/Attribute/xlink:href
*/
exports.fn = () => {
/**
* 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 = [];

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 hrefAttrs = findPrefixedAttrs(node, xlinkPrefixes, 'href');
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];
}

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.a.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];
}
},
exit: (node) => {
for (const [key, value] of Object.entries(node.attributes)) {
const [prefix, attr] = key.split(':', 2);

if (
xlinkPrefixes.includes(prefix) &&
!overriddenPrefixes.includes(prefix)
) {
delete node.attributes[key];
continue;
}

if (key.startsWith('xmlns:')) {
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);
}
}
}
},
},
};
};
25 changes: 25 additions & 0 deletions test/plugins/removeXlink.01.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions test/plugins/removeXlink.02.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions test/plugins/removeXlink.03.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit e0a32d2

Please sign in to comment.