Comparing version
'use strict'; | ||
const os = require('os'); | ||
const fs = require('fs'); | ||
@@ -7,6 +8,9 @@ const path = require('path'); | ||
extendDefaultPlugins, | ||
optimize, | ||
optimize: optimizeAgnostic, | ||
createContentItem, | ||
} = require('./svgo.js'); | ||
exports.extendDefaultPlugins = extendDefaultPlugins; | ||
exports.createContentItem = createContentItem; | ||
const importConfig = async (configFile) => { | ||
@@ -51,6 +55,17 @@ const config = require(configFile); | ||
}; | ||
exports.loadConfig = loadConfig; | ||
exports.loadConfig = loadConfig; | ||
exports.extendDefaultPlugins = extendDefaultPlugins; | ||
const optimize = (input, config) => { | ||
if (typeof config !== 'object') { | ||
throw Error('Config should be an object'); | ||
} | ||
return optimizeAgnostic(input, { | ||
...config, | ||
js2svg: { | ||
// platform specific default for end of line | ||
eol: os.EOL === '\r\n' ? 'crlf' : 'lf', | ||
...(config == null ? null : config.js2svg), | ||
}, | ||
}); | ||
}; | ||
exports.optimize = optimize; | ||
exports.createContentItem = createContentItem; |
@@ -32,3 +32,8 @@ 'use strict'; | ||
info.multipassCount = i; | ||
svgjs = svg2js(input); | ||
// TODO throw this error in v3 | ||
try { | ||
svgjs = svg2js(input, config.path); | ||
} catch (error) { | ||
return { error: error.toString(), modernError: error }; | ||
} | ||
if (svgjs.error != null) { | ||
@@ -35,0 +40,0 @@ if (config.path != null) { |
@@ -5,3 +5,3 @@ 'use strict'; | ||
const PATH = require('path'); | ||
const { green } = require('colorette'); | ||
const { green, red } = require('colorette'); | ||
const { loadConfig, optimize } = require('../svgo-node.js'); | ||
@@ -59,2 +59,7 @@ const pluginsMap = require('../../plugins/plugins.js'); | ||
.option( | ||
'--eol <EOL>', | ||
'Line break to use when outputting SVG: lf, crlf. If unspecified, uses platform default.' | ||
) | ||
.option('--final-newline', 'Ensure SVG ends with a line break') | ||
.option( | ||
'-r, --recursive', | ||
@@ -117,2 +122,9 @@ "Use with '--folder'. Optimizes *.svg files in folders recursively." | ||
if (opts.eol != null && opts.eol !== 'lf' && opts.eol !== 'crlf') { | ||
console.error( | ||
"error: option '--eol' must have one of the following values: 'lf' or 'crlf'" | ||
); | ||
process.exit(1); | ||
} | ||
// --show-plugins | ||
@@ -191,2 +203,14 @@ if (opts.showPlugins) { | ||
// --eol | ||
if (opts.eol) { | ||
config.js2svg = config.js2svg || {}; | ||
config.js2svg.eol = opts.eol; | ||
} | ||
// --final-newline | ||
if (opts.finalNewline) { | ||
config.js2svg = config.js2svg || {}; | ||
config.js2svg.finalNewline = true; | ||
} | ||
// --output | ||
@@ -366,8 +390,5 @@ if (output) { | ||
const result = optimize(data, { ...config, ...info }); | ||
if (result.error) { | ||
let message = result.error; | ||
if (result.path != null) { | ||
message += `\nFile: ${result.path}`; | ||
} | ||
throw Error(message); | ||
if (result.modernError) { | ||
console.error(red(result.modernError.toString())); | ||
process.exit(1); | ||
} | ||
@@ -374,0 +395,0 @@ if (config.datauri) { |
'use strict'; | ||
var EOL = require('os').EOL, | ||
textElems = require('../../plugins/_collections.js').textElems; | ||
const { textElems } = require('../../plugins/_collections.js'); | ||
@@ -31,2 +30,4 @@ var defaults = { | ||
useShortTags: true, | ||
eol: 'lf', | ||
finalNewline: false, | ||
}; | ||
@@ -68,11 +69,17 @@ | ||
if (this.config.eol === 'crlf') { | ||
this.eol = '\r\n'; | ||
} else { | ||
this.eol = '\n'; | ||
} | ||
if (this.config.pretty) { | ||
this.config.doctypeEnd += EOL; | ||
this.config.procInstEnd += EOL; | ||
this.config.commentEnd += EOL; | ||
this.config.cdataEnd += EOL; | ||
this.config.tagShortEnd += EOL; | ||
this.config.tagOpenEnd += EOL; | ||
this.config.tagCloseEnd += EOL; | ||
this.config.textEnd += EOL; | ||
this.config.doctypeEnd += this.eol; | ||
this.config.procInstEnd += this.eol; | ||
this.config.commentEnd += this.eol; | ||
this.config.cdataEnd += this.eol; | ||
this.config.tagShortEnd += this.eol; | ||
this.config.tagOpenEnd += this.eol; | ||
this.config.tagCloseEnd += this.eol; | ||
this.config.textEnd += this.eol; | ||
} | ||
@@ -123,2 +130,11 @@ | ||
if ( | ||
this.config.finalNewline && | ||
this.indentLevel === 0 && | ||
svg.length > 0 && | ||
svg[svg.length - 1] !== '\n' | ||
) { | ||
svg += this.eol; | ||
} | ||
return { | ||
@@ -125,0 +141,0 @@ data: svg, |
'use strict'; | ||
const { selectAll, selectOne, is } = require('css-select'); | ||
const { parseName } = require('./tools.js'); | ||
const svgoCssSelectAdapter = require('./css-select-adapter'); | ||
@@ -9,2 +8,31 @@ const CSSClassList = require('./css-class-list'); | ||
/** | ||
* @type {(name: string) => { prefix: string, local: string }} | ||
*/ | ||
const parseName = (name) => { | ||
if (name == null) { | ||
return { | ||
prefix: '', | ||
local: '', | ||
}; | ||
} | ||
if (name === 'xmlns') { | ||
return { | ||
prefix: 'xmlns', | ||
local: '', | ||
}; | ||
} | ||
const chunks = name.split(':'); | ||
if (chunks.length === 1) { | ||
return { | ||
prefix: '', | ||
local: chunks[0], | ||
}; | ||
} | ||
return { | ||
prefix: chunks[0], | ||
local: chunks[1], | ||
}; | ||
}; | ||
var cssSelectOpts = { | ||
@@ -11,0 +39,0 @@ xmlMode: true, |
@@ -37,3 +37,5 @@ 'use strict'; | ||
const visitor = plugin.fn(ast, params, info); | ||
visit(ast, visitor); | ||
if (visitor != null) { | ||
visit(ast, visitor); | ||
} | ||
} | ||
@@ -40,0 +42,0 @@ } |
@@ -7,2 +7,51 @@ 'use strict'; | ||
class SvgoParserError extends Error { | ||
constructor(message, line, column, source, file) { | ||
super(message); | ||
this.name = 'SvgoParserError'; | ||
this.message = `${file || '<input>'}:${line}:${column}: ${message}`; | ||
this.reason = message; | ||
this.line = line; | ||
this.column = column; | ||
this.source = source; | ||
if (Error.captureStackTrace) { | ||
Error.captureStackTrace(this, SvgoParserError); | ||
} | ||
} | ||
toString() { | ||
const lines = this.source.split(/\r?\n/); | ||
const startLine = Math.max(this.line - 3, 0); | ||
const endLine = Math.min(this.line + 2, lines.length); | ||
const lineNumberWidth = String(endLine).length; | ||
const startColumn = Math.max(this.column - 54, 0); | ||
const endColumn = Math.max(this.column + 20, 80); | ||
const code = lines | ||
.slice(startLine, endLine) | ||
.map((line, index) => { | ||
const lineSlice = line.slice(startColumn, endColumn); | ||
let ellipsisPrefix = ''; | ||
let ellipsisSuffix = ''; | ||
if (startColumn !== 0) { | ||
ellipsisPrefix = startColumn > line.length - 1 ? ' ' : '…'; | ||
} | ||
if (endColumn < line.length - 1) { | ||
ellipsisSuffix = '…'; | ||
} | ||
const number = startLine + 1 + index; | ||
const gutter = ` ${number.toString().padStart(lineNumberWidth)} | `; | ||
if (number === this.line) { | ||
const gutterSpacing = gutter.replace(/[^|]/g, ' '); | ||
const lineSpacing = ( | ||
ellipsisPrefix + line.slice(startColumn, this.column - 1) | ||
).replace(/[^\t]/g, ' '); | ||
const spacing = gutterSpacing + lineSpacing; | ||
return `>${gutter}${ellipsisPrefix}${lineSlice}${ellipsisSuffix}\n ${spacing}^`; | ||
} | ||
return ` ${gutter}${ellipsisPrefix}${lineSlice}${ellipsisSuffix}`; | ||
}) | ||
.join('\n'); | ||
return `${this.name}: ${this.message}\n\n${code}\n`; | ||
} | ||
} | ||
const entityDeclaration = /<!ENTITY\s+(\S+)\s+(?:'([^']+)'|"([^"]+)")\s*>/g; | ||
@@ -24,3 +73,3 @@ | ||
*/ | ||
module.exports = function (data) { | ||
module.exports = function (data, from) { | ||
const sax = SAX.parser(config.strict, config); | ||
@@ -120,14 +169,16 @@ const root = new JSAPI({ type: 'root', children: [] }); | ||
sax.onerror = function (e) { | ||
e.message = 'Error in parsing SVG: ' + e.message; | ||
if (e.message.indexOf('Unexpected end') < 0) { | ||
throw e; | ||
const error = new SvgoParserError( | ||
e.reason, | ||
e.line + 1, | ||
e.column, | ||
data, | ||
from | ||
); | ||
if (e.message.indexOf('Unexpected end') === -1) { | ||
throw error; | ||
} | ||
}; | ||
try { | ||
sax.write(data).close(); | ||
return root; | ||
} catch (e) { | ||
return { error: e.message }; | ||
} | ||
sax.write(data).close(); | ||
return root; | ||
}; |
@@ -138,31 +138,1 @@ 'use strict'; | ||
exports.removeLeadingZero = removeLeadingZero; | ||
/** | ||
* @type {(name: string) => { prefix: string, local: string }} | ||
*/ | ||
const parseName = (name) => { | ||
if (name == null) { | ||
return { | ||
prefix: '', | ||
local: '', | ||
}; | ||
} | ||
if (name === 'xmlns') { | ||
return { | ||
prefix: 'xmlns', | ||
local: '', | ||
}; | ||
} | ||
const chunks = name.split(':'); | ||
if (chunks.length === 1) { | ||
return { | ||
prefix: '', | ||
local: chunks[0], | ||
}; | ||
} | ||
return { | ||
prefix: chunks[0], | ||
local: chunks[1], | ||
}; | ||
}; | ||
exports.parseName = parseName; |
@@ -74,4 +74,13 @@ type XastDoctype = { | ||
export type Plugin<Params> = (root: XastRoot, params: Params) => null | Visitor; | ||
export type PluginInfo = { | ||
path?: string; | ||
multipassCount: number; | ||
}; | ||
export type Plugin<Params> = ( | ||
root: XastRoot, | ||
params: Params, | ||
info: PluginInfo | ||
) => null | Visitor; | ||
export type Specificity = [number, number, number, number]; | ||
@@ -78,0 +87,0 @@ |
@@ -58,22 +58,2 @@ 'use strict'; | ||
const traverseBreak = Symbol(); | ||
exports.traverseBreak = traverseBreak; | ||
/** | ||
* @type {(node: any, fn: any) => any} | ||
*/ | ||
const traverse = (node, fn) => { | ||
if (fn(node) === traverseBreak) { | ||
return traverseBreak; | ||
} | ||
if (node.type === 'root' || node.type === 'element') { | ||
for (const child of node.children) { | ||
if (traverse(child, fn) === traverseBreak) { | ||
return traverseBreak; | ||
} | ||
} | ||
} | ||
}; | ||
exports.traverse = traverse; | ||
const visitSkip = Symbol(); | ||
@@ -80,0 +60,0 @@ exports.visitSkip = visitSkip; |
{ | ||
"name": "svgo", | ||
"version": "2.5.0", | ||
"version": "2.6.0", | ||
"description": "Nodejs-based tool for optimizing SVG vector graphics files", | ||
@@ -95,3 +95,3 @@ "keywords": [ | ||
"dependencies": { | ||
"@trysound/sax": "0.1.1", | ||
"@trysound/sax": "0.2.0", | ||
"colorette": "^1.3.0", | ||
@@ -105,5 +105,5 @@ "commander": "^7.2.0", | ||
"devDependencies": { | ||
"@rollup/plugin-commonjs": "^17.1.0", | ||
"@rollup/plugin-commonjs": "^20.0.0", | ||
"@rollup/plugin-json": "^4.1.0", | ||
"@rollup/plugin-node-resolve": "^11.2.1", | ||
"@rollup/plugin-node-resolve": "^13.0.4", | ||
"@types/css-tree": "^1.0.6", | ||
@@ -114,13 +114,14 @@ "@types/csso": "^4.2.0", | ||
"eslint": "^7.32.0", | ||
"jest": "^27.0.6", | ||
"jest": "^27.1.0", | ||
"mock-stdin": "^1.0.0", | ||
"node-fetch": "^2.6.1", | ||
"pixelmatch": "^5.2.1", | ||
"playwright": "^1.14.0", | ||
"playwright": "^1.14.1", | ||
"pngjs": "^6.0.0", | ||
"prettier": "^2.3.2", | ||
"rollup": "^2.56.2", | ||
"rollup": "^2.56.3", | ||
"rollup-plugin-terser": "^7.0.2", | ||
"strip-ansi": "^6.0.0", | ||
"tar-stream": "^2.2.0", | ||
"typescript": "^4.3.5" | ||
"typescript": "^4.4.2" | ||
}, | ||
@@ -127,0 +128,0 @@ "engines": { |
'use strict'; | ||
const { traverse, traverseBreak } = require('../lib/xast.js'); | ||
const { parseName } = require('../lib/svgo/tools.js'); | ||
/** | ||
* @typedef {import('../lib/types').XastElement} XastElement | ||
*/ | ||
const { visitSkip } = require('../lib/xast.js'); | ||
const { referencesProps } = require('./_collections.js'); | ||
exports.type = 'visitor'; | ||
exports.name = 'cleanupIDs'; | ||
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, | ||
}; | ||
const regReferencesUrl = /\burl\(("|')?#(.+?)\1\)/; | ||
const regReferencesHref = /^#(.+?)$/; | ||
const regReferencesBegin = /(\w+)\./; | ||
const 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', | ||
]; | ||
const maxIDindex = generateIDchars.length - 1; | ||
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>). | ||
* Check if an ID starts with any one of a list of strings. | ||
* | ||
* @param {Object} item current iteration item | ||
* @param {Object} params plugin params | ||
* | ||
* @author Kir Belevich | ||
* @type {(string: string, prefixes: Array<string>) => boolean} | ||
*/ | ||
exports.fn = function (root, 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 = '.'; | ||
traverse(root, (node) => { | ||
if (hasStyleOrScript === true) { | ||
return traverseBreak; | ||
const hasStringPrefix = (string, prefixes) => { | ||
for (const prefix of prefixes) { | ||
if (string.startsWith(prefix)) { | ||
return true; | ||
} | ||
// quit if <style> or <script> present ('force' param prevents quitting) | ||
if (!params.force) { | ||
if (node.isElem(styleOrScript) && node.children.length !== 0) { | ||
hasStyleOrScript = true; | ||
return; | ||
} | ||
// Don't remove IDs if the whole SVG consists only of defs. | ||
if (node.type === 'element' && node.name === 'svg') { | ||
let hasDefsOnly = true; | ||
for (const child of node.children) { | ||
if (child.type !== 'element' || child.name !== 'defs') { | ||
hasDefsOnly = false; | ||
break; | ||
} | ||
} | ||
if (hasDefsOnly) { | ||
return traverseBreak; | ||
} | ||
} | ||
} | ||
// …and don't remove any ID if yes | ||
if (node.type === 'element') { | ||
for (const [name, value] of Object.entries(node.attributes)) { | ||
let key; | ||
let match; | ||
// save IDs | ||
if (name === 'id') { | ||
key = value; | ||
if (IDs.has(key)) { | ||
delete node.attributes.id; // remove repeated id | ||
} else { | ||
IDs.set(key, node); | ||
} | ||
} else { | ||
// save references | ||
const { local } = parseName(name); | ||
if ( | ||
referencesProps.has(name) && | ||
(match = value.match(regReferencesUrl)) | ||
) { | ||
key = match[2]; // url() reference | ||
} else if ( | ||
(local === 'href' && (match = value.match(regReferencesHref))) || | ||
(name === 'begin' && (match = value.match(regReferencesBegin))) | ||
) { | ||
key = match[1]; // href reference | ||
} | ||
if (key) { | ||
const refs = referencesIDs.get(key) || []; | ||
refs.push({ element: node, name, value }); | ||
referencesIDs.set(key, refs); | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
if (hasStyleOrScript) { | ||
return root; | ||
} | ||
const idPreserved = (id) => | ||
preserveIDs.has(id) || idMatchesPrefix(preserveIDPrefixes, id); | ||
for (const [key, refs] of referencesIDs) { | ||
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).attributes.id = currentIDstring; | ||
for (const { element, name, value } of refs) { | ||
element.attributes[name] = value.includes(idValuePrefix) | ||
? value.replace( | ||
idValuePrefix + key, | ||
idValuePrefix + currentIDstring | ||
) | ||
: 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])) { | ||
delete keyElem[1].attributes.id; | ||
} | ||
} | ||
} | ||
return root; | ||
return false; | ||
}; | ||
/** | ||
* 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 | ||
* @type {(currentID: null | Array<number>) => Array<number>} | ||
*/ | ||
function generateID(currentID) { | ||
if (!currentID) return [0]; | ||
currentID[currentID.length - 1]++; | ||
for (var i = currentID.length - 1; i > 0; i--) { | ||
const generateID = (currentID) => { | ||
if (currentID == null) { | ||
return [0]; | ||
} | ||
currentID[currentID.length - 1] += 1; | ||
for (let i = currentID.length - 1; i > 0; i--) { | ||
if (currentID[i] > maxIDindex) { | ||
currentID[i] = 0; | ||
if (currentID[i - 1] !== undefined) { | ||
@@ -267,3 +111,3 @@ currentID[i - 1]++; | ||
return currentID; | ||
} | ||
}; | ||
@@ -273,8 +117,183 @@ /** | ||
* | ||
* @param {Array} arr input ID array | ||
* @return {String} output ID string | ||
* @type {(arr: Array<number>, prefix: string) => string} | ||
*/ | ||
function getIDstring(arr, params) { | ||
var str = params.prefix; | ||
return str + arr.map((i) => generateIDchars[i]).join(''); | ||
} | ||
const getIDstring = (arr, prefix) => { | ||
return prefix + arr.map((i) => generateIDchars[i]).join(''); | ||
}; | ||
/** | ||
* Remove unused and minify used IDs | ||
* (only if there are no any <style> or <script>). | ||
* | ||
* @author Kir Belevich | ||
* | ||
* @type {import('../lib/types').Plugin<{ | ||
* remove?: boolean, | ||
* minify?: boolean, | ||
* prefix?: string, | ||
* preserve?: Array<string>, | ||
* preservePrefixes?: Array<string>, | ||
* force?: boolean, | ||
* }>} | ||
*/ | ||
exports.fn = (_root, params) => { | ||
const { | ||
remove = true, | ||
minify = true, | ||
prefix = '', | ||
preserve = [], | ||
preservePrefixes = [], | ||
force = false, | ||
} = params; | ||
const preserveIDs = new Set( | ||
Array.isArray(preserve) ? preserve : preserve ? [preserve] : [] | ||
); | ||
const preserveIDPrefixes = Array.isArray(preservePrefixes) | ||
? preservePrefixes | ||
: preservePrefixes | ||
? [preservePrefixes] | ||
: []; | ||
/** | ||
* @type {Map<string, XastElement>} | ||
*/ | ||
const nodeById = new Map(); | ||
/** | ||
* @type {Map<string, Array<{element: XastElement, name: string, value: string }>>} | ||
*/ | ||
const referencesById = new Map(); | ||
let deoptimized = false; | ||
return { | ||
element: { | ||
enter: (node) => { | ||
if (force == false) { | ||
// deoptimize if style or script elements are present | ||
if ( | ||
(node.name === 'style' || node.name === 'script') && | ||
node.children.length !== 0 | ||
) { | ||
deoptimized = true; | ||
return; | ||
} | ||
// avoid removing IDs if the whole SVG consists only of defs | ||
if (node.name === 'svg') { | ||
let hasDefsOnly = true; | ||
for (const child of node.children) { | ||
if (child.type !== 'element' || child.name !== 'defs') { | ||
hasDefsOnly = false; | ||
break; | ||
} | ||
} | ||
if (hasDefsOnly) { | ||
return visitSkip; | ||
} | ||
} | ||
} | ||
for (const [name, value] of Object.entries(node.attributes)) { | ||
if (name === 'id') { | ||
// collect all ids | ||
const id = value; | ||
if (nodeById.has(id)) { | ||
delete node.attributes.id; // remove repeated id | ||
} else { | ||
nodeById.set(id, node); | ||
} | ||
} else { | ||
// collect all references | ||
/** | ||
* @type {null | string} | ||
*/ | ||
let id = null; | ||
if (referencesProps.includes(name)) { | ||
const match = value.match(regReferencesUrl); | ||
if (match != null) { | ||
id = match[2]; // url() reference | ||
} | ||
} | ||
if (name === 'href' || name.endsWith(':href')) { | ||
const match = value.match(regReferencesHref); | ||
if (match != null) { | ||
id = match[1]; // href reference | ||
} | ||
} | ||
if (name === 'begin') { | ||
const match = value.match(regReferencesBegin); | ||
if (match != null) { | ||
id = match[1]; // href reference | ||
} | ||
} | ||
if (id != null) { | ||
let refs = referencesById.get(id); | ||
if (refs == null) { | ||
refs = []; | ||
referencesById.set(id, refs); | ||
} | ||
refs.push({ element: node, name, value }); | ||
} | ||
} | ||
} | ||
}, | ||
}, | ||
root: { | ||
exit: () => { | ||
if (deoptimized) { | ||
return; | ||
} | ||
/** | ||
* @type {(id: string) => boolean} | ||
**/ | ||
const isIdPreserved = (id) => | ||
preserveIDs.has(id) || hasStringPrefix(id, preserveIDPrefixes); | ||
/** | ||
* @type {null | Array<number>} | ||
*/ | ||
let currentID = null; | ||
for (const [id, refs] of referencesById) { | ||
const node = nodeById.get(id); | ||
if (node != null) { | ||
// replace referenced IDs with the minified ones | ||
if (minify && isIdPreserved(id) === false) { | ||
/** | ||
* @type {null | string} | ||
*/ | ||
let currentIDString = null; | ||
do { | ||
currentID = generateID(currentID); | ||
currentIDString = getIDstring(currentID, prefix); | ||
} while (isIdPreserved(currentIDString)); | ||
node.attributes.id = currentIDString; | ||
for (const { element, name, value } of refs) { | ||
if (value.includes('#')) { | ||
// replace id in href and url() | ||
element.attributes[name] = value.replace( | ||
`#${id}`, | ||
`#${currentIDString}` | ||
); | ||
} else { | ||
// replace id in begin attribute | ||
element.attributes[name] = value.replace( | ||
`${id}.`, | ||
`${currentIDString}.` | ||
); | ||
} | ||
} | ||
} | ||
// keep referenced node | ||
nodeById.delete(id); | ||
} | ||
} | ||
// remove non-referenced IDs attributes from elements | ||
if (remove) { | ||
for (const [id, node] of nodeById) { | ||
if (isIdPreserved(id) === false) { | ||
delete node.attributes.id; | ||
} | ||
} | ||
} | ||
}, | ||
}, | ||
}; | ||
}; |
'use strict'; | ||
/** | ||
* @typedef {import('../lib/types').XastElement} XastElement | ||
*/ | ||
const csso = require('csso'); | ||
const { traverse } = require('../lib/xast.js'); | ||
exports.type = 'visitor'; | ||
exports.name = 'minifyStyles'; | ||
exports.type = 'full'; | ||
exports.active = true; | ||
exports.description = | ||
'minifies styles and removes unused styles based on usage data'; | ||
exports.params = { | ||
// ... CSSO options goes here | ||
// additional | ||
usage: { | ||
force: false, // force to use usage data even if it unsafe (document contains <script> or on* attributes) | ||
ids: true, | ||
classes: true, | ||
tags: true, | ||
}, | ||
}; | ||
/** | ||
@@ -31,124 +19,131 @@ * Minifies styles (<style> element + style attribute) using CSSO | ||
* @author strarsis <strarsis@gmail.com> | ||
* | ||
* @type {import('../lib/types').Plugin<csso.MinifyOptions & Omit<csso.CompressOptions, 'usage'> & { | ||
* usage?: boolean | { | ||
* force?: boolean, | ||
* ids?: boolean, | ||
* classes?: boolean, | ||
* tags?: boolean | ||
* } | ||
* }>} | ||
*/ | ||
exports.fn = function (ast, options) { | ||
options = options || {}; | ||
exports.fn = (_root, { usage, ...params }) => { | ||
let enableTagsUsage = true; | ||
let enableIdsUsage = true; | ||
let enableClassesUsage = true; | ||
// force to use usage data even if it unsafe (document contains <script> or on* attributes) | ||
let forceUsageDeoptimized = false; | ||
if (typeof usage === 'boolean') { | ||
enableTagsUsage = usage; | ||
enableIdsUsage = usage; | ||
enableClassesUsage = usage; | ||
} else if (usage) { | ||
enableTagsUsage = usage.tags == null ? true : usage.tags; | ||
enableIdsUsage = usage.ids == null ? true : usage.ids; | ||
enableClassesUsage = usage.classes == null ? true : usage.classes; | ||
forceUsageDeoptimized = usage.force == null ? false : usage.force; | ||
} | ||
/** | ||
* @type {Array<XastElement>} | ||
*/ | ||
const styleElements = []; | ||
/** | ||
* @type {Array<XastElement>} | ||
*/ | ||
const elementsWithStyleAttributes = []; | ||
let deoptimized = false; | ||
/** | ||
* @type {Set<string>} | ||
*/ | ||
const tagsUsage = new Set(); | ||
/** | ||
* @type {Set<string>} | ||
*/ | ||
const idsUsage = new Set(); | ||
/** | ||
* @type {Set<string>} | ||
*/ | ||
const classesUsage = new Set(); | ||
var minifyOptionsForStylesheet = cloneObject(options); | ||
var minifyOptionsForAttribute = cloneObject(options); | ||
var elems = findStyleElems(ast); | ||
return { | ||
element: { | ||
enter: (node) => { | ||
// detect deoptimisations | ||
if (node.name === 'script') { | ||
deoptimized = true; | ||
} | ||
for (const name of Object.keys(node.attributes)) { | ||
if (name.startsWith('on')) { | ||
deoptimized = true; | ||
} | ||
} | ||
// collect tags, ids and classes usage | ||
tagsUsage.add(node.name); | ||
if (node.attributes.id != null) { | ||
idsUsage.add(node.attributes.id); | ||
} | ||
if (node.attributes.class != null) { | ||
for (const className of node.attributes.class.split(/\s+/)) { | ||
classesUsage.add(className); | ||
} | ||
} | ||
// collect style elements or elements with style attribute | ||
if (node.name === 'style' && node.children.length !== 0) { | ||
styleElements.push(node); | ||
} else if (node.attributes.style != null) { | ||
elementsWithStyleAttributes.push(node); | ||
} | ||
}, | ||
}, | ||
minifyOptionsForStylesheet.usage = collectUsageData(ast, options); | ||
minifyOptionsForAttribute.usage = null; | ||
elems.forEach(function (elem) { | ||
if (elem.isElem('style')) { | ||
if ( | ||
elem.children[0].type === 'text' || | ||
elem.children[0].type === 'cdata' | ||
) { | ||
const styleCss = elem.children[0].value; | ||
const minified = csso.minify(styleCss, minifyOptionsForStylesheet).css; | ||
// preserve cdata if necessary | ||
// TODO split cdata -> text optimisation into separate plugin | ||
if (styleCss.indexOf('>') >= 0 || styleCss.indexOf('<') >= 0) { | ||
elem.children[0].type = 'cdata'; | ||
elem.children[0].value = minified; | ||
} else { | ||
elem.children[0].type = 'text'; | ||
elem.children[0].value = minified; | ||
root: { | ||
exit: () => { | ||
/** | ||
* @type {csso.Usage} | ||
*/ | ||
const cssoUsage = {}; | ||
if (deoptimized === false || forceUsageDeoptimized === true) { | ||
if (enableTagsUsage && tagsUsage.size !== 0) { | ||
cssoUsage.tags = Array.from(tagsUsage); | ||
} | ||
if (enableIdsUsage && idsUsage.size !== 0) { | ||
cssoUsage.ids = Array.from(idsUsage); | ||
} | ||
if (enableClassesUsage && classesUsage.size !== 0) { | ||
cssoUsage.classes = Array.from(classesUsage); | ||
} | ||
} | ||
} | ||
} else { | ||
// style attribute | ||
var elemStyle = elem.attributes.style; | ||
elem.attributes.style = csso.minifyBlock( | ||
elemStyle, | ||
minifyOptionsForAttribute | ||
).css; | ||
} | ||
}); | ||
return ast; | ||
// minify style elements | ||
for (const node of styleElements) { | ||
if ( | ||
node.children[0].type === 'text' || | ||
node.children[0].type === 'cdata' | ||
) { | ||
const cssText = node.children[0].value; | ||
const minified = csso.minify(cssText, { | ||
...params, | ||
usage: cssoUsage, | ||
}).css; | ||
// preserve cdata if necessary | ||
// TODO split cdata -> text optimisation into separate plugin | ||
if (cssText.indexOf('>') >= 0 || cssText.indexOf('<') >= 0) { | ||
node.children[0].type = 'cdata'; | ||
node.children[0].value = minified; | ||
} else { | ||
node.children[0].type = 'text'; | ||
node.children[0].value = minified; | ||
} | ||
} | ||
} | ||
// minify style attributes | ||
for (const node of elementsWithStyleAttributes) { | ||
// style attribute | ||
const elemStyle = node.attributes.style; | ||
node.attributes.style = csso.minifyBlock(elemStyle, { | ||
...params, | ||
}).css; | ||
} | ||
}, | ||
}, | ||
}; | ||
}; | ||
function cloneObject(obj) { | ||
return { ...obj }; | ||
} | ||
function findStyleElems(ast) { | ||
const nodesWithStyles = []; | ||
traverse(ast, (node) => { | ||
if (node.type === 'element') { | ||
if (node.name === 'style' && node.children.length !== 0) { | ||
nodesWithStyles.push(node); | ||
} else if (node.attributes.style != null) { | ||
nodesWithStyles.push(node); | ||
} | ||
} | ||
}); | ||
return nodesWithStyles; | ||
} | ||
function shouldFilter(options, name) { | ||
if ('usage' in options === false) { | ||
return true; | ||
} | ||
if (options.usage && name in options.usage === false) { | ||
return true; | ||
} | ||
return Boolean(options.usage && options.usage[name]); | ||
} | ||
function collectUsageData(ast, options) { | ||
let safe = true; | ||
const usageData = {}; | ||
let hasData = false; | ||
const rawData = { | ||
ids: Object.create(null), | ||
classes: Object.create(null), | ||
tags: Object.create(null), | ||
}; | ||
traverse(ast, (node) => { | ||
if (node.type === 'element') { | ||
if (node.name === 'script') { | ||
safe = false; | ||
} | ||
rawData.tags[node.name] = true; | ||
if (node.attributes.id != null) { | ||
rawData.ids[node.attributes.id] = true; | ||
} | ||
if (node.attributes.class != null) { | ||
node.attributes.class | ||
.replace(/^\s+|\s+$/g, '') | ||
.split(/\s+/) | ||
.forEach((className) => { | ||
rawData.classes[className] = true; | ||
}); | ||
} | ||
if (Object.keys(node.attributes).some((name) => /^on/i.test(name))) { | ||
safe = false; | ||
} | ||
} | ||
}); | ||
if (!safe && options.usage && options.usage.force) { | ||
safe = true; | ||
} | ||
for (const [key, data] of Object.entries(rawData)) { | ||
if (shouldFilter(options, key)) { | ||
usageData[key] = Object.keys(data); | ||
hasData = true; | ||
} | ||
} | ||
return safe && hasData ? usageData : null; | ||
} |
'use strict'; | ||
exports.name = 'prefixIds'; | ||
const csstree = require('css-tree'); | ||
const { referencesProps } = require('./_collections.js'); | ||
exports.type = 'perItem'; | ||
/** | ||
* @typedef {import('../lib/types').XastElement} XastElement | ||
* @typedef {import('../lib/types').PluginInfo} PluginInfo | ||
*/ | ||
exports.type = 'visitor'; | ||
exports.name = 'prefixIds'; | ||
exports.active = false; | ||
exports.params = { | ||
delim: '__', | ||
prefixIds: true, | ||
prefixClassNames: true, | ||
}; | ||
exports.description = 'prefix IDs'; | ||
var csstree = require('css-tree'), | ||
collections = require('./_collections.js'), | ||
referencesProps = collections.referencesProps, | ||
rxId = /^#(.*)$/, // regular expression for matching an ID + extracing its name | ||
addPrefix = null; | ||
const unquote = (string) => { | ||
const first = string.charAt(0); | ||
if (first === "'" || first === '"') { | ||
if (first === string.charAt(string.length - 1)) { | ||
return string.slice(1, -1); | ||
} | ||
/** | ||
* extract basename from path | ||
* @type {(path: string) => string} | ||
*/ | ||
const getBasename = (path) => { | ||
// extract everything after latest slash or backslash | ||
const matched = path.match(/[/\\]?([^/\\]+)$/); | ||
if (matched) { | ||
return matched[1]; | ||
} | ||
return string; | ||
return ''; | ||
}; | ||
// Escapes a string for being used as ID | ||
var escapeIdentifierName = function (str) { | ||
/** | ||
* escapes a string for being used as ID | ||
* @type {(string: string) => string} | ||
*/ | ||
const escapeIdentifierName = (str) => { | ||
return str.replace(/[. ]/g, '_'); | ||
}; | ||
// Matches an #ID value, captures the ID name | ||
var matchId = function (urlVal) { | ||
var idUrlMatches = urlVal.match(rxId); | ||
if (idUrlMatches === null) { | ||
return false; | ||
} | ||
return idUrlMatches[1]; | ||
}; | ||
// Matches an url(...) value, captures the URL | ||
var matchUrl = function (val) { | ||
var urlMatches = /url\((.*?)\)/gi.exec(val); | ||
if (urlMatches === null) { | ||
return false; | ||
} | ||
return urlMatches[1]; | ||
}; | ||
// prefixes an #ID | ||
var prefixId = function (val) { | ||
var idName = matchId(val); | ||
if (!idName) { | ||
return false; | ||
} | ||
return '#' + addPrefix(idName); | ||
}; | ||
// prefixes a class attribute value | ||
const addPrefixToClassAttr = (element, name) => { | ||
/** | ||
* @type {(string: string) => string} | ||
*/ | ||
const unquote = (string) => { | ||
if ( | ||
element.attributes[name] == null || | ||
element.attributes[name].length === 0 | ||
(string.startsWith('"') && string.endsWith('"')) || | ||
(string.startsWith("'") && string.endsWith("'")) | ||
) { | ||
return; | ||
return string.slice(1, -1); | ||
} | ||
element.attributes[name] = element.attributes[name] | ||
.split(/\s+/) | ||
.map(addPrefix) | ||
.join(' '); | ||
return string; | ||
}; | ||
// prefixes an ID attribute value | ||
const addPrefixToIdAttr = (element, name) => { | ||
if ( | ||
element.attributes[name] == null || | ||
element.attributes[name].length === 0 | ||
) { | ||
return; | ||
/** | ||
* prefix an ID | ||
* @type {(prefix: string, name: string) => string} | ||
*/ | ||
const prefixId = (prefix, value) => { | ||
if (value.startsWith(prefix)) { | ||
return value; | ||
} | ||
element.attributes[name] = addPrefix(element.attributes[name]); | ||
return prefix + value; | ||
}; | ||
// prefixes a href attribute value | ||
const addPrefixToHrefAttr = (element, name) => { | ||
if ( | ||
element.attributes[name] == null || | ||
element.attributes[name].length === 0 | ||
) { | ||
return; | ||
/** | ||
* prefix an #ID | ||
* @type {(prefix: string, name: string) => string | null} | ||
*/ | ||
const prefixReference = (prefix, value) => { | ||
if (value.startsWith('#')) { | ||
return '#' + prefixId(prefix, value.slice(1)); | ||
} | ||
const idPrefixed = prefixId(element.attributes[name]); | ||
if (!idPrefixed) { | ||
return; | ||
} | ||
element.attributes[name] = idPrefixed; | ||
return null; | ||
}; | ||
// prefixes an URL attribute value | ||
const addPrefixToUrlAttr = (element, name) => { | ||
if ( | ||
element.attributes[name] == null || | ||
element.attributes[name].length === 0 | ||
) { | ||
return; | ||
} | ||
// url(...) in value | ||
const urlVal = matchUrl(element.attributes[name]); | ||
if (!urlVal) { | ||
return; | ||
} | ||
const idPrefixed = prefixId(urlVal); | ||
if (!idPrefixed) { | ||
return; | ||
} | ||
element.attributes[name] = 'url(' + idPrefixed + ')'; | ||
}; | ||
// prefixes begin/end attribute value | ||
const addPrefixToBeginEndAttr = (element, name) => { | ||
if ( | ||
element.attributes[name] == null || | ||
element.attributes[name].length === 0 | ||
) { | ||
return; | ||
} | ||
const parts = element.attributes[name].split('; ').map((val) => { | ||
val = val.trim(); | ||
if (val.endsWith('.end') || val.endsWith('.start')) { | ||
const [id, postfix] = val.split('.'); | ||
let idPrefixed = prefixId(`#${id}`); | ||
if (!idPrefixed) { | ||
return val; | ||
} | ||
idPrefixed = idPrefixed.slice(1); | ||
return `${idPrefixed}.${postfix}`; | ||
} else { | ||
return val; | ||
} | ||
}); | ||
element.attributes[name] = parts.join('; '); | ||
}; | ||
const getBasename = (path) => { | ||
// extract everything after latest slash or backslash | ||
const matched = path.match(/[/\\]([^/\\]+)$/); | ||
if (matched) { | ||
return matched[1]; | ||
} | ||
return ''; | ||
}; | ||
/** | ||
* Prefixes identifiers | ||
* | ||
* @param {Object} node node | ||
* @param {Object} opts plugin params | ||
* @param {Object} extra plugin extra information | ||
* @author strarsis <strarsis@gmail.com> | ||
* | ||
* @author strarsis <strarsis@gmail.com> | ||
* @type {import('../lib/types').Plugin<{ | ||
* prefix?: boolean | string | ((node: XastElement, info: PluginInfo) => string), | ||
* delim?: string, | ||
* prefixIds?: boolean, | ||
* prefixClassNames?: boolean, | ||
* }>} | ||
*/ | ||
exports.fn = function (node, opts, extra) { | ||
// skip subsequent passes when multipass is used | ||
if (extra.multipassCount && extra.multipassCount > 0) { | ||
return; | ||
} | ||
exports.fn = (_root, params, info) => { | ||
const { delim = '__', prefixIds = true, prefixClassNames = true } = params; | ||
// prefix, from file name or option | ||
var prefix = 'prefix'; | ||
if (opts.prefix) { | ||
if (typeof opts.prefix === 'function') { | ||
prefix = opts.prefix(node, extra); | ||
} else { | ||
prefix = opts.prefix; | ||
} | ||
} else if (opts.prefix === false) { | ||
prefix = false; | ||
} else if (extra && extra.path && extra.path.length > 0) { | ||
var filename = getBasename(extra.path); | ||
prefix = filename; | ||
} | ||
return { | ||
element: { | ||
enter: (node) => { | ||
/** | ||
* prefix, from file name or option | ||
* @type {string} | ||
*/ | ||
let prefix = 'prefix' + delim; | ||
if (typeof params.prefix === 'function') { | ||
prefix = params.prefix(node, info) + delim; | ||
} else if (typeof params.prefix === 'string') { | ||
prefix = params.prefix + delim; | ||
} else if (params.prefix === false) { | ||
prefix = ''; | ||
} else if (info.path != null && info.path.length > 0) { | ||
prefix = escapeIdentifierName(getBasename(info.path)) + delim; | ||
} | ||
// prefixes a normal value | ||
addPrefix = function (name) { | ||
if (prefix === false) { | ||
return escapeIdentifierName(name); | ||
} | ||
return escapeIdentifierName(prefix + opts.delim + name); | ||
}; | ||
// prefix id/class selectors and url() references in styles | ||
if (node.name === 'style') { | ||
// skip empty <style/> elements | ||
if (node.children.length === 0) { | ||
return; | ||
} | ||
// <style/> property values | ||
// parse styles | ||
let cssText = ''; | ||
if ( | ||
node.children[0].type === 'text' || | ||
node.children[0].type === 'cdata' | ||
) { | ||
cssText = node.children[0].value; | ||
} | ||
/** | ||
* @type {null | csstree.CssNode} | ||
*/ | ||
let cssAst = null; | ||
try { | ||
cssAst = csstree.parse(cssText, { | ||
parseValue: true, | ||
parseCustomProperty: false, | ||
}); | ||
} catch { | ||
return; | ||
} | ||
if (node.type === 'element' && node.name === 'style') { | ||
if (node.children.length === 0) { | ||
// skip empty <style/>s | ||
return; | ||
} | ||
csstree.walk(cssAst, (node) => { | ||
// #ID, .class selectors | ||
if ( | ||
(prefixIds && node.type === 'IdSelector') || | ||
(prefixClassNames && node.type === 'ClassSelector') | ||
) { | ||
node.name = prefixId(prefix, node.name); | ||
return; | ||
} | ||
// url(...) references | ||
if ( | ||
node.type === 'Url' && | ||
node.value.value && | ||
node.value.value.length > 0 | ||
) { | ||
const prefixed = prefixReference( | ||
prefix, | ||
unquote(node.value.value) | ||
); | ||
if (prefixed != null) { | ||
node.value.value = prefixed; | ||
} | ||
} | ||
}); | ||
var cssStr = ''; | ||
if (node.children[0].type === 'text' || node.children[0].type === 'cdata') { | ||
cssStr = node.children[0].value; | ||
} | ||
var cssAst = {}; | ||
try { | ||
cssAst = csstree.parse(cssStr, { | ||
parseValue: true, | ||
parseCustomProperty: false, | ||
}); | ||
} catch (parseError) { | ||
console.warn( | ||
'Warning: Parse error of styles of <style/> element, skipped. Error details: ' + | ||
parseError | ||
); | ||
return; | ||
} | ||
var idPrefixed = ''; | ||
csstree.walk(cssAst, function (node) { | ||
// #ID, .class | ||
if ( | ||
((opts.prefixIds && node.type === 'IdSelector') || | ||
(opts.prefixClassNames && node.type === 'ClassSelector')) && | ||
node.name | ||
) { | ||
node.name = addPrefix(node.name); | ||
return; | ||
} | ||
// url(...) in value | ||
if ( | ||
node.type === 'Url' && | ||
node.value.value && | ||
node.value.value.length > 0 | ||
) { | ||
idPrefixed = prefixId(unquote(node.value.value)); | ||
if (!idPrefixed) { | ||
// update styles | ||
if ( | ||
node.children[0].type === 'text' || | ||
node.children[0].type === 'cdata' | ||
) { | ||
node.children[0].value = csstree.generate(cssAst); | ||
} | ||
return; | ||
} | ||
node.value.value = idPrefixed; | ||
} | ||
}); | ||
// update <style>s | ||
node.children[0].value = csstree.generate(cssAst); | ||
return; | ||
} | ||
// prefix an ID attribute value | ||
if ( | ||
prefixIds && | ||
node.attributes.id != null && | ||
node.attributes.id.length !== 0 | ||
) { | ||
node.attributes.id = prefixId(prefix, node.attributes.id); | ||
} | ||
// element attributes | ||
// prefix a class attribute value | ||
if ( | ||
prefixClassNames && | ||
node.attributes.class != null && | ||
node.attributes.class.length !== 0 | ||
) { | ||
node.attributes.class = node.attributes.class | ||
.split(/\s+/) | ||
.map((name) => prefixId(prefix, name)) | ||
.join(' '); | ||
} | ||
if (node.type !== 'element') { | ||
return; | ||
} | ||
// prefix a href attribute value | ||
// xlink:href is deprecated, must be still supported | ||
for (const name of ['href', 'xlink:href']) { | ||
if ( | ||
node.attributes[name] != null && | ||
node.attributes[name].length !== 0 | ||
) { | ||
const prefixed = prefixReference(prefix, node.attributes[name]); | ||
if (prefixed != null) { | ||
node.attributes[name] = prefixed; | ||
} | ||
} | ||
} | ||
// Nodes | ||
// prefix an URL attribute value | ||
for (const name of referencesProps) { | ||
if ( | ||
node.attributes[name] != null && | ||
node.attributes[name].length !== 0 | ||
) { | ||
// extract id reference from url(...) value | ||
const matches = /url\((.*?)\)/gi.exec(node.attributes[name]); | ||
if (matches != null) { | ||
const value = matches[1]; | ||
const prefixed = prefixReference(prefix, value); | ||
if (prefixed != null) { | ||
node.attributes[name] = `url(${prefixed})`; | ||
} | ||
} | ||
} | ||
} | ||
if (opts.prefixIds) { | ||
// ID | ||
addPrefixToIdAttr(node, 'id'); | ||
} | ||
if (opts.prefixClassNames) { | ||
// Class | ||
addPrefixToClassAttr(node, 'class'); | ||
} | ||
// References | ||
// href | ||
addPrefixToHrefAttr(node, 'href'); | ||
// (xlink:)href (deprecated, must be still supported) | ||
addPrefixToHrefAttr(node, 'xlink:href'); | ||
// (referenceable) properties | ||
for (var referencesProp of referencesProps) { | ||
addPrefixToUrlAttr(node, referencesProp); | ||
} | ||
addPrefixToBeginEndAttr(node, 'begin'); | ||
addPrefixToBeginEndAttr(node, 'end'); | ||
// prefix begin/end attribute value | ||
for (const name of ['begin', 'end']) { | ||
if ( | ||
node.attributes[name] != null && | ||
node.attributes[name].length !== 0 | ||
) { | ||
const parts = node.attributes[name].split(/\s*;\s+/).map((val) => { | ||
if (val.endsWith('.end') || val.endsWith('.start')) { | ||
const [id, postfix] = val.split('.'); | ||
return `${prefixId(prefix, id)}.${postfix}`; | ||
} | ||
return val; | ||
}); | ||
node.attributes[name] = parts.join('; '); | ||
} | ||
} | ||
}, | ||
}, | ||
}; | ||
}; |
'use strict'; | ||
const { parseName } = require('../lib/svgo/tools.js'); | ||
const { editorNamespaces } = require('./_collections'); | ||
const { detachNodeFromParent } = require('../lib/xast.js'); | ||
const { editorNamespaces } = require('./_collections.js'); | ||
exports.type = 'visitor'; | ||
exports.name = 'removeEditorsNSData'; | ||
exports.type = 'perItem'; | ||
exports.active = true; | ||
exports.description = 'removes editors namespaces, elements and attributes'; | ||
const prefixes = []; | ||
exports.params = { | ||
additionalNamespaces: [], | ||
}; | ||
/** | ||
@@ -28,9 +19,9 @@ * Remove editors namespaces, elements and attributes. | ||
* | ||
* @param {Object} item current iteration item | ||
* @param {Object} params plugin params | ||
* @return {Boolean} if false, item will be filtered out | ||
* @author Kir Belevich | ||
* | ||
* @author Kir Belevich | ||
* @type {import('../lib/types').Plugin<{ | ||
* additionalNamespaces?: Array<string> | ||
* }>} | ||
*/ | ||
exports.fn = function (item, params) { | ||
exports.fn = (_root, params) => { | ||
let namespaces = editorNamespaces; | ||
@@ -40,30 +31,40 @@ if (Array.isArray(params.additionalNamespaces)) { | ||
} | ||
if (item.type === 'element') { | ||
if (item.isElem('svg')) { | ||
for (const [name, value] of Object.entries(item.attributes)) { | ||
const { prefix, local } = parseName(name); | ||
if (prefix === 'xmlns' && namespaces.includes(value)) { | ||
prefixes.push(local); | ||
// <svg xmlns:sodipodi=""> | ||
delete item.attributes[name]; | ||
/** | ||
* @type {Array<string>} | ||
*/ | ||
const prefixes = []; | ||
return { | ||
element: { | ||
enter: (node, parentNode) => { | ||
// collect namespace aliases from svg element | ||
if (node.name === 'svg') { | ||
for (const [name, value] of Object.entries(node.attributes)) { | ||
if (name.startsWith('xmlns:') && namespaces.includes(value)) { | ||
prefixes.push(name.slice('xmlns:'.length)); | ||
// <svg xmlns:sodipodi=""> | ||
delete node.attributes[name]; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
// <* sodipodi:*=""> | ||
for (const name of Object.keys(item.attributes)) { | ||
const { prefix } = parseName(name); | ||
if (prefixes.includes(prefix)) { | ||
delete item.attributes[name]; | ||
} | ||
} | ||
// <sodipodi:*> | ||
const { prefix } = parseName(item.name); | ||
if (prefixes.includes(prefix)) { | ||
return false; | ||
} | ||
} | ||
// remove editor attributes, for example | ||
// <* sodipodi:*=""> | ||
for (const name of Object.keys(node.attributes)) { | ||
if (name.includes(':')) { | ||
const [prefix] = name.split(':'); | ||
if (prefixes.includes(prefix)) { | ||
delete node.attributes[name]; | ||
} | ||
} | ||
} | ||
// remove editor elements, for example | ||
// <sodipodi:*> | ||
if (node.name.includes(':')) { | ||
const [prefix] = node.name.split(':'); | ||
if (prefixes.includes(prefix)) { | ||
detachNodeFromParent(node, parentNode); | ||
} | ||
} | ||
}, | ||
}, | ||
}; | ||
}; |
'use strict'; | ||
const { traverse } = require('../lib/xast.js'); | ||
const { parseName } = require('../lib/svgo/tools.js'); | ||
exports.type = 'visitor'; | ||
exports.name = 'removeUnusedNS'; | ||
exports.type = 'full'; | ||
exports.active = true; | ||
exports.description = 'removes unused namespaces declaration'; | ||
/** | ||
* Remove unused namespaces declaration. | ||
* Remove unused namespaces declaration from svg element | ||
* which are not used in elements or attributes | ||
* | ||
* @param {Object} item current iteration item | ||
* @return {Boolean} if false, item will be filtered out | ||
* @author Kir Belevich | ||
* | ||
* @author Kir Belevich | ||
* @type {import('../lib/types').Plugin<void>} | ||
*/ | ||
exports.fn = function (root) { | ||
let svgElem; | ||
const xmlnsCollection = []; | ||
exports.fn = () => { | ||
/** | ||
* Remove namespace from collection. | ||
* | ||
* @param {String} ns namescape name | ||
* @type {Set<string>} | ||
*/ | ||
function removeNSfromCollection(ns) { | ||
const pos = xmlnsCollection.indexOf(ns); | ||
// if found - remove ns from the namespaces collection | ||
if (pos > -1) { | ||
xmlnsCollection.splice(pos, 1); | ||
} | ||
} | ||
traverse(root, (node) => { | ||
if (node.type === 'element') { | ||
if (node.name === 'svg') { | ||
for (const name of Object.keys(node.attributes)) { | ||
const { prefix, local } = parseName(name); | ||
// collect namespaces | ||
if (prefix === 'xmlns' && local) { | ||
xmlnsCollection.push(local); | ||
const unusedNamespaces = new Set(); | ||
return { | ||
element: { | ||
enter: (node, parentNode) => { | ||
// collect all namespaces from svg element | ||
// (such as xmlns:xlink="http://www.w3.org/1999/xlink") | ||
if (node.name === 'svg' && parentNode.type === 'root') { | ||
for (const name of Object.keys(node.attributes)) { | ||
if (name.startsWith('xmlns:')) { | ||
const local = name.slice('xmlns:'.length); | ||
unusedNamespaces.add(local); | ||
} | ||
} | ||
} | ||
// if svg element has ns-attr | ||
if (xmlnsCollection.length) { | ||
// save svg element | ||
svgElem = node; | ||
if (unusedNamespaces.size !== 0) { | ||
// preserve namespace used in nested elements names | ||
if (node.name.includes(':')) { | ||
const [ns] = node.name.split(':'); | ||
if (unusedNamespaces.has(ns)) { | ||
unusedNamespaces.delete(ns); | ||
} | ||
} | ||
// preserve namespace used in nested elements attributes | ||
for (const name of Object.keys(node.attributes)) { | ||
if (name.includes(':')) { | ||
const [ns] = name.split(':'); | ||
unusedNamespaces.delete(ns); | ||
} | ||
} | ||
} | ||
} | ||
if (xmlnsCollection.length) { | ||
const { prefix } = parseName(node.name); | ||
// check node for the ns-attrs | ||
if (prefix) { | ||
removeNSfromCollection(prefix); | ||
}, | ||
exit: (node, parentNode) => { | ||
// remove unused namespace attributes from svg element | ||
if (node.name === 'svg' && parentNode.type === 'root') { | ||
for (const name of unusedNamespaces) { | ||
delete node.attributes[`xmlns:${name}`]; | ||
} | ||
} | ||
// check each attr for the ns-attrs | ||
for (const name of Object.keys(node.attributes)) { | ||
const { prefix } = parseName(name); | ||
removeNSfromCollection(prefix); | ||
} | ||
} | ||
} | ||
}); | ||
// remove svg element ns-attributes if they are not used even once | ||
if (xmlnsCollection.length) { | ||
for (const name of xmlnsCollection) { | ||
delete svgElem.attributes['xmlns:' + name]; | ||
} | ||
} | ||
return root; | ||
}, | ||
}, | ||
}; | ||
}; |
'use strict'; | ||
const { visit, visitSkip, detachNodeFromParent } = require('../lib/xast.js'); | ||
const { collectStylesheet, computeStyle } = require('../lib/style.js'); | ||
const { elemsGroups } = require('./_collections.js'); | ||
exports.type = 'visitor'; | ||
exports.name = 'removeUselessStrokeAndFill'; | ||
exports.type = 'perItem'; | ||
exports.active = true; | ||
exports.description = 'removes useless stroke and fill attributes'; | ||
exports.params = { | ||
stroke: true, | ||
fill: true, | ||
removeNone: false, | ||
hasStyleOrScript: false, | ||
}; | ||
var shape = require('./_collections').elemsGroups.shape, | ||
styleOrScript = ['style', 'script']; | ||
/** | ||
* Remove useless stroke and fill attrs. | ||
* | ||
* @param {Object} item current iteration item | ||
* @param {Object} params plugin params | ||
* @return {Boolean} if false, item will be filtered out | ||
* @author Kir Belevich | ||
* | ||
* @author Kir Belevich | ||
* @type {import('../lib/types').Plugin<{ | ||
* stroke?: boolean, | ||
* fill?: boolean, | ||
* removeNone?: boolean | ||
* }>} | ||
*/ | ||
exports.fn = function (item, params) { | ||
if (item.isElem(styleOrScript)) { | ||
params.hasStyleOrScript = true; | ||
exports.fn = (root, params) => { | ||
const { | ||
stroke: removeStroke = true, | ||
fill: removeFill = true, | ||
removeNone = false, | ||
} = params; | ||
// style and script elements deoptimise this plugin | ||
let hasStyleOrScript = false; | ||
visit(root, { | ||
element: { | ||
enter: (node) => { | ||
if (node.name === 'style' || node.name === 'script') { | ||
hasStyleOrScript = true; | ||
} | ||
}, | ||
}, | ||
}); | ||
if (hasStyleOrScript) { | ||
return null; | ||
} | ||
if ( | ||
!params.hasStyleOrScript && | ||
item.isElem(shape) && | ||
!item.computedAttr('id') | ||
) { | ||
var stroke = params.stroke && item.computedAttr('stroke'), | ||
fill = params.fill && !item.computedAttr('fill', 'none'); | ||
const stylesheet = collectStylesheet(root); | ||
// remove stroke* | ||
if ( | ||
params.stroke && | ||
(!stroke || | ||
stroke == 'none' || | ||
item.computedAttr('stroke-opacity', '0') || | ||
item.computedAttr('stroke-width', '0')) | ||
) { | ||
// stroke-width may affect the size of marker-end | ||
if ( | ||
item.computedAttr('stroke-width', '0') === true || | ||
item.computedAttr('marker-end') == null | ||
) { | ||
var parentStroke = item.parentNode.computedAttr('stroke'), | ||
declineStroke = parentStroke && parentStroke != 'none'; | ||
return { | ||
element: { | ||
enter: (node, parentNode) => { | ||
// id attribute deoptimise the whole subtree | ||
if (node.attributes.id != null) { | ||
return visitSkip; | ||
} | ||
if (elemsGroups.shape.includes(node.name) == false) { | ||
return; | ||
} | ||
const computedStyle = computeStyle(stylesheet, node); | ||
const stroke = computedStyle.stroke; | ||
const strokeOpacity = computedStyle['stroke-opacity']; | ||
const strokeWidth = computedStyle['stroke-width']; | ||
const markerEnd = computedStyle['marker-end']; | ||
const fill = computedStyle.fill; | ||
const fillOpacity = computedStyle['fill-opacity']; | ||
const computedParentStyle = | ||
parentNode.type === 'element' | ||
? computeStyle(stylesheet, parentNode) | ||
: null; | ||
const parentStroke = | ||
computedParentStyle == null ? null : computedParentStyle.stroke; | ||
for (const name of Object.keys(item.attributes)) { | ||
if (name.startsWith('stroke')) { | ||
delete item.attributes[name]; | ||
// remove stroke* | ||
if (removeStroke) { | ||
if ( | ||
stroke == null || | ||
(stroke.type === 'static' && stroke.value == 'none') || | ||
(strokeOpacity != null && | ||
strokeOpacity.type === 'static' && | ||
strokeOpacity.value === '0') || | ||
(strokeWidth != null && | ||
strokeWidth.type === 'static' && | ||
strokeWidth.value === '0') | ||
) { | ||
// stroke-width may affect the size of marker-end | ||
// marker is not visible when stroke-width is 0 | ||
if ( | ||
(strokeWidth != null && | ||
strokeWidth.type === 'static' && | ||
strokeWidth.value === '0') || | ||
markerEnd == null | ||
) { | ||
for (const name of Object.keys(node.attributes)) { | ||
if (name.startsWith('stroke')) { | ||
delete node.attributes[name]; | ||
} | ||
} | ||
// set explicit none to not inherit from parent | ||
if ( | ||
parentStroke != null && | ||
parentStroke.type === 'static' && | ||
parentStroke.value !== 'none' | ||
) { | ||
node.attributes.stroke = 'none'; | ||
} | ||
} | ||
} | ||
} | ||
if (declineStroke) { | ||
item.attributes.stroke = 'none'; | ||
// remove fill* | ||
if (removeFill) { | ||
if ( | ||
(fill != null && fill.type === 'static' && fill.value === 'none') || | ||
(fillOpacity != null && | ||
fillOpacity.type === 'static' && | ||
fillOpacity.value === '0') | ||
) { | ||
for (const name of Object.keys(node.attributes)) { | ||
if (name.startsWith('fill-')) { | ||
delete node.attributes[name]; | ||
} | ||
} | ||
if ( | ||
fill == null || | ||
(fill.type === 'static' && fill.value !== 'none') | ||
) { | ||
node.attributes.fill = 'none'; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
// remove fill* | ||
if (params.fill && (!fill || item.computedAttr('fill-opacity', '0'))) { | ||
for (const name of Object.keys(item.attributes)) { | ||
if (name.startsWith('fill-')) { | ||
delete item.attributes[name]; | ||
if (removeNone) { | ||
if ( | ||
(stroke == null || node.attributes.stroke === 'none') && | ||
((fill != null && | ||
fill.type === 'static' && | ||
fill.value === 'none') || | ||
node.attributes.fill === 'none') | ||
) { | ||
detachNodeFromParent(node, parentNode); | ||
} | ||
} | ||
} | ||
if (fill) { | ||
item.attributes.fill = 'none'; | ||
} | ||
} | ||
if ( | ||
params.removeNone && | ||
(!stroke || item.attributes.stroke == 'none') && | ||
(!fill || item.attributes.fill == 'none') | ||
) { | ||
return false; | ||
} | ||
} | ||
}, | ||
}, | ||
}; | ||
}; |
'use strict'; | ||
const { traverse } = require('../lib/xast.js'); | ||
const JSAPI = require('../lib/svgo/jsAPI'); | ||
/** | ||
* @typedef {import('../lib/types').XastElement} XastElement | ||
* @typedef {import('../lib/types').XastParent} XastParent | ||
* @typedef {import('../lib/types').XastNode} XastNode | ||
*/ | ||
const JSAPI = require('../lib/svgo/jsAPI.js'); | ||
exports.type = 'visitor'; | ||
exports.name = 'reusePaths'; | ||
exports.type = 'full'; | ||
exports.active = false; | ||
exports.description = | ||
@@ -22,65 +24,91 @@ 'Finds <path> elements with the same d, fill, and ' + | ||
* @author Jacob Howcroft | ||
* | ||
* @type {import('../lib/types').Plugin<void>} | ||
*/ | ||
exports.fn = function (root) { | ||
const seen = new Map(); | ||
let count = 0; | ||
const defs = []; | ||
traverse(root, (node) => { | ||
if ( | ||
node.type !== 'element' || | ||
node.name !== 'path' || | ||
node.attributes.d == null | ||
) { | ||
return; | ||
} | ||
const d = node.attributes.d; | ||
const fill = node.attributes.fill || ''; | ||
const stroke = node.attributes.stroke || ''; | ||
const key = d + ';s:' + stroke + ';f:' + fill; | ||
const hasSeen = seen.get(key); | ||
if (!hasSeen) { | ||
seen.set(key, { elem: node, reused: false }); | ||
return; | ||
} | ||
if (!hasSeen.reused) { | ||
hasSeen.reused = true; | ||
if (hasSeen.elem.attributes.id == null) { | ||
hasSeen.elem.attributes.id = 'reuse-' + count++; | ||
} | ||
defs.push(hasSeen.elem); | ||
} | ||
convertToUse(node, hasSeen.elem.attributes.id); | ||
}); | ||
if (defs.length > 0) { | ||
const defsTag = new JSAPI( | ||
{ | ||
type: 'element', | ||
name: 'defs', | ||
attributes: {}, | ||
children: [], | ||
exports.fn = () => { | ||
/** | ||
* @type {Map<string, Array<XastElement>>} | ||
*/ | ||
const paths = new Map(); | ||
return { | ||
element: { | ||
enter: (node) => { | ||
if (node.name === 'path' && node.attributes.d != null) { | ||
const d = node.attributes.d; | ||
const fill = node.attributes.fill || ''; | ||
const stroke = node.attributes.stroke || ''; | ||
const key = d + ';s:' + stroke + ';f:' + fill; | ||
let list = paths.get(key); | ||
if (list == null) { | ||
list = []; | ||
paths.set(key, list); | ||
} | ||
list.push(node); | ||
} | ||
}, | ||
root | ||
); | ||
root.children[0].spliceContent(0, 0, defsTag); | ||
for (let def of defs) { | ||
const defClone = def.clone(); | ||
delete defClone.attributes.transform; | ||
defsTag.spliceContent(0, 0, defClone); | ||
// Convert the original def to a use so the first usage isn't duplicated. | ||
def = convertToUse(def, defClone.attributes.id); | ||
delete def.attributes.id; | ||
} | ||
} | ||
return root; | ||
exit: (node, parentNode) => { | ||
if (node.name === 'svg' && parentNode.type === 'root') { | ||
/** | ||
* @type {XastElement} | ||
*/ | ||
const rawDefs = { | ||
type: 'element', | ||
name: 'defs', | ||
attributes: {}, | ||
children: [], | ||
}; | ||
/** | ||
* @type {XastElement} | ||
*/ | ||
const defsTag = new JSAPI(rawDefs, node); | ||
let index = 0; | ||
for (const list of paths.values()) { | ||
if (list.length > 1) { | ||
// add reusable path to defs | ||
/** | ||
* @type {XastElement} | ||
*/ | ||
const rawPath = { | ||
type: 'element', | ||
name: 'path', | ||
attributes: { ...list[0].attributes }, | ||
children: [], | ||
}; | ||
delete rawPath.attributes.transform; | ||
let id; | ||
if (rawPath.attributes.id == null) { | ||
id = 'reuse-' + index; | ||
index += 1; | ||
rawPath.attributes.id = id; | ||
} else { | ||
id = rawPath.attributes.id; | ||
delete list[0].attributes.id; | ||
} | ||
/** | ||
* @type {XastElement} | ||
*/ | ||
const reusablePath = new JSAPI(rawPath, defsTag); | ||
defsTag.children.push(reusablePath); | ||
// convert paths to <use> | ||
for (const pathNode of list) { | ||
pathNode.name = 'use'; | ||
pathNode.attributes['xlink:href'] = '#' + id; | ||
delete pathNode.attributes.d; | ||
delete pathNode.attributes.stroke; | ||
delete pathNode.attributes.fill; | ||
} | ||
} | ||
} | ||
if (defsTag.children.length !== 0) { | ||
if (node.attributes['xmlns:xlink'] == null) { | ||
node.attributes['xmlns:xlink'] = 'http://www.w3.org/1999/xlink'; | ||
} | ||
node.children.unshift(defsTag); | ||
} | ||
} | ||
}, | ||
}, | ||
}; | ||
}; | ||
/** */ | ||
function convertToUse(item, href) { | ||
item.renameElem('use'); | ||
delete item.attributes.d; | ||
delete item.attributes.stroke; | ||
delete item.attributes.fill; | ||
item.attributes['xlink:href'] = '#' + href; | ||
delete item.pathJS; | ||
return item; | ||
} |
'use strict'; | ||
const { parseName } = require('../lib/svgo/tools.js'); | ||
exports.type = 'visitor'; | ||
exports.name = 'sortAttrs'; | ||
exports.type = 'perItem'; | ||
exports.active = false; | ||
exports.description = 'Sort element attributes for better compression'; | ||
exports.description = 'sorts element attributes (disabled by default)'; | ||
exports.params = { | ||
order: [ | ||
'id', | ||
'width', | ||
'height', | ||
'x', | ||
'x1', | ||
'x2', | ||
'y', | ||
'y1', | ||
'y2', | ||
'cx', | ||
'cy', | ||
'r', | ||
'fill', | ||
'stroke', | ||
'marker', | ||
'd', | ||
'points', | ||
], | ||
}; | ||
/** | ||
* Sort element attributes for epic readability. | ||
* Sort element attributes for better compression | ||
* | ||
* @param {Object} item current iteration item | ||
* @param {Object} params plugin params | ||
* @author Nikolay Frantsev | ||
* | ||
* @author Nikolay Frantsev | ||
* @type {import('../lib/types').Plugin<{ | ||
* order?: Array<string> | ||
* xmlnsOrder?: 'front' | 'alphabetical' | ||
* }>} | ||
*/ | ||
exports.fn = function (item, params) { | ||
const orderlen = params.order.length + 1; | ||
const xmlnsOrder = params.xmlnsOrder || 'front'; | ||
exports.fn = (_root, params) => { | ||
const { | ||
order = [ | ||
'id', | ||
'width', | ||
'height', | ||
'x', | ||
'x1', | ||
'x2', | ||
'y', | ||
'y1', | ||
'y2', | ||
'cx', | ||
'cy', | ||
'r', | ||
'fill', | ||
'stroke', | ||
'marker', | ||
'd', | ||
'points', | ||
], | ||
xmlnsOrder = 'front', | ||
} = params; | ||
if (item.type === 'element') { | ||
const attrs = Object.entries(item.attributes); | ||
attrs.sort(([aName], [bName]) => { | ||
const { prefix: aPrefix } = parseName(aName); | ||
const { prefix: bPrefix } = parseName(bName); | ||
if (aPrefix != bPrefix) { | ||
// xmlns attributes implicitly have the prefix xmlns | ||
if (xmlnsOrder == 'front') { | ||
if (aPrefix === 'xmlns') return -1; | ||
if (bPrefix === 'xmlns') return 1; | ||
} | ||
return aPrefix < bPrefix ? -1 : 1; | ||
/** | ||
* @type {(name: string) => number} | ||
*/ | ||
const getNsPriority = (name) => { | ||
if (xmlnsOrder === 'front') { | ||
// put xmlns first | ||
if (name === 'xmlns') { | ||
return 3; | ||
} | ||
let aindex = orderlen; | ||
let bindex = orderlen; | ||
for (let i = 0; i < params.order.length; i++) { | ||
if (aName == params.order[i]) { | ||
aindex = i; | ||
} else if (aName.indexOf(params.order[i] + '-') === 0) { | ||
aindex = i + 0.5; | ||
} | ||
if (bName == params.order[i]) { | ||
bindex = i; | ||
} else if (bName.indexOf(params.order[i] + '-') === 0) { | ||
bindex = i + 0.5; | ||
} | ||
// xmlns:* attributes second | ||
if (name.startsWith('xmlns:')) { | ||
return 2; | ||
} | ||
} | ||
// other namespaces after and sort them alphabetically | ||
if (name.includes(':')) { | ||
return 1; | ||
} | ||
// other attributes | ||
return 0; | ||
}; | ||
if (aindex != bindex) { | ||
return aindex - bindex; | ||
/** | ||
* @type {(a: [string, string], b: [string, string]) => number} | ||
*/ | ||
const compareAttrs = ([aName], [bName]) => { | ||
// sort namespaces | ||
const aPriority = getNsPriority(aName); | ||
const bPriority = getNsPriority(bName); | ||
const priorityNs = bPriority - aPriority; | ||
if (priorityNs !== 0) { | ||
return priorityNs; | ||
} | ||
// extract the first part from attributes | ||
// for example "fill" from "fill" and "fill-opacity" | ||
const [aPart] = aName.split('-'); | ||
const [bPart] = bName.split('-'); | ||
// rely on alphabetical sort when the first part is the same | ||
if (aPart !== bPart) { | ||
const aInOrderFlag = order.includes(aPart) ? 1 : 0; | ||
const bInOrderFlag = order.includes(bPart) ? 1 : 0; | ||
// sort by position in order param | ||
if (aInOrderFlag === 1 && bInOrderFlag === 1) { | ||
return order.indexOf(aPart) - order.indexOf(bPart); | ||
} | ||
return aName < bName ? -1 : 1; | ||
}); | ||
// put attributes from order param before others | ||
const priorityOrder = bInOrderFlag - aInOrderFlag; | ||
if (priorityOrder !== 0) { | ||
return priorityOrder; | ||
} | ||
} | ||
// sort alphabetically | ||
return aName < bName ? -1 : 1; | ||
}; | ||
const sorted = {}; | ||
for (const [name, value] of attrs) { | ||
sorted[name] = value; | ||
} | ||
item.attributes = sorted; | ||
} | ||
return { | ||
element: { | ||
enter: (node) => { | ||
const attrs = Object.entries(node.attributes); | ||
attrs.sort(compareAttrs); | ||
/** | ||
* @type {Record<string, string>} | ||
*/ | ||
const sortedAttributes = {}; | ||
for (const [name, value] of attrs) { | ||
sortedAttributes[name] = value; | ||
} | ||
node.attributes = sortedAttributes; | ||
}, | ||
}, | ||
}; | ||
}; |
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
955085
1.01%80
1.27%14799
1.59%20
5.26%4
33.33%+ Added
- Removed
Updated