@iconify/tools 4.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.cjs CHANGED
@@ -88,6 +88,7 @@ require('extract-zip');
88
88
  require('tar');
89
89
  require('./download/gitlab/types.cjs');
90
90
  require('./colors/attribs.cjs');
91
+ require('./optimise/unwrap.cjs');
91
92
  require('./export/helpers/custom-files.cjs');
92
93
  require('child_process');
93
94
 
package/lib/index.mjs CHANGED
@@ -86,5 +86,6 @@ import 'extract-zip';
86
86
  import 'tar';
87
87
  import './download/gitlab/types.mjs';
88
88
  import './colors/attribs.mjs';
89
+ import './optimise/unwrap.mjs';
89
90
  import './export/helpers/custom-files.mjs';
90
91
  import 'child_process';
@@ -17,8 +17,8 @@ type ScanDirectoryCallbackFalseResult = boolean | null | undefined;
17
17
  type ScanDirectoryCallbackStringResult = ScanDirectoryCallbackFalseResult | string;
18
18
  type Callback<T> = (ext: string, file: string, subdir: string, path: string, stat: Stats) => T;
19
19
  type AsyncCallback<T> = Callback<T | Promise<T>>;
20
- type ScanDirectoryCallback = AsyncCallback<ScanDirectoryCallbackStringResult | unknown>;
21
- type ScanDirectorySyncCallback = Callback<ScanDirectoryCallbackStringResult | unknown>;
20
+ type ScanDirectoryCallback = AsyncCallback<ScanDirectoryCallbackStringResult | undefined>;
21
+ type ScanDirectorySyncCallback = Callback<ScanDirectoryCallbackStringResult | undefined>;
22
22
  /**
23
23
  * Find all files in directory
24
24
  */
@@ -17,8 +17,8 @@ type ScanDirectoryCallbackFalseResult = boolean | null | undefined;
17
17
  type ScanDirectoryCallbackStringResult = ScanDirectoryCallbackFalseResult | string;
18
18
  type Callback<T> = (ext: string, file: string, subdir: string, path: string, stat: Stats) => T;
19
19
  type AsyncCallback<T> = Callback<T | Promise<T>>;
20
- type ScanDirectoryCallback = AsyncCallback<ScanDirectoryCallbackStringResult | unknown>;
21
- type ScanDirectorySyncCallback = Callback<ScanDirectoryCallbackStringResult | unknown>;
20
+ type ScanDirectoryCallback = AsyncCallback<ScanDirectoryCallbackStringResult | undefined>;
21
+ type ScanDirectorySyncCallback = Callback<ScanDirectoryCallbackStringResult | undefined>;
22
22
  /**
23
23
  * Find all files in directory
24
24
  */
@@ -17,8 +17,8 @@ type ScanDirectoryCallbackFalseResult = boolean | null | undefined;
17
17
  type ScanDirectoryCallbackStringResult = ScanDirectoryCallbackFalseResult | string;
18
18
  type Callback<T> = (ext: string, file: string, subdir: string, path: string, stat: Stats) => T;
19
19
  type AsyncCallback<T> = Callback<T | Promise<T>>;
20
- type ScanDirectoryCallback = AsyncCallback<ScanDirectoryCallbackStringResult | unknown>;
21
- type ScanDirectorySyncCallback = Callback<ScanDirectoryCallbackStringResult | unknown>;
20
+ type ScanDirectoryCallback = AsyncCallback<ScanDirectoryCallbackStringResult | undefined>;
21
+ type ScanDirectorySyncCallback = Callback<ScanDirectoryCallbackStringResult | undefined>;
22
22
  /**
23
23
  * Find all files in directory
24
24
  */
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const svg_data_tags = require('../svg/data/tags.cjs');
4
+ const optimise_unwrap = require('./unwrap.cjs');
4
5
 
