You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
209 lines
6.6 KiB
209 lines
6.6 KiB
'use strict'; |
|
|
|
exports.type = 'full'; |
|
|
|
exports.active = true; |
|
|
|
exports.description = 'removes unused IDs and minifies used'; |
|
|
|
exports.params = { |
|
remove: true, |
|
minify: true, |
|
prefix: '', |
|
preserve: [], |
|
preservePrefixes: [], |
|
force: false |
|
}; |
|
|
|
var referencesProps = new Set(require('./_collections').referencesProps), |
|
regReferencesUrl = /\burl\(("|')?#(.+?)\1\)/, |
|
regReferencesHref = /^#(.+?)$/, |
|
regReferencesBegin = /(\w+)\./, |
|
styleOrScript = ['style', 'script'], |
|
generateIDchars = [ |
|
'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', |
|
'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' |
|
], |
|
maxIDindex = generateIDchars.length - 1; |
|
|
|
/** |
|
* Remove unused and minify used IDs |
|
* (only if there are no any <style> or <script>). |
|
* |
|
* @param {Object} item current iteration item |
|
* @param {Object} params plugin params |
|
* |
|
* @author Kir Belevich |
|
*/ |
|
exports.fn = function(data, params) { |
|
var currentID, |
|
currentIDstring, |
|
IDs = new Map(), |
|
referencesIDs = new Map(), |
|
hasStyleOrScript = false, |
|
preserveIDs = new Set(Array.isArray(params.preserve) ? params.preserve : params.preserve ? [params.preserve] : []), |
|
preserveIDPrefixes = new Set(Array.isArray(params.preservePrefixes) ? params.preservePrefixes : (params.preservePrefixes ? [params.preservePrefixes] : [])), |
|
idValuePrefix = '#', |
|
idValuePostfix = '.'; |
|
|
|
/** |
|
* Bananas! |
|
* |
|
* @param {Array} items input items |
|
* @return {Array} output items |
|
*/ |
|
function monkeys(items) { |
|
for (var i = 0; i < items.content.length && !hasStyleOrScript; i++) { |
|
var item = items.content[i]; |
|
|
|
// quit if <style> or <script> present ('force' param prevents quitting) |
|
if (!params.force) { |
|
if (item.isElem(styleOrScript)) { |
|
hasStyleOrScript = true; |
|
continue; |
|
} |
|
// Don't remove IDs if the whole SVG consists only of defs. |
|
if (item.isElem('defs') && item.parentNode.isElem('svg')) { |
|
var hasDefsOnly = true; |
|
for (var j = i + 1; j < items.content.length; j++) { |
|
if (items.content[j].isElem()) { |
|
hasDefsOnly = false; |
|
break; |
|
} |
|
} |
|
if (hasDefsOnly) { |
|
break; |
|
} |
|
} |
|
} |
|
// …and don't remove any ID if yes |
|
if (item.isElem()) { |
|
item.eachAttr(function(attr) { |
|
var key, match; |
|
|
|
// save IDs |
|
if (attr.name === 'id') { |
|
key = attr.value; |
|
if (IDs.has(key)) { |
|
item.removeAttr('id'); // remove repeated id |
|
} else { |
|
IDs.set(key, item); |
|
} |
|
return; |
|
} |
|
// save references |
|
if (referencesProps.has(attr.name) && (match = attr.value.match(regReferencesUrl))) { |
|
key = match[2]; // url() reference |
|
} else if ( |
|
attr.local === 'href' && (match = attr.value.match(regReferencesHref)) || |
|
attr.name === 'begin' && (match = attr.value.match(regReferencesBegin)) |
|
) { |
|
key = match[1]; // href reference |
|
} |
|
if (key) { |
|
var ref = referencesIDs.get(key) || []; |
|
ref.push(attr); |
|
referencesIDs.set(key, ref); |
|
} |
|
}); |
|
} |
|
// go deeper |
|
if (item.content) { |
|
monkeys(item); |
|
} |
|
} |
|
return items; |
|
} |
|
|
|
data = monkeys(data); |
|
|
|
if (hasStyleOrScript) { |
|
return data; |
|
} |
|
|
|
const idPreserved = id => preserveIDs.has(id) || idMatchesPrefix(preserveIDPrefixes, id); |
|
|
|
for (var ref of referencesIDs) { |
|
var key = ref[0]; |
|
|
|
if (IDs.has(key)) { |
|
// replace referenced IDs with the minified ones |
|
if (params.minify && !idPreserved(key)) { |
|
do { |
|
currentIDstring = getIDstring(currentID = generateID(currentID), params); |
|
} while (idPreserved(currentIDstring)); |
|
|
|
IDs.get(key).attr('id').value = currentIDstring; |
|
|
|
for (var attr of ref[1]) { |
|
attr.value = attr.value.includes(idValuePrefix) ? |
|
attr.value.replace(idValuePrefix + key, idValuePrefix + currentIDstring) : |
|
attr.value.replace(key + idValuePostfix, currentIDstring + idValuePostfix); |
|
} |
|
} |
|
// don't remove referenced IDs |
|
IDs.delete(key); |
|
} |
|
} |
|
// remove non-referenced IDs attributes from elements |
|
if (params.remove) { |
|
for(var keyElem of IDs) { |
|
if (!idPreserved(keyElem[0])) { |
|
keyElem[1].removeAttr('id'); |
|
} |
|
} |
|
} |
|
return data; |
|
}; |
|
|
|
/** |
|
* Check if an ID starts with any one of a list of strings. |
|
* |
|
* @param {Array} of prefix strings |
|
* @param {String} current ID |
|
* @return {Boolean} if currentID starts with one of the strings in prefixArray |
|
*/ |
|
function idMatchesPrefix(prefixArray, currentID) { |
|
if (!currentID) return false; |
|
|
|
for (var prefix of prefixArray) if (currentID.startsWith(prefix)) return true; |
|
return false; |
|
} |
|
|
|
/** |
|
* Generate unique minimal ID. |
|
* |
|
* @param {Array} [currentID] current ID |
|
* @return {Array} generated ID array |
|
*/ |
|
function generateID(currentID) { |
|
if (!currentID) return [0]; |
|
|
|
currentID[currentID.length - 1]++; |
|
|
|
for(var i = currentID.length - 1; i > 0; i--) { |
|
if (currentID[i] > maxIDindex) { |
|
currentID[i] = 0; |
|
|
|
if (currentID[i - 1] !== undefined) { |
|
currentID[i - 1]++; |
|
} |
|
} |
|
} |
|
if (currentID[0] > maxIDindex) { |
|
currentID[0] = 0; |
|
currentID.unshift(0); |
|
} |
|
return currentID; |
|
} |
|
|
|
/** |
|
* Get string from generated ID array. |
|
* |
|
* @param {Array} arr input ID array |
|
* @return {String} output ID string |
|
*/ |
|
function getIDstring(arr, params) { |
|
var str = params.prefix; |
|
return str + arr.map(i => generateIDchars[i]).join(''); |
|
}
|
|
|