1
0
mirror of https://github.com/svg/svgo.git synced 2025-08-09 02:22:08 +03:00

Covert removeHiddenElems with types (#1532)

Covered removeHiddenElems plugin and big part of our code
- path parser and stringifier
- style manager
This commit is contained in:
Bogdan Chadkin
2021-08-21 14:55:05 +03:00
committed by GitHub
parent 1368f8dc93
commit e4918ccdd1
8 changed files with 205 additions and 61 deletions

View File

@@ -1,5 +1,10 @@
'use strict'; 'use strict';
/**
* @typedef {import('./types').PathDataItem} PathDataItem
* @typedef {import('./types').PathDataCommand} PathDataCommand
*/
// Based on https://www.w3.org/TR/SVG11/paths.html#PathDataBNF // Based on https://www.w3.org/TR/SVG11/paths.html#PathDataBNF
const argsCountPerCommand = { const argsCountPerCommand = {
@@ -26,14 +31,14 @@ const argsCountPerCommand = {
}; };
/** /**
* @param {string} c * @type {(c: string) => c is PathDataCommand}
*/ */
const isCommand = (c) => { const isCommand = (c) => {
return c in argsCountPerCommand; return c in argsCountPerCommand;
}; };
/** /**
* @param {string} c * @type {(c: string) => boolean}
*/ */
const isWsp = (c) => { const isWsp = (c) => {
const codePoint = c.codePointAt(0); const codePoint = c.codePointAt(0);
@@ -46,7 +51,7 @@ const isWsp = (c) => {
}; };
/** /**
* @param {string} c * @type {(c: string) => boolean}
*/ */
const isDigit = (c) => { const isDigit = (c) => {
const codePoint = c.codePointAt(0); const codePoint = c.codePointAt(0);
@@ -61,9 +66,7 @@ const isDigit = (c) => {
*/ */
/** /**
* @param {string} string * @type {(string: string, cursor: number) => [number, number | null]}
* @param {number} cursor
* @return {[number, number | null]}
*/ */
const readNumber = (string, cursor) => { const readNumber = (string, cursor) => {
let i = cursor; let i = cursor;
@@ -130,10 +133,16 @@ const readNumber = (string, cursor) => {
}; };
/** /**
* @param {string} string * @type {(string: string) => Array<PathDataItem>}
*/ */
const parsePathData = (string) => { const parsePathData = (string) => {
/**
* @type {Array<PathDataItem>}
*/
const pathData = []; const pathData = [];
/**
* @type {null | PathDataCommand}
*/
let command = null; let command = null;
let args = /** @type {number[]} */ ([]); let args = /** @type {number[]} */ ([]);
let argsCount = 0; let argsCount = 0;
@@ -232,15 +241,9 @@ const parsePathData = (string) => {
exports.parsePathData = parsePathData; exports.parsePathData = parsePathData;
/** /**
* @typedef {{ * @type {(number: number, precision?: number) => string}
* number: number;
* precision?: number;
* }} StringifyNumberOptions
*/ */
/** const stringifyNumber = (number, precision) => {
* @param {StringifyNumberOptions} param
*/
const stringifyNumber = ({ number, precision }) => {
if (precision != null) { if (precision != null) {
const ratio = 10 ** precision; const ratio = 10 ** precision;
number = Math.round(number * ratio) / ratio; number = Math.round(number * ratio) / ratio;
@@ -250,31 +253,22 @@ const stringifyNumber = ({ number, precision }) => {
}; };
/** /**
* @typedef {{
* command: string;
* args: number[];
* precision?: number;
* disableSpaceAfterFlags?: boolean;
* }} StringifyArgsOptions
*/
/**
*
* Elliptical arc large-arc and sweep flags are rendered with spaces * Elliptical arc large-arc and sweep flags are rendered with spaces
* because many non-browser environments are not able to parse such paths * because many non-browser environments are not able to parse such paths
* *
* @param {StringifyArgsOptions} param * @type {(
* command: string,
* args: number[],
* precision?: number,
* disableSpaceAfterFlags?: boolean
* ) => string}
*/ */
const stringifyArgs = ({ const stringifyArgs = (command, args, precision, disableSpaceAfterFlags) => {
command,
args,
precision,
disableSpaceAfterFlags,
}) => {
let result = ''; let result = '';
let prev = ''; let prev = '';
for (let i = 0; i < args.length; i += 1) { for (let i = 0; i < args.length; i += 1) {
const number = args[i]; const number = args[i];
const numberString = stringifyNumber({ number, precision }); const numberString = stringifyNumber(number, precision);
if ( if (
disableSpaceAfterFlags && disableSpaceAfterFlags &&
(command === 'A' || command === 'a') && (command === 'A' || command === 'a') &&
@@ -298,21 +292,15 @@ const stringifyArgs = ({
}; };
/** /**
*
* @typedef {{ * @typedef {{
* command: string; * pathData: Array<PathDataItem>;
* args: number[];
* }} Command
*/
/**
* @typedef {{
* pathData: Command[];
* precision?: number; * precision?: number;
* disableSpaceAfterFlags?: boolean; * disableSpaceAfterFlags?: boolean;
* }} StringifyPathDataOptions * }} StringifyPathDataOptions
*/ */
/** /**
* @param {StringifyPathDataOptions} param * @type {(options: StringifyPathDataOptions) => string}
*/ */
const stringifyPathData = ({ pathData, precision, disableSpaceAfterFlags }) => { const stringifyPathData = ({ pathData, precision, disableSpaceAfterFlags }) => {
// combine sequence of the same commands // combine sequence of the same commands
@@ -322,6 +310,9 @@ const stringifyPathData = ({ pathData, precision, disableSpaceAfterFlags }) => {
if (i === 0) { if (i === 0) {
combined.push({ command, args }); combined.push({ command, args });
} else { } else {
/**
* @type {PathDataItem}
*/
const last = combined[combined.length - 1]; const last = combined[combined.length - 1];
// match leading moveto with following lineto // match leading moveto with following lineto
if (i === 1) { if (i === 1) {
@@ -349,8 +340,7 @@ const stringifyPathData = ({ pathData, precision, disableSpaceAfterFlags }) => {
let result = ''; let result = '';
for (const { command, args } of combined) { for (const { command, args } of combined) {
result += result +=
command + command + stringifyArgs(command, args, precision, disableSpaceAfterFlags);
stringifyArgs({ command, args, precision, disableSpaceAfterFlags });
} }
return result; return result;
}; };

View File

@@ -1,19 +1,40 @@
'use strict'; 'use strict';
/**
* @typedef {import('css-tree').Rule} CsstreeRule
* @typedef {import('./types').Specificity} Specificity
* @typedef {import('./types').StylesheetRule} StylesheetRule
* @typedef {import('./types').StylesheetDeclaration} StylesheetDeclaration
* @typedef {import('./types').ComputedStyles} ComputedStyles
* @typedef {import('./types').XastRoot} XastRoot
* @typedef {import('./types').XastElement} XastElement
* @typedef {import('./types').XastParent} XastParent
* @typedef {import('./types').XastChild} XastChild
*/
const stable = require('stable'); const stable = require('stable');
const csstree = require('css-tree'); const csstree = require('css-tree');
// @ts-ignore not defined in @types/csso
const specificity = require('csso/lib/restructure/prepare/specificity'); const specificity = require('csso/lib/restructure/prepare/specificity');
const { visit, matches } = require('./xast.js'); const { visit, matches } = require('./xast.js');
const { compareSpecificity } = require('./css-tools.js');
const { const {
attrsGroups, attrsGroups,
inheritableAttrs, inheritableAttrs,
presentationNonInheritableGroupAttrs, presentationNonInheritableGroupAttrs,
} = require('../plugins/_collections.js'); } = require('../plugins/_collections.js');
// @ts-ignore not defined in @types/csstree
const csstreeWalkSkip = csstree.walk.skip;
/**
* @type {(ruleNode: CsstreeRule, dynamic: boolean) => StylesheetRule}
*/
const parseRule = (ruleNode, dynamic) => { const parseRule = (ruleNode, dynamic) => {
let selectors; let selectors;
let selectorsSpecificity; let selectorsSpecificity;
/**
* @type {Array<StylesheetDeclaration>}
*/
const declarations = []; const declarations = [];
csstree.walk(ruleNode, (cssNode) => { csstree.walk(ruleNode, (cssNode) => {
if (cssNode.type === 'SelectorList') { if (cssNode.type === 'SelectorList') {
@@ -27,17 +48,20 @@ const parseRule = (ruleNode, dynamic) => {
} }
}); });
selectors = csstree.generate(newSelectorsNode); selectors = csstree.generate(newSelectorsNode);
return csstree.walk.skip; return csstreeWalkSkip;
} }
if (cssNode.type === 'Declaration') { if (cssNode.type === 'Declaration') {
declarations.push({ declarations.push({
name: cssNode.property, name: cssNode.property,
value: csstree.generate(cssNode.value), value: csstree.generate(cssNode.value),
important: cssNode.important, important: cssNode.important === true,
}); });
return csstree.walk.skip; return csstreeWalkSkip;
} }
}); });
if (selectors == null || selectorsSpecificity == null) {
throw Error('assert');
}
return { return {
dynamic, dynamic,
selectors, selectors,
@@ -46,31 +70,43 @@ const parseRule = (ruleNode, dynamic) => {
}; };
}; };
/**
* @type {(css: string, dynamic: boolean) => Array<StylesheetRule>}
*/
const parseStylesheet = (css, dynamic) => { const parseStylesheet = (css, dynamic) => {
/**
* @type {Array<StylesheetRule>}
*/
const rules = []; const rules = [];
const ast = csstree.parse(css); const ast = csstree.parse(css);
csstree.walk(ast, (cssNode) => { csstree.walk(ast, (cssNode) => {
if (cssNode.type === 'Rule') { if (cssNode.type === 'Rule') {
rules.push(parseRule(cssNode, dynamic || false)); rules.push(parseRule(cssNode, dynamic || false));
return csstree.walk.skip; return csstreeWalkSkip;
} }
if (cssNode.type === 'Atrule') { if (cssNode.type === 'Atrule') {
if (cssNode.name === 'keyframes') { if (cssNode.name === 'keyframes') {
return csstree.walk.skip; return csstreeWalkSkip;
} }
csstree.walk(cssNode, (ruleNode) => { csstree.walk(cssNode, (ruleNode) => {
if (ruleNode.type === 'Rule') { if (ruleNode.type === 'Rule') {
rules.push(parseRule(ruleNode, dynamic || true)); rules.push(parseRule(ruleNode, dynamic || true));
return csstree.walk.skip; return csstreeWalkSkip;
} }
}); });
return csstree.walk.skip; return csstreeWalkSkip;
} }
}); });
return rules; return rules;
}; };
/**
* @type {(stylesheet: Array<StylesheetRule>, node: XastElement) => ComputedStyles}
*/
const computeOwnStyle = (stylesheet, node) => { const computeOwnStyle = (stylesheet, node) => {
/**
* @type {ComputedStyles}
*/
const computedStyle = {}; const computedStyle = {};
const importantStyles = new Map(); const importantStyles = new Map();
@@ -107,6 +143,7 @@ const computeOwnStyle = (stylesheet, node) => {
} }
// collect inline styles // collect inline styles
// @ts-ignore node.style is hidden from pubilc usage
for (const [name, { value, priority }] of node.style.properties) { for (const [name, { value, priority }] of node.style.properties) {
const computed = computedStyle[name]; const computed = computedStyle[name];
const important = priority === 'important'; const important = priority === 'important';
@@ -126,7 +163,31 @@ const computeOwnStyle = (stylesheet, node) => {
return computedStyle; return computedStyle;
}; };
/**
* Compares two selector specificities.
* extracted from https://github.com/keeganstreet/specificity/blob/master/specificity.js#L211
*
* @type {(a: Specificity, b: Specificity) => number}
*/
const compareSpecificity = (a, b) => {
for (var i = 0; i < 4; i += 1) {
if (a[i] < b[i]) {
return -1;
} else if (a[i] > b[i]) {
return 1;
}
}
return 0;
};
/**
* @type {(root: XastRoot) => Array<StylesheetRule>}
*/
const collectStylesheet = (root) => { const collectStylesheet = (root) => {
/**
* @type {Array<StylesheetRule>}
*/
const stylesheet = []; const stylesheet = [];
// find and parse all styles // find and parse all styles
visit(root, { visit(root, {
@@ -159,11 +220,16 @@ const collectStylesheet = (root) => {
}; };
exports.collectStylesheet = collectStylesheet; exports.collectStylesheet = collectStylesheet;
/**
* @type {(stylesheet: Array<StylesheetRule>, node: XastElement) => ComputedStyles}
*/
const computeStyle = (stylesheet, node) => { const computeStyle = (stylesheet, node) => {
// collect inherited styles // collect inherited styles
const computedStyles = computeOwnStyle(stylesheet, node); const computedStyles = computeOwnStyle(stylesheet, node);
let parent = node; let parent = node;
// @ts-ignore parentNode is forbidden in public usage
while (parent.parentNode && parent.parentNode.type !== 'root') { while (parent.parentNode && parent.parentNode.type !== 'root') {
// @ts-ignore parentNode is forbidden in public usage
const inheritedStyles = computeOwnStyle(stylesheet, parent.parentNode); const inheritedStyles = computeOwnStyle(stylesheet, parent.parentNode);
for (const [name, computed] of Object.entries(inheritedStyles)) { for (const [name, computed] of Object.entries(inheritedStyles)) {
if ( if (
@@ -175,9 +241,9 @@ const computeStyle = (stylesheet, node) => {
computedStyles[name] = { ...computed, inherited: true }; computedStyles[name] = { ...computed, inherited: true };
} }
} }
// @ts-ignore parentNode is forbidden in public usage
parent = parent.parentNode; parent = parent.parentNode;
} }
return computedStyles; return computedStyles;
}; };
exports.computeStyle = computeStyle; exports.computeStyle = computeStyle;

View File

@@ -27,7 +27,7 @@ type XastText = {
value: string; value: string;
}; };
type XastElement = { export type XastElement = {
type: 'element'; type: 'element';
name: string; name: string;
attributes: Record<string, string>; attributes: Record<string, string>;
@@ -42,7 +42,7 @@ export type XastChild =
| XastText | XastText
| XastElement; | XastElement;
type XastRoot = { export type XastRoot = {
type: 'root'; type: 'root';
children: Array<XastChild>; children: Array<XastChild>;
}; };
@@ -53,12 +53,12 @@ export type XastNode = XastRoot | XastChild;
type VisitorNode<Node> = { type VisitorNode<Node> = {
enter?: (node: Node, parentNode: XastParent) => void; enter?: (node: Node, parentNode: XastParent) => void;
leave?: (node: Node, parentNode: XastParent) => void; exit?: (node: Node, parentNode: XastParent) => void;
}; };
type VisitorRoot = { type VisitorRoot = {
enter?: (node: XastRoot, parentNode: null) => void; enter?: (node: XastRoot, parentNode: null) => void;
leave?: (node: XastRoot, parentNode: null) => void; exit?: (node: XastRoot, parentNode: null) => void;
}; };
export type Visitor = { export type Visitor = {
@@ -72,3 +72,58 @@ export type Visitor = {
}; };
export type Plugin<Params> = (root: XastRoot, params: Params) => null | Visitor; export type Plugin<Params> = (root: XastRoot, params: Params) => null | Visitor;
export type Specificity = [number, number, number, number];
export type StylesheetDeclaration = {
name: string;
value: string;
important: boolean;
};
export type StylesheetRule = {
dynamic: boolean;
selectors: string;
specificity: Specificity;
declarations: Array<StylesheetDeclaration>;
};
type StaticStyle = {
type: 'static';
inherited: boolean;
value: string;
};
type DynamicStyle = {
type: 'dynamic';
inherited: boolean;
};
export type ComputedStyles = Record<string, StaticStyle | DynamicStyle>;
export type PathDataCommand =
| 'M'
| 'm'
| 'Z'
| 'z'
| 'L'
| 'l'
| 'H'
| 'h'
| 'V'
| 'v'
| 'C'
| 'c'
| 'S'
| 's'
| 'Q'
| 'q'
| 'T'
| 't'
| 'A'
| 'a';
export type PathDataItem = {
command: PathDataCommand;
args: Array<number>;
};

View File

@@ -76,11 +76,12 @@ const traverse = (node, fn) => {
exports.traverse = traverse; exports.traverse = traverse;
/** /**
* @type {(node: any, visitor: any, parentNode: any) => void} * @type {(node: XastNode, visitor: Visitor, parentNode?: any) => void}
*/ */
const visit = (node, visitor, parentNode) => { const visit = (node, visitor, parentNode) => {
const callbacks = visitor[node.type]; const callbacks = visitor[node.type];
if (callbacks && callbacks.enter) { if (callbacks && callbacks.enter) {
// @ts-ignore hard to infer
callbacks.enter(node, parentNode); callbacks.enter(node, parentNode);
} }
// visit root children // visit root children
@@ -99,6 +100,7 @@ const visit = (node, visitor, parentNode) => {
} }
} }
if (callbacks && callbacks.exit) { if (callbacks && callbacks.exit) {
// @ts-ignore hard to infer
callbacks.exit(node, parentNode); callbacks.exit(node, parentNode);
} }
}; };

15
package-lock.json generated
View File

@@ -912,6 +912,21 @@
"@babel/types": "^7.3.0" "@babel/types": "^7.3.0"
} }
}, },
"@types/css-tree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/css-tree/-/css-tree-1.0.6.tgz",
"integrity": "sha512-zjSMDm4C7J1azi9SdT1XwNaVCzeHZx+Y5AVebcg/mrtUULNxZjeamc8oQpUDKpaudM5R+Sy7RFvm2xphfwN64w==",
"dev": true
},
"@types/csso": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@types/csso/-/csso-4.2.0.tgz",
"integrity": "sha512-tGMZcJGgeIA67qbbm/ZKUi7ZyiTETEL0X4opRpFzULbIE1Kyt6EfOVSctw4N/WrEJZwmKw0J+1i/OApm6/CAOQ==",
"dev": true,
"requires": {
"@types/css-tree": "*"
}
},
"@types/estree": { "@types/estree": {
"version": "0.0.39", "version": "0.0.39",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",

View File

@@ -104,6 +104,8 @@
"@rollup/plugin-commonjs": "^17.1.0", "@rollup/plugin-commonjs": "^17.1.0",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.2.1", "@rollup/plugin-node-resolve": "^11.2.1",
"@types/css-tree": "^1.0.6",
"@types/csso": "^4.2.0",
"@types/jest": "^27.0.1", "@types/jest": "^27.0.1",
"del": "^6.0.0", "del": "^6.0.0",
"eslint": "^7.32.0", "eslint": "^7.32.0",

View File

@@ -27,10 +27,25 @@ exports.description =
* - polyline with empty points * - polyline with empty points
* - polygon with empty points * - polygon with empty points
* *
* @param {Object} root
* @param {Object} params
*
* @author Kir Belevich * @author Kir Belevich
*
* @type {import('../lib/types').Plugin<{
* isHidden: boolean,
* displayNone: boolean,
* opacity0: boolean,
* circleR0: boolean,
* ellipseRX0: boolean,
* ellipseRY0: boolean,
* rectWidth0: boolean,
* rectHeight0: boolean,
* patternWidth0: boolean,
* patternHeight0: boolean,
* imageWidth0: boolean,
* imageHeight0: boolean,
* pathEmptyD: boolean,
* polylineEmptyPoints: boolean,
* polygonEmptyPoints: boolean,
* }>}
*/ */
exports.fn = (root, params) => { exports.fn = (root, params) => {
const { const {

View File

@@ -34,7 +34,6 @@
"plugins/removeDimensions.js", "plugins/removeDimensions.js",
"plugins/removeEditorsNSData.js", "plugins/removeEditorsNSData.js",
"plugins/removeEmptyAttrs.js", "plugins/removeEmptyAttrs.js",
"plugins/removeHiddenElems.js",
"plugins/removeNonInheritableGroupAttrs.js", "plugins/removeNonInheritableGroupAttrs.js",
"plugins/removeOffCanvasPaths.js", "plugins/removeOffCanvasPaths.js",
"plugins/removeUnknownsAndDefaults.js", "plugins/removeUnknownsAndDefaults.js",