5
6
  function isTinyNumber(value, limit) {
6
7
  const num = parseInt(value);
@@ -24,8 +25,8 @@ function checkClipPathNode(clipNode, expectedWidth, expectedHeight) {
24
25
  ...childNode.attribs
25
26
  };
26
27
  delete attribs["fill"];
27
- const fill = (childNode.attribs["fill"] ?? "").toLowerCase();
28
- if (fill !== "white" && fill !== "#fff" && fill !== "#ffffff") {
28
+ const fill = childNode.attribs["fill"]?.toLowerCase();
29
+ if (fill !== "white" && fill !== "#fff" && fill !== "#ffffff" && fill !== void 0) {
29
30
  console.warn(
30
31
  "Unxepected fill on clip path:",
31
32
  childNode.attribs["fill"]
@@ -42,6 +43,24 @@ function checkClipPathNode(clipNode, expectedWidth, expectedHeight) {
42
43
  }
43
44
  delete attribs["width"];
44
45
  delete attribs["height"];
46
+ for (const attr in childNode.attribs) {
47
+ const value = childNode.attribs[attr];
48
+ switch (attr) {
49
+ case "rx":
50
+ case "ry":
51
+ case "x":
52
+ case "y":
53
+ if (value === "0") {
54
+ delete attribs[attr];
55
+ }
56
+ break;
57
+ case "transform":
58
+ if (value === "") {
59
+ delete attribs[attr];
60
+ }
61
+ break;
62
+ }
63
+ }
45
64
  break;
46
65
  }
47
66
  default:
@@ -74,11 +93,28 @@ function checkClipPathNode(clipNode, expectedWidth, expectedHeight) {
74
93
  }
75
94
  const urlStart = "url(#";
76
95
  const urlEnd = ")";
77
- function removeFigmaClipPathFromSVG(svg) {
96
+ function remove(svg) {
97
+ optimise_unwrap.unwrapEmptyGroup(svg);
98
+ let content = svg.toString();
99
+ const backup = content;
100
+ const clipPathBlocks = content.match(
101
+ /<clipPath[^>]*>[\s\S]+?<\/clipPath>/g
102
+ );
103
+ if (clipPathBlocks?.length === 2 && clipPathBlocks[0] === clipPathBlocks[1]) {
104
+ const split = clipPathBlocks[0];
105
+ const lines = content.split(split);
106
+ content = lines.shift() + split + lines.join("");
107
+ }
108
+ content = content.replaceAll('class="frame-clip-def frame-clip"', "");
109
+ if (content.includes("<defs>")) {
110
+ content = content.replace(/<\/?defs>/g, "");
111
+ }
112
+ if (content !== backup) {
113
+ svg.load(content);
114
+ }
78
115
  const cheerio = svg.$svg;
79
116
  const $root = svg.$svg(":root");
80
117
  const children = $root.children();
81
- const backup = svg.toString();
82
118
  const shapesToClip = [];
83
119
  let clipID;
84
120
  for (let i = 0; i < children.length; i++) {
@@ -120,29 +156,6 @@ function removeFigmaClipPathFromSVG(svg) {
120
156
  const node = children[i];
121
157
  if (node.type === "tag") {
122
158
  const tagName = node.tagName;
123
- if (svg_data_tags.defsTag.has(tagName)) {
124
- const defsChildren = node.children;
125
- for (let j = 0; j < defsChildren.length; j++) {
126
- const childNode = defsChildren[j];
127
- if (childNode.type === "tag" && childNode.tagName === "clipPath") {
128
- const result = checkClipPath(childNode);
129
- if (result !== void 0) {
130
- const validChildren = node.children.filter(
131
- (test) => {
132
- if (test.type === "text") {
133
- return false;
134
- }
135
- return true;
136
- }
137
- );
138
- if (!validChildren.length) {
139
- cheerio(node).remove();
140
- }
141
- return result;
142
- }
143
- }
144
- }
145
- }
146
159
  if (tagName === "clipPath") {
147
160
  const result = checkClipPath(node);
148
161
  if (result !== void 0) {
@@ -154,7 +167,6 @@ function removeFigmaClipPathFromSVG(svg) {
154
167
  };
155
168
  const clipPath = findClipPath();
156
169
  if (!clipPath) {
157
- svg.load(backup);
158
170
  return false;
159
171
  }
160
172
  const attribs = clipPath.attribs;
@@ -163,7 +175,6 @@ function removeFigmaClipPathFromSVG(svg) {
163
175
  cheerio(node).removeAttr("clip-path");
164
176
  for (const attr in attribs) {
165
177
  if (node.attribs[attr] !== void 0) {
166
- svg.load(backup);
167
178
  return false;
168
179
  }
169
180
  cheerio(node).attr(attr, attribs[attr]);
@@ -171,5 +182,16 @@ function removeFigmaClipPathFromSVG(svg) {
171
182
  }
172
183
  return true;
173
184
  }
185
+ function removeFigmaClipPathFromSVG(svg) {
186
+ const backup = svg.toString();
187
+ try {
188
+ if (remove(svg)) {
189
+ return true;
190
+ }
191
+ } catch {
192
+ }
193
+ svg.load(backup);
194
+ return false;
195
+ }
174
196
 
175
197
  exports.removeFigmaClipPathFromSVG = removeFigmaClipPathFromSVG;
@@ -4,7 +4,9 @@ import '@iconify/types';
4
4
  import '@iconify/utils/lib/customisations/defaults';
5
5
 
6
6
  /**
7
- * Removes clip path from SVG, which Figma adds to icons that might have overflowing elements
7
+ * Removes clip path from SVG, which Figma and Penpot add to icons that might have overflowing elements
8
+ *
9
+ * Function was originally designed for Figma only, but later added support for Penpot
8
10
  */
9
11
  declare function removeFigmaClipPathFromSVG(svg: SVG): boolean;
10
12
 
@@ -4,7 +4,9 @@ import '@iconify/types';
4
4
  import '@iconify/utils/lib/customisations/defaults';
5
5
 
6
6
  /**
7
- * Removes clip path from SVG, which Figma adds to icons that might have overflowing elements
7
+ * Removes clip path from SVG, which Figma and Penpot add to icons that might have overflowing elements
8
+ *
9
+ * Function was originally designed for Figma only, but later added support for Penpot
8
10
  */
9
11
  declare function removeFigmaClipPathFromSVG(svg: SVG): boolean;
10
12
 
@@ -4,7 +4,9 @@ import '@iconify/types';
4
4
  import '@iconify/utils/lib/customisations/defaults';
5
5
 
6
6
  /**
7
- * Removes clip path from SVG, which Figma adds to icons that might have overflowing elements
7
+ * Removes clip path from SVG, which Figma and Penpot add to icons that might have overflowing elements
8
+ *
9
+ * Function was originally designed for Figma only, but later added support for Penpot
8
10
  */
9
11
  declare function removeFigmaClipPathFromSVG(svg: SVG): boolean;
10
12
 
@@ -1,4 +1,5 @@
1
1
  import { defsTag, maskTags, symbolTag } from '../svg/data/tags.mjs';
2
+ import { unwrapEmptyGroup } from './unwrap.mjs';
2
3
 
3
4
  function isTinyNumber(value, limit) {
4
5
  const num = parseInt(value);
@@ -22,8 +23,8 @@ function checkClipPathNode(clipNode, expectedWidth, expectedHeight) {
22
23
  ...childNode.attribs
23
24
  };
24
25
  delete attribs["fill"];
25
- const fill = (childNode.attribs["fill"] ?? "").toLowerCase();
26
- if (fill !== "white" && fill !== "#fff" && fill !== "#ffffff") {
26
+ const fill = childNode.attribs["fill"]?.toLowerCase();
27
+ if (fill !== "white" && fill !== "#fff" && fill !== "#ffffff" && fill !== void 0) {
27
28
  console.warn(
28
29
  "Unxepected fill on clip path:",
29
30
  childNode.attribs["fill"]
@@ -40,6 +41,24 @@ function checkClipPathNode(clipNode, expectedWidth, expectedHeight) {
40
41
  }
41
42
  delete attribs["width"];
42
43
  delete attribs["height"];
44
+ for (const attr in childNode.attribs) {
45
+ const value = childNode.attribs[attr];
46
+ switch (attr) {
47
+ case "rx":
48
+ case "ry":
49
+ case "x":
50
+ case "y":
51
+ if (value === "0") {
52
+ delete attribs[attr];
53
+ }
54
+ break;
55
+ case "transform":
56
+ if (value === "") {
57
+ delete attribs[attr];
58
+ }
59
+ break;
60
+ }
61
+ }
43
62
  break;
44
63
  }
45
64
  default:
@@ -72,11 +91,28 @@ function checkClipPathNode(clipNode, expectedWidth, expectedHeight) {
72
91
  }
73
92
  const urlStart = "url(#";
74
93
  const urlEnd = ")";
75
- function removeFigmaClipPathFromSVG(svg) {
94
+ function remove(svg) {
95
+ unwrapEmptyGroup(svg);
96
+ let content = svg.toString();
97
+ const backup = content;
98
+ const clipPathBlocks = content.match(
99
+ /<clipPath[^>]*>[\s\S]+?<\/clipPath>/g
100
+ );
101
+ if (clipPathBlocks?.length === 2 && clipPathBlocks[0] === clipPathBlocks[1]) {
102
+ const split = clipPathBlocks[0];
103
+ const lines = content.split(split);
104
+ content = lines.shift() + split + lines.join("");
105
+ }
106
+ content = content.replaceAll('class="frame-clip-def frame-clip"', "");
107
+ if (content.includes("<defs>")) {
108
+ content = content.replace(/<\/?defs>/g, "");
109
+ }
110
+ if (content !== backup) {
111
+ svg.load(content);
112
+ }
76
113
  const cheerio = svg.$svg;
77
114
  const $root = svg.$svg(":root");
78
115
  const children = $root.children();
79
- const backup = svg.toString();
80
116
  const shapesToClip = [];
81
117
  let clipID;
82
118
  for (let i = 0; i < children.length; i++) {
@@ -118,29 +154,6 @@ function removeFigmaClipPathFromSVG(svg) {
118
154
  const node = children[i];
119
155
  if (node.type === "tag") {
120
156
  const tagName = node.tagName;
121
- if (defsTag.has(tagName)) {
122
- const defsChildren = node.children;
123
- for (let j = 0; j < defsChildren.length; j++) {
124
- const childNode = defsChildren[j];
125
- if (childNode.type === "tag" && childNode.tagName === "clipPath") {
126
- const result = checkClipPath(childNode);
127
- if (result !== void 0) {
128
- const validChildren = node.children.filter(
129
- (test) => {
130
- if (test.type === "text") {
131
- return false;
132
- }
133
- return true;
134
- }
135
- );
136
- if (!validChildren.length) {
137
- cheerio(node).remove();
138
- }
139
- return result;
140
- }
141
- }
142
- }
143
- }
144
157
  if (tagName === "clipPath") {
145
158
  const result = checkClipPath(node);
146
159
  if (result !== void 0) {
@@ -152,7 +165,6 @@ function removeFigmaClipPathFromSVG(svg) {
152
165
  };
153
166
  const clipPath = findClipPath();
154
167
  if (!clipPath) {
155
- svg.load(backup);
156
168
  return false;
157
169
  }
158
170
  const attribs = clipPath.attribs;
@@ -161,7 +173,6 @@ function removeFigmaClipPathFromSVG(svg) {
161
173
  cheerio(node).removeAttr("clip-path");
162
174
  for (const attr in attribs) {
163
175
  if (node.attribs[attr] !== void 0) {
164
- svg.load(backup);
165
176
  return false;
166
177
  }
167
178
  cheerio(node).attr(attr, attribs[attr]);
@@ -169,5 +180,16 @@ function removeFigmaClipPathFromSVG(svg) {
169
180
  }
170
181
  return true;
171
182
  }
183
+ function removeFigmaClipPathFromSVG(svg) {
184
+ const backup = svg.toString();
185
+ try {
186
+ if (remove(svg)) {
187
+ return true;
188
+ }
189
+ } catch {
190
+ }
191
+ svg.load(backup);
192
+ return false;
193
+ }
172
194
 
173
195
  export { removeFigmaClipPathFromSVG };
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ function unwrapEmptyGroup(svg) {
4
+ const cheerio = svg.$svg;
5
+ const $root = svg.$svg(":root");
6
+ const children = $root.children();
7
+ if (children.length !== 1 || children[0].tagName !== "g") {
8
+ return;
9
+ }
10
+ const groupNode = children[0];
11
+ const html = cheerio(groupNode).html();
12
+ if (!html) {
13
+ return;
14
+ }
15
+ for (const attr in groupNode.attribs) {
16
+ const value = groupNode.attribs[attr];
17
+ switch (attr) {
18
+ case "id": {
19
+ if (html?.includes(value)) {
20
+ return;
21
+ }
22
+ break;
23
+ }
24
+ default:
25
+ return;
26
+ }
27
+ }
28
+ $root.html(html);
29
+ }
30
+
31
+ exports.unwrapEmptyGroup = unwrapEmptyGroup;
@@ -0,0 +1,11 @@
1
+ import { SVG } from '../svg/index.cjs';
2
+ import 'cheerio';
3
+ import '@iconify/types';
4
+ import '@iconify/utils/lib/customisations/defaults';
5
+
6
+ /**
7
+ * Removes empty group from SVG root element
8
+ */
9
+ declare function unwrapEmptyGroup(svg: SVG): void;
10
+
11
+ export { unwrapEmptyGroup };
@@ -0,0 +1,11 @@
1
+ import { SVG } from '../svg/index.mjs';
2
+ import 'cheerio';
3
+ import '@iconify/types';
4
+ import '@iconify/utils/lib/customisations/defaults';
5
+
6
+ /**
7
+ * Removes empty group from SVG root element
8
+ */
9
+ declare function unwrapEmptyGroup(svg: SVG): void;
10
+
11
+ export { unwrapEmptyGroup };
@@ -0,0 +1,11 @@
1
+ import { SVG } from '../svg/index.js';
2
+ import 'cheerio';
3
+ import '@iconify/types';
4
+ import '@iconify/utils/lib/customisations/defaults';
5
+
6
+ /**
7
+ * Removes empty group from SVG root element
8
+ */
9
+ declare function unwrapEmptyGroup(svg: SVG): void;
10
+
11
+ export { unwrapEmptyGroup };
@@ -0,0 +1,29 @@
1
+ function unwrapEmptyGroup(svg) {
2
+ const cheerio = svg.$svg;
3
+ const $root = svg.$svg(":root");
4
+ const children = $root.children();
5
+ if (children.length !== 1 || children[0].tagName !== "g") {
6
+ return;
7
+ }
8
+ const groupNode = children[0];
9
+ const html = cheerio(groupNode).html();
10
+ if (!html) {
11
+ return;
12
+ }
13
+ for (const attr in groupNode.attribs) {
14
+ const value = groupNode.attribs[attr];
15
+ switch (attr) {
16
+ case "id": {
17
+ if (html?.includes(value)) {
18
+ return;
19
+ }
20
+ break;
21
+ }
22
+ default:
23
+ return;
24
+ }
25
+ }
26
+ $root.html(html);
27
+ }
28
+
29
+ export { unwrapEmptyGroup };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "type": "module",
4
4
  "description": "Collection of functions for cleaning up and parsing SVG for Iconify project",
5
5
  "author": "Vjacheslav Trushkin",
6
- "version": "4.0.0",
6
+ "version": "4.0.1",
7
7
  "license": "MIT",
8
8
  "bugs": "https://github.com/iconify/tools/issues",
9
9
  "homepage": "https://github.com/iconify/tools",
@@ -16,29 +16,29 @@
16
16
  "types": "./lib/index.d.ts",
17
17
  "dependencies": {
18
18
  "@iconify/types": "^2.0.0",
19
- "@iconify/utils": "^2.1.14",
20
- "@types/tar": "^6.1.10",
21
- "axios": "^1.6.3",
19
+ "@iconify/utils": "^2.1.22",
20
+ "@types/tar": "^6.1.11",
21
+ "axios": "^1.6.7",
22
22
  "cheerio": "1.0.0-rc.12",
23
23
  "extract-zip": "^2.0.1",
24
24
  "local-pkg": "^0.5.0",
25
- "pathe": "^1.1.1",
26
- "svgo": "^3.1.0",
25
+ "pathe": "^1.1.2",
26
+ "svgo": "^3.2.0",
27
27
  "tar": "^6.2.0"
28
28
  },
29
29
  "devDependencies": {
30
- "@types/jest": "^29.5.11",
31
- "@types/node": "^20.10.5",
32
- "@typescript-eslint/eslint-plugin": "^6.16.0",
33
- "@typescript-eslint/parser": "^6.16.0",
30
+ "@types/jest": "^29.5.12",
31
+ "@types/node": "^20.11.17",
32
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
33
+ "@typescript-eslint/parser": "^6.21.0",
34
34
  "cross-env": "^7.0.3",
35
35
  "eslint": "^8.56.0",
36
36
  "eslint-config-prettier": "^9.1.0",
37
- "eslint-plugin-prettier": "^5.1.2",
37
+ "eslint-plugin-prettier": "^5.1.3",
38
38
  "jest": "^29.7.0",
39
- "prettier": "^3.1.1",
39
+ "prettier": "^3.2.5",
40
40
  "rimraf": "^5.0.5",
41
- "ts-jest": "^29.1.1",
41
+ "ts-jest": "^29.1.2",
42
42
  "typescript": "^5.3.3",
43
43
  "unbuild": "^2.0.0"
44
44
  },
@@ -454,6 +454,11 @@
454
454
  "require": "./lib/optimise/svgo.cjs",
455
455
  "import": "./lib/optimise/svgo.mjs"
456
456
  },
457
+ "./lib/optimise/unwrap": {
458
+ "types": "./lib/optimise/unwrap.d.ts",
459
+ "require": "./lib/optimise/unwrap.cjs",
460
+ "import": "./lib/optimise/unwrap.mjs"
461
+ },
457
462
  "./lib/svg/analyse": {
458
463
  "types": "./lib/svg/analyse.d.ts",
459
464
  "require": "./lib/svg/analyse.cjs",