svgo
Advanced tools
Comparing version 2.5.0 to 2.6.0
'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
955085
80
14799
20
+ Added@trysound/sax@0.2.0(transitive)
- Removed@trysound/sax@0.1.1(transitive)
Updated@trysound/sax@0.2.0