UNPKG

6.67 kBJavaScriptView Raw
1'use strict';
2
3exports.type = 'full';
4
5exports.active = true;
6
7exports.description = 'removes unused IDs and minifies used';
8
9exports.params = {
10 remove: true,
11 minify: true,
12 prefix: '',
13 preserve: [],
14 preservePrefixes: [],
15 force: false
16};
17
18var referencesProps = new Set(require('./_collections').referencesProps),
19 regReferencesUrl = /\burl\(("|')?#(.+?)\1\)/,
20 regReferencesHref = /^#(.+?)$/,
21 regReferencesBegin = /(\w+)\./,
22 styleOrScript = ['style', 'script'],
23 generateIDchars = [
24 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
25 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
26 ],
27 maxIDindex = generateIDchars.length - 1;
28
29/**
30 * Remove unused and minify used IDs
31 * (only if there are no any <style> or <script>).
32 *
33 * @param {Object} item current iteration item
34 * @param {Object} params plugin params
35 *
36 * @author Kir Belevich
37 */
38exports.fn = function(data, params) {
39 var currentID,
40 currentIDstring,
41 IDs = new Map(),
42 referencesIDs = new Map(),
43 hasStyleOrScript = false,
44 preserveIDs = new Set(Array.isArray(params.preserve) ? params.preserve : params.preserve ? [params.preserve] : []),
45 preserveIDPrefixes = new Set(Array.isArray(params.preservePrefixes) ? params.preservePrefixes : (params.preservePrefixes ? [params.preservePrefixes] : [])),
46 idValuePrefix = '#',
47 idValuePostfix = '.';
48
49 /**
50 * Bananas!
51 *
52 * @param {Array} items input items
53 * @return {Array} output items
54 */
55 function monkeys(items) {
56 for (var i = 0; i < items.content.length && !hasStyleOrScript; i++) {
57 var item = items.content[i];
58
59 // quit if <style> or <script> present ('force' param prevents quitting)
60 if (!params.force) {
61 if (item.isElem(styleOrScript)) {
62 hasStyleOrScript = true;
63 continue;
64 }
65 // Don't remove IDs if the whole SVG consists only of defs.
66 if (item.isElem('defs') && item.parentNode.isElem('svg')) {
67 var hasDefsOnly = true;
68 for (var j = i + 1; j < items.content.length; j++) {
69 if (items.content[j].isElem()) {
70 hasDefsOnly = false;
71 break;
72 }
73 }
74 if (hasDefsOnly) {
75 break;
76 }
77 }
78 }
79 // …and don't remove any ID if yes
80 if (item.isElem()) {
81 item.eachAttr(function(attr) {
82 var key, match;
83
84 // save IDs
85 if (attr.name === 'id') {
86 key = attr.value;
87 if (IDs.has(key)) {
88 item.removeAttr('id'); // remove repeated id
89 } else {
90 IDs.set(key, item);
91 }
92 return;
93 }
94 // save references
95 if (referencesProps.has(attr.name) && (match = attr.value.match(regReferencesUrl))) {
96 key = match[2]; // url() reference
97 } else if (
98 attr.local === 'href' && (match = attr.value.match(regReferencesHref)) ||
99 attr.name === 'begin' && (match = attr.value.match(regReferencesBegin))
100 ) {
101 key = match[1]; // href reference
102 }
103 if (key) {
104 var ref = referencesIDs.get(key) || [];
105 ref.push(attr);
106 referencesIDs.set(key, ref);
107 }
108 });
109 }
110 // go deeper
111 if (item.content) {
112 monkeys(item);
113 }
114 }
115 return items;
116 }
117
118 data = monkeys(data);
119
120 if (hasStyleOrScript) {
121 return data;
122 }
123
124 for (var ref of referencesIDs) {
125 var key = ref[0];
126
127 if (IDs.has(key)) {
128 // replace referenced IDs with the minified ones
129 if (params.minify && !preserveIDs.has(key) && !idMatchesPrefix(preserveIDPrefixes, key)) {
130 currentIDstring = getIDstring(currentID = generateID(currentID), params);
131 IDs.get(key).attr('id').value = currentIDstring;
132
133 for (var attr of ref[1]) {
134 attr.value = attr.value.includes(idValuePrefix) ?
135 attr.value.replace(idValuePrefix + key, idValuePrefix + currentIDstring) :
136 attr.value.replace(key + idValuePostfix, currentIDstring + idValuePostfix);
137 }
138 }
139 // don't remove referenced IDs
140 IDs.delete(key);
141 }
142 }
143 // remove non-referenced IDs attributes from elements
144 if (params.remove) {
145 for(var keyElem of IDs) {
146 if (!preserveIDs.has(keyElem[0]) && !idMatchesPrefix(preserveIDPrefixes, keyElem[0])) {
147 keyElem[1].removeAttr('id');
148 }
149 }
150 }
151 return data;
152};
153
154/**
155 * Check if an ID starts with any one of a list of strings.
156 *
157 * @param {Array} of prefix strings
158 * @param {String} current ID
159 * @return {Boolean} if currentID starts with one of the strings in prefixArray
160 */
161function idMatchesPrefix(prefixArray, currentID) {
162 if (!currentID) return false;
163
164 for (var prefix of prefixArray) if (currentID.startsWith(prefix)) return true;
165 return false;
166}
167
168/**
169 * Generate unique minimal ID.
170 *
171 * @param {Array} [currentID] current ID
172 * @return {Array} generated ID array
173 */
174function generateID(currentID) {
175 if (!currentID) return [0];
176
177 currentID[currentID.length - 1]++;
178
179 for(var i = currentID.length - 1; i > 0; i--) {
180 if (currentID[i] > maxIDindex) {
181 currentID[i] = 0;
182
183 if (currentID[i - 1] !== undefined) {
184 currentID[i - 1]++;
185 }
186 }
187 }
188 if (currentID[0] > maxIDindex) {
189 currentID[0] = 0;
190 currentID.unshift(0);
191 }
192 return currentID;
193}
194
195/**
196 * Get string from generated ID array.
197 *
198 * @param {Array} arr input ID array
199 * @return {String} output ID string
200 */
201function getIDstring(arr, params) {
202 var str = params.prefix;
203 return str + arr.map(i => generateIDchars[i]).join('');
204}