From 9b8f13e91136bcab74843d3b8c8066a37ef31bb7 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sun, 15 Aug 2021 13:52:41 +0300 Subject: [PATCH] Add plugin types (#1527) Covered following plugins - addAttributesToSVGElement.js - addClassesToSVGElement.js - cleanupAttrs.js - convertEllipseToCircle.js - removeAttributesBySelector.js - removeAttrs.js - removeComments.js - removeDesc.js - removeDoctype.js - removeElementsByAttr.js - removeEmptyText.js - removeMetadata.js - removeRasterImages.js - removeScriptElement.js - removeStyleElement.js - removeTitle.js - removeXMLProcInst.js --- lib/svgo/css-select-adapter.d.ts | 2 + lib/svgo/css-select-adapter.js | 4 -- lib/types.ts | 74 +++++++++++++++++++++++++++ lib/xast.js | 31 ++++++++++- plugins/addAttributesToSVGElement.js | 9 +++- plugins/addClassesToSVGElement.js | 8 ++- plugins/cleanupAttrs.js | 6 +++ plugins/convertEllipseToCircle.js | 6 ++- plugins/removeAttributesBySelector.js | 16 +++--- plugins/removeAttrs.js | 12 +++-- plugins/removeComments.js | 2 + plugins/removeDesc.js | 2 + plugins/removeDoctype.js | 2 + plugins/removeElementsByAttr.js | 13 ++--- plugins/removeEmptyText.js | 6 +++ plugins/removeMetadata.js | 2 + plugins/removeRasterImages.js | 2 + plugins/removeScriptElement.js | 3 +- plugins/removeStyleElement.js | 2 + plugins/removeTitle.js | 2 + plugins/removeXMLProcInst.js | 2 + tsconfig.json | 32 +++--------- 22 files changed, 188 insertions(+), 50 deletions(-) create mode 100644 lib/svgo/css-select-adapter.d.ts create mode 100644 lib/types.ts diff --git a/lib/svgo/css-select-adapter.d.ts b/lib/svgo/css-select-adapter.d.ts new file mode 100644 index 00000000..a11ba35f --- /dev/null +++ b/lib/svgo/css-select-adapter.d.ts @@ -0,0 +1,2 @@ +declare let obj: any; +export = obj; diff --git a/lib/svgo/css-select-adapter.js b/lib/svgo/css-select-adapter.js index 79bf24f8..e9f1024a 100644 --- a/lib/svgo/css-select-adapter.js +++ b/lib/svgo/css-select-adapter.js @@ -1,9 +1,5 @@ 'use strict'; -/** - * @param {any} node - * @return {node is any} - */ const isTag = (node) => { return node.type === 'element'; }; diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 00000000..a013dbbf --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,74 @@ +type XastDoctype = { + type: 'doctype'; + name: string; + data: { + doctype: string; + }; +}; + +type XastInstruction = { + type: 'instruction'; + name: string; + value: string; +}; + +type XastComment = { + type: 'comment'; + value: string; +}; + +type XastCdata = { + type: 'cdata'; + value: string; +}; + +type XastText = { + type: 'text'; + value: string; +}; + +type XastElement = { + type: 'element'; + name: string; + attributes: Record; + children: Array; +}; + +export type XastChild = + | XastDoctype + | XastInstruction + | XastComment + | XastCdata + | XastText + | XastElement; + +type XastRoot = { + type: 'root'; + children: Array; +}; + +export type XastParent = XastRoot | XastElement; + +export type XastNode = XastRoot | XastChild; + +type VisitorNode = { + enter?: (node: Node, parentNode: XastParent) => void; + leave?: (node: Node, parentNode: XastParent) => void; +}; + +type VisitorRoot = { + enter?: (node: XastRoot, parentNode: null) => void; + leave?: (node: XastRoot, parentNode: null) => void; +}; + +export type Visitor = { + doctype?: VisitorNode; + instruction?: VisitorNode; + comment?: VisitorNode; + cdata?: VisitorNode; + text?: VisitorNode; + element?: VisitorNode; + root?: VisitorRoot; +}; + +export type Plugin = (root: XastRoot, params: Params) => null | Visitor; diff --git a/lib/xast.js b/lib/xast.js index a9ecf928..16b5afcd 100644 --- a/lib/xast.js +++ b/lib/xast.js @@ -1,5 +1,12 @@ 'use strict'; +/** + * @typedef {import('./types').XastNode} XastNode + * @typedef {import('./types').XastChild} XastChild + * @typedef {import('./types').XastParent} XastParent + * @typedef {import('./types').Visitor} Visitor + */ + const { selectAll, selectOne, is } = require('css-select'); const xastAdaptor = require('./svgo/css-select-adapter.js'); @@ -8,27 +15,40 @@ const cssSelectOptions = { adapter: xastAdaptor, }; +/** + * @type {(node: XastNode, selector: string) => Array} + */ const querySelectorAll = (node, selector) => { return selectAll(selector, node, cssSelectOptions); }; exports.querySelectorAll = querySelectorAll; +/** + * @type {(node: XastNode, selector: string) => null | XastChild} + */ const querySelector = (node, selector) => { return selectOne(selector, node, cssSelectOptions); }; exports.querySelector = querySelector; +/** + * @type {(node: XastChild, selector: string) => boolean} + */ const matches = (node, selector) => { return is(node, selector, cssSelectOptions); }; exports.matches = matches; +/** + * @type {(node: XastChild, name: string) => null | XastChild} + */ const closestByName = (node, name) => { let currentNode = node; while (currentNode) { if (currentNode.type === 'element' && currentNode.name === name) { return currentNode; } + // @ts-ignore parentNode is hidden from public usage currentNode = currentNode.parentNode; } return null; @@ -38,6 +58,9 @@ exports.closestByName = closestByName; const traverseBreak = Symbol(); exports.traverseBreak = traverseBreak; +/** + * @type {(node: any, fn: any) => any} + */ const traverse = (node, fn) => { if (fn(node) === traverseBreak) { return traverseBreak; @@ -52,7 +75,10 @@ const traverse = (node, fn) => { }; exports.traverse = traverse; -const visit = (node, visitor, parentNode = null) => { +/** + * @type {(node: any, visitor: any, parentNode: any) => void} + */ +const visit = (node, visitor, parentNode) => { const callbacks = visitor[node.type]; if (callbacks && callbacks.enter) { callbacks.enter(node, parentNode); @@ -78,6 +104,9 @@ const visit = (node, visitor, parentNode = null) => { }; exports.visit = visit; +/** + * @type {(node: XastChild, parentNode: XastParent) => void} + */ const detachNodeFromParent = (node, parentNode) => { // avoid splice to not break for loops parentNode.children = parentNode.children.filter((child) => child !== node); diff --git a/plugins/addAttributesToSVGElement.js b/plugins/addAttributesToSVGElement.js index 4997513b..337131cd 100644 --- a/plugins/addAttributesToSVGElement.js +++ b/plugins/addAttributesToSVGElement.js @@ -48,11 +48,16 @@ plugins: [ * Add attributes to an outer element. Example config: * * @author April Arcus + * + * @type {import('../lib/types').Plugin<{ + * attribute?: string | Record, + * attributes?: Array> + * }>} */ exports.fn = (root, params) => { if (!Array.isArray(params.attributes) && !params.attribute) { console.error(ENOCLS); - return; + return null; } const attributes = params.attributes || [params.attribute]; return { @@ -62,12 +67,14 @@ exports.fn = (root, params) => { for (const attribute of attributes) { if (typeof attribute === 'string') { if (node.attributes[attribute] == null) { + // @ts-ignore disallow explicit nullable attribute value node.attributes[attribute] = undefined; } } if (typeof attribute === 'object') { for (const key of Object.keys(attribute)) { if (node.attributes[key] == null) { + // @ts-ignore disallow explicit nullable attribute value node.attributes[key] = attribute[key]; } } diff --git a/plugins/addClassesToSVGElement.js b/plugins/addClassesToSVGElement.js index e13ca4ec..7f38dd0d 100644 --- a/plugins/addClassesToSVGElement.js +++ b/plugins/addClassesToSVGElement.js @@ -50,6 +50,11 @@ plugins: [ * ] * * @author April Arcus + * + * @type {import('../lib/types').Plugin<{ + * className?: string, + * classNames?: Array + * }>} */ exports.fn = (root, params) => { if ( @@ -57,13 +62,14 @@ exports.fn = (root, params) => { !params.className ) { console.error(ENOCLS); - return; + return null; } const classNames = params.classNames || [params.className]; return { element: { enter: (node, parentNode) => { if (node.name === 'svg' && parentNode.type === 'root') { + // @ts-ignore class attribute will be just a string eventually node.class.add(...classNames); } }, diff --git a/plugins/cleanupAttrs.js b/plugins/cleanupAttrs.js index 3990cf9b..d271b15f 100644 --- a/plugins/cleanupAttrs.js +++ b/plugins/cleanupAttrs.js @@ -14,6 +14,12 @@ const regSpaces = /\s{2,}/g; * Cleanup attributes values from newlines, trailing and repeating spaces. * * @author Kir Belevich + * + * @type {import('../lib/types').Plugin<{ + * newlines?: boolean, + * trim?: boolean, + * spaces?: boolean + * }>} */ exports.fn = (root, params) => { const { newlines = true, trim = true, spaces = true } = params; diff --git a/plugins/convertEllipseToCircle.js b/plugins/convertEllipseToCircle.js index 654efe1a..57b26a70 100644 --- a/plugins/convertEllipseToCircle.js +++ b/plugins/convertEllipseToCircle.js @@ -11,14 +11,16 @@ exports.description = 'converts non-eccentric s to s'; * @see https://www.w3.org/TR/SVG11/shapes.html * * @author Taylor Hunt + * + * @type {import('../lib/types').Plugin} */ exports.fn = () => { return { element: { enter: (node) => { if (node.name === 'ellipse') { - const rx = node.attributes.rx || 0; - const ry = node.attributes.ry || 0; + const rx = node.attributes.rx || '0'; + const ry = node.attributes.ry || '0'; if ( rx === ry || rx === 'auto' || diff --git a/plugins/removeAttributesBySelector.js b/plugins/removeAttributesBySelector.js index 60c19b38..691df7b4 100644 --- a/plugins/removeAttributesBySelector.js +++ b/plugins/removeAttributesBySelector.js @@ -71,9 +71,11 @@ exports.description = * ↓ * * - * @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|MDN CSS Selectors} + * @link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|MDN CSS Selectors * * @author Bradley Mease + * + * @type {import('../lib/types').Plugin} */ exports.fn = (root, params) => { const selectors = Array.isArray(params.selectors) @@ -82,12 +84,14 @@ exports.fn = (root, params) => { for (const { selector, attributes } of selectors) { const nodes = querySelectorAll(root, selector); for (const node of nodes) { - if (Array.isArray(attributes)) { - for (const name of attributes) { - delete node.attributes[name]; + if (node.type === 'element') { + if (Array.isArray(attributes)) { + for (const name of attributes) { + delete node.attributes[name]; + } + } else { + delete node.attributes[attributes]; } - } else { - delete node.attributes[attributes]; } } } diff --git a/plugins/removeAttrs.js b/plugins/removeAttrs.js index 54f37363..29b84c44 100644 --- a/plugins/removeAttrs.js +++ b/plugins/removeAttrs.js @@ -10,13 +10,13 @@ const DEFAULT_SEPARATOR = ':'; /** * Remove attributes * - * @param elemSeparator + * @example elemSeparator * format: string * - * @param preserveCurrentColor + * @example preserveCurrentColor * format: boolean * - * @param attrs: + * @example attrs: * * format: [ element* : attribute* : value* ] * @@ -69,6 +69,12 @@ const DEFAULT_SEPARATOR = ':'; * * * @author Benny Schudel + * + * @type {import('../lib/types').Plugin<{ + * elemSeparator?: string, + * preserveCurrentColor?: boolean, + * attrs: string | Array + * }>} */ exports.fn = (root, params) => { // wrap into an array if params is not diff --git a/plugins/removeComments.js b/plugins/removeComments.js index 77f53e11..c3899575 100644 --- a/plugins/removeComments.js +++ b/plugins/removeComments.js @@ -15,6 +15,8 @@ exports.description = 'removes comments'; * Plug-In . SVG Version: 6.00 Build 0) --> * * @author Kir Belevich + * + * @type {import('../lib/types').Plugin} */ exports.fn = () => { return { diff --git a/plugins/removeDesc.js b/plugins/removeDesc.js index 4aaf46f6..9cdf9dcf 100644 --- a/plugins/removeDesc.js +++ b/plugins/removeDesc.js @@ -17,6 +17,8 @@ const standardDescs = /^(Created with|Created using)/; * https://developer.mozilla.org/en-US/docs/Web/SVG/Element/desc * * @author Daniel Wabyick + * + * @type {import('../lib/types').Plugin<{ removeAny?: boolean }>} */ exports.fn = (root, params) => { const { removeAny = true } = params; diff --git a/plugins/removeDoctype.js b/plugins/removeDoctype.js index 9596ef86..81634ecf 100644 --- a/plugins/removeDoctype.js +++ b/plugins/removeDoctype.js @@ -28,6 +28,8 @@ exports.description = 'removes doctype declaration'; * ]> * * @author Kir Belevich + * + * @type {import('../lib/types').Plugin} */ exports.fn = () => { return { diff --git a/plugins/removeElementsByAttr.js b/plugins/removeElementsByAttr.js index 03e2c2df..4cac8b5e 100644 --- a/plugins/removeElementsByAttr.js +++ b/plugins/removeElementsByAttr.js @@ -11,9 +11,7 @@ exports.description = /** * Remove arbitrary SVG elements by ID or className. * - * @param id - * examples: - * + * @example id * > single: remove element with ID of `elementID` * --- * removeElementsByAttr: @@ -26,9 +24,7 @@ exports.description = * - 'elementID' * - 'anotherID' * - * @param class - * examples: - * + * @example class * > single: remove all elements with class of `elementClass` * --- * removeElementsByAttr: @@ -42,6 +38,11 @@ exports.description = * - 'anotherClass' * * @author Eli Dupuis (@elidupuis) + * + * @type {import('../lib/types').Plugin<{ + * id?: string | Array, + * class?: string | Array + * }>} */ exports.fn = (root, params) => { const ids = diff --git a/plugins/removeEmptyText.js b/plugins/removeEmptyText.js index df3586b9..eb2713a1 100644 --- a/plugins/removeEmptyText.js +++ b/plugins/removeEmptyText.js @@ -23,6 +23,12 @@ exports.description = 'removes empty elements'; * * * @author Kir Belevich + * + * @type {import('../lib/types').Plugin<{ + * text?: boolean, + * tspan?: boolean, + * tref?: boolean + * }>} */ exports.fn = (root, params) => { const { text = true, tspan = true, tref = true } = params; diff --git a/plugins/removeMetadata.js b/plugins/removeMetadata.js index 4404a09c..b71b2e8b 100644 --- a/plugins/removeMetadata.js +++ b/plugins/removeMetadata.js @@ -13,6 +13,8 @@ exports.description = 'removes '; * https://www.w3.org/TR/SVG11/metadata.html * * @author Kir Belevich + * + * @type {import('../lib/types').Plugin} */ exports.fn = () => { return { diff --git a/plugins/removeRasterImages.js b/plugins/removeRasterImages.js index 1f7ef111..84d396c3 100644 --- a/plugins/removeRasterImages.js +++ b/plugins/removeRasterImages.js @@ -13,6 +13,8 @@ exports.description = 'removes raster images (disabled by default)'; * @see https://bugs.webkit.org/show_bug.cgi?id=63548 * * @author Kir Belevich + * + * @type {import('../lib/types').Plugin} */ exports.fn = () => { return { diff --git a/plugins/removeScriptElement.js b/plugins/removeScriptElement.js index 9d21e2bd..c2e9946c 100644 --- a/plugins/removeScriptElement.js +++ b/plugins/removeScriptElement.js @@ -12,8 +12,9 @@ exports.description = 'removes