svgo
Advanced tools
Comparing version 2.7.0 to 2.8.0
@@ -18,17 +18,24 @@ 'use strict'; | ||
let config; | ||
try { | ||
// dynamic import expects file url instead of path and may fail | ||
// when windows path is provided | ||
const { default: imported } = await import(pathToFileURL(configFile)); | ||
config = imported; | ||
} catch (importError) { | ||
// TODO remove require in v3 | ||
// at the moment dynamic import may randomly fail with segfault | ||
// to workaround this for some users .cjs extension is loaded | ||
// exclusively with require | ||
if (configFile.endsWith('.cjs')) { | ||
config = require(configFile); | ||
} else { | ||
try { | ||
config = require(configFile); | ||
} catch (requireError) { | ||
// throw original error if es module is detected | ||
if (requireError.code === 'ERR_REQUIRE_ESM') { | ||
throw importError; | ||
} else { | ||
throw requireError; | ||
// dynamic import expects file url instead of path and may fail | ||
// when windows path is provided | ||
const { default: imported } = await import(pathToFileURL(configFile)); | ||
config = imported; | ||
} catch (importError) { | ||
// TODO remove require in v3 | ||
try { | ||
config = require(configFile); | ||
} catch (requireError) { | ||
// throw original error if es module is detected | ||
if (requireError.code === 'ERR_REQUIRE_ESM') { | ||
throw importError; | ||
} else { | ||
throw requireError; | ||
} | ||
} | ||
@@ -35,0 +42,0 @@ } |
@@ -9,3 +9,3 @@ 'use strict'; | ||
const { parseSvg } = require('./parser.js'); | ||
const js2svg = require('./svgo/js2svg.js'); | ||
const { stringifySvg } = require('./stringifier.js'); | ||
const { invokePlugins } = require('./svgo/plugins.js'); | ||
@@ -57,6 +57,3 @@ const JSAPI = require('./svgo/jsAPI.js'); | ||
svgjs = invokePlugins(svgjs, info, resolvedPlugins, null, globalOverrides); | ||
svgjs = js2svg(svgjs, config.js2svg); | ||
if (svgjs.error) { | ||
throw Error(svgjs.error); | ||
} | ||
svgjs = stringifySvg(svgjs, config.js2svg); | ||
if (svgjs.data.length < prevResultSize) { | ||
@@ -63,0 +60,0 @@ input = svgjs.data; |
'use strict'; | ||
const FS = require('fs'); | ||
const PATH = require('path'); | ||
const { green, red } = require('nanocolors'); | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const colors = require('picocolors'); | ||
const { loadConfig, optimize } = require('../svgo-node.js'); | ||
@@ -19,3 +19,3 @@ const pluginsMap = require('../../plugins/plugins.js'); | ||
try { | ||
return FS.lstatSync(path).isDirectory(); | ||
return fs.lstatSync(path).isDirectory(); | ||
} catch (e) { | ||
@@ -77,2 +77,4 @@ return false; | ||
.option('--show-plugins', 'Show available plugins and exit') | ||
// used by picocolors internally | ||
.option('--no-color', 'Output plain text without color') | ||
.action(action); | ||
@@ -223,3 +225,3 @@ }; | ||
? input[i] | ||
: PATH.resolve(dir, PATH.basename(input[i])); | ||
: path.resolve(dir, path.basename(input[i])); | ||
} | ||
@@ -289,3 +291,3 @@ } else if (output.length < input.length) { | ||
} | ||
return FS.promises | ||
return fs.promises | ||
.readdir(dir) | ||
@@ -338,4 +340,4 @@ .then((files) => processDirectory(config, dir, files, output)); | ||
.map((name) => ({ | ||
inputPath: PATH.resolve(dir, name), | ||
outputPath: PATH.resolve(output, name), | ||
inputPath: path.resolve(dir, name), | ||
outputPath: path.resolve(output, name), | ||
})); | ||
@@ -347,7 +349,7 @@ | ||
files | ||
.filter((name) => checkIsDir(PATH.resolve(dir, name))) | ||
.filter((name) => checkIsDir(path.resolve(dir, name))) | ||
.map((subFolderName) => { | ||
const subFolderPath = PATH.resolve(dir, subFolderName); | ||
const subFolderFiles = FS.readdirSync(subFolderPath); | ||
const subFolderOutput = PATH.resolve(output, subFolderName); | ||
const subFolderPath = path.resolve(dir, subFolderName); | ||
const subFolderFiles = fs.readdirSync(subFolderPath); | ||
const subFolderOutput = path.resolve(output, subFolderName); | ||
return getFilesDescriptions( | ||
@@ -373,3 +375,3 @@ config, | ||
function optimizeFile(config, file, output) { | ||
return FS.promises.readFile(file, 'utf8').then( | ||
return fs.promises.readFile(file, 'utf8').then( | ||
(data) => | ||
@@ -395,3 +397,3 @@ processSVGData(config, { input: 'file', path: file }, data, output, file), | ||
if (result.modernError) { | ||
console.error(red(result.modernError.toString())); | ||
console.error(colors.red(result.modernError.toString())); | ||
process.exit(1); | ||
@@ -409,3 +411,3 @@ } | ||
if (input) { | ||
console.log(`\n${PATH.basename(input)}:`); | ||
console.log(`\n${path.basename(input)}:`); | ||
} | ||
@@ -440,5 +442,5 @@ printTimeInfo(processingTime); | ||
FS.mkdirSync(PATH.dirname(output), { recursive: true }); | ||
fs.mkdirSync(path.dirname(output), { recursive: true }); | ||
return FS.promises | ||
return fs.promises | ||
.writeFile(output, data, 'utf8') | ||
@@ -468,3 +470,3 @@ .catch((error) => checkWriteFileError(input, output, data, error)); | ||
(profitPercents < 0 ? ' + ' : ' - ') + | ||
green(Math.abs(Math.round(profitPercents * 10) / 10) + '%') + | ||
colors.green(Math.abs(Math.round(profitPercents * 10) / 10) + '%') + | ||
' = ' + | ||
@@ -505,4 +507,4 @@ Math.round((outBytes / 1024) * 1000) / 1000 + | ||
if (error.code == 'EISDIR' && input) { | ||
return FS.promises.writeFile( | ||
PATH.resolve(output, PATH.basename(input)), | ||
return fs.promises.writeFile( | ||
path.resolve(output, path.basename(input)), | ||
data, | ||
@@ -522,3 +524,3 @@ 'utf8' | ||
.sort(([a], [b]) => a.localeCompare(b)) | ||
.map(([name, plugin]) => ` [ ${green(name)} ] ${plugin.description}`) | ||
.map(([name, plugin]) => ` [ ${colors.green(name)} ] ${plugin.description}`) | ||
.join('\n'); | ||
@@ -525,0 +527,0 @@ console.log('Currently available plugins:\n' + list); |
@@ -89,2 +89,18 @@ 'use strict'; | ||
} | ||
if (overrides) { | ||
for (const [pluginName, override] of Object.entries(overrides)) { | ||
if (override === true) { | ||
console.warn( | ||
`You are trying to enable ${pluginName} which is not part of preset.\n` + | ||
`Try to put it before or after preset, for example\n\n` + | ||
`plugins: [\n` + | ||
` {\n` + | ||
` name: 'preset-default',\n` + | ||
` },\n` + | ||
` 'cleanupListOfValues'\n` + | ||
`]\n` | ||
); | ||
} | ||
} | ||
} | ||
return invokePlugins(ast, info, plugins, overrides, globalOverrides); | ||
@@ -91,0 +107,0 @@ }, |
@@ -54,2 +54,31 @@ export type XastDoctype = { | ||
export type StringifyOptions = { | ||
doctypeStart?: string; | ||
doctypeEnd?: string; | ||
procInstStart?: string; | ||
procInstEnd?: string; | ||
tagOpenStart?: string; | ||
tagOpenEnd?: string; | ||
tagCloseStart?: string; | ||
tagCloseEnd?: string; | ||
tagShortStart?: string; | ||
tagShortEnd?: string; | ||
attrStart?: string; | ||
attrEnd?: string; | ||
commentStart?: string; | ||
commentEnd?: string; | ||
cdataStart?: string; | ||
cdataEnd?: string; | ||
textStart?: string; | ||
textEnd?: string; | ||
indent?: number | string; | ||
regEntities?: RegExp; | ||
regValEntities?: RegExp; | ||
encodeEntity?: (char: string) => string; | ||
pretty?: boolean; | ||
useShortTags?: boolean; | ||
eol?: 'lf' | 'crlf'; | ||
finalNewline?: boolean; | ||
}; | ||
type VisitorNode<Node> = { | ||
@@ -56,0 +85,0 @@ enter?: (node: Node, parentNode: XastParent) => void | symbol; |
{ | ||
"packageManager": "yarn@2.4.3", | ||
"name": "svgo", | ||
"version": "2.7.0", | ||
"version": "2.8.0", | ||
"description": "Nodejs-based tool for optimizing SVG vector graphics files", | ||
"license": "MIT", | ||
"keywords": [ | ||
@@ -42,5 +44,3 @@ "svgo", | ||
"main": "./lib/svgo-node.js", | ||
"bin": { | ||
"svgo": "./bin/svgo" | ||
}, | ||
"bin": "./bin/svgo", | ||
"files": [ | ||
@@ -53,4 +53,7 @@ "bin", | ||
], | ||
"engines": { | ||
"node": ">=10.13.0" | ||
}, | ||
"scripts": { | ||
"test": "NODE_OPTIONS=--experimental-vm-modules jest --maxWorkers=3 --coverage", | ||
"test": "NODE_OPTIONS=--experimental-vm-modules jest --maxWorkers=4 --coverage", | ||
"lint": "eslint --ignore-path .gitignore . && prettier --check \"**/*.js\" --ignore-path .gitignore", | ||
@@ -107,3 +110,3 @@ "fix": "eslint --ignore-path .gitignore --fix . && prettier --write \"**/*.js\" --ignore-path .gitignore", | ||
"csso": "^4.2.0", | ||
"nanocolors": "^0.1.12", | ||
"picocolors": "^1.0.0", | ||
"stable": "^0.1.8" | ||
@@ -120,4 +123,3 @@ }, | ||
"eslint": "^7.32.0", | ||
"jest": "^27.2.1", | ||
"mock-stdin": "^1.0.0", | ||
"jest": "^27.2.5", | ||
"node-fetch": "^2.6.2", | ||
@@ -130,10 +132,5 @@ "pixelmatch": "^5.2.1", | ||
"rollup-plugin-terser": "^7.0.2", | ||
"strip-ansi": "^6.0.0", | ||
"tar-stream": "^2.2.0", | ||
"typescript": "^4.4.3" | ||
}, | ||
"engines": { | ||
"node": ">=10.13.0" | ||
}, | ||
"license": "MIT" | ||
} | ||
} |
'use strict'; | ||
/** | ||
* @typedef {import('../lib/types').Specificity} Specificity | ||
* @typedef {import('../lib/types').XastElement} XastElement | ||
* @typedef {import('../lib/types').XastParent} XastParent | ||
*/ | ||
const csstree = require('css-tree'); | ||
const { querySelectorAll, closestByName } = require('../lib/xast.js'); | ||
const cssTools = require('../lib/css-tools'); | ||
// @ts-ignore not defined in @types/csso | ||
const specificity = require('csso/lib/restructure/prepare/specificity'); | ||
const stable = require('stable'); | ||
const { | ||
visitSkip, | ||
querySelectorAll, | ||
detachNodeFromParent, | ||
} = require('../lib/xast.js'); | ||
exports.type = 'visitor'; | ||
exports.name = 'inlineStyles'; | ||
exports.type = 'full'; | ||
exports.active = true; | ||
exports.description = 'inline styles (additional options)'; | ||
exports.params = { | ||
onlyMatchedOnce: true, | ||
removeMatchedSelectors: true, | ||
useMqs: ['', 'screen'], | ||
usePseudos: [''], | ||
/** | ||
* Compares two selector specificities. | ||
* extracted from https://github.com/keeganstreet/specificity/blob/master/specificity.js#L211 | ||
* | ||
* @type {(a: Specificity, b: Specificity) => number} | ||
*/ | ||
const compareSpecificity = (a, b) => { | ||
for (var i = 0; i < 4; i += 1) { | ||
if (a[i] < b[i]) { | ||
return -1; | ||
} else if (a[i] > b[i]) { | ||
return 1; | ||
} | ||
} | ||
return 0; | ||
}; | ||
exports.description = 'inline styles (additional options)'; | ||
/** | ||
@@ -41,265 +60,321 @@ * Moves + merges styles from style elements to element styles | ||
* | ||
* @param {Object} root document element | ||
* @param {Object} opts plugin params | ||
* @author strarsis <strarsis@gmail.com> | ||
* | ||
* @author strarsis <strarsis@gmail.com> | ||
* @type {import('../lib/types').Plugin<{ | ||
* onlyMatchedOnce?: boolean, | ||
* removeMatchedSelectors?: boolean, | ||
* useMqs?: Array<string>, | ||
* usePseudos?: Array<string> | ||
* }>} | ||
*/ | ||
exports.fn = function (root, opts) { | ||
// collect <style/>s | ||
var styleEls = querySelectorAll(root, 'style'); | ||
exports.fn = (root, params) => { | ||
const { | ||
onlyMatchedOnce = true, | ||
removeMatchedSelectors = true, | ||
useMqs = ['', 'screen'], | ||
usePseudos = [''], | ||
} = params; | ||
//no <styles/>s, nothing to do | ||
if (styleEls.length === 0) { | ||
return root; | ||
} | ||
/** | ||
* @type {Array<{ node: XastElement, parentNode: XastParent, cssAst: csstree.StyleSheet }>} | ||
*/ | ||
const styles = []; | ||
/** | ||
* @type {Array<{ | ||
* node: csstree.Selector, | ||
* item: csstree.ListItem<csstree.CssNode>, | ||
* rule: csstree.Rule, | ||
* matchedElements?: Array<XastElement> | ||
* }>} | ||
*/ | ||
let selectors = []; | ||
var styles = [], | ||
selectors = []; | ||
return { | ||
element: { | ||
enter: (node, parentNode) => { | ||
// skip <foreignObject /> content | ||
if (node.name === 'foreignObject') { | ||
return visitSkip; | ||
} | ||
// collect only non-empty <style /> elements | ||
if (node.name !== 'style' || node.children.length === 0) { | ||
return; | ||
} | ||
// values other than the empty string or text/css are not used | ||
if ( | ||
node.attributes.type != null && | ||
node.attributes.type !== '' && | ||
node.attributes.type !== 'text/css' | ||
) { | ||
return; | ||
} | ||
// parse css in style element | ||
let cssText = ''; | ||
for (const child of node.children) { | ||
if (child.type === 'text' || child.type === 'cdata') { | ||
cssText += child.value; | ||
} | ||
} | ||
/** | ||
* @type {null | csstree.CssNode} | ||
*/ | ||
let cssAst = null; | ||
try { | ||
cssAst = csstree.parse(cssText, { | ||
parseValue: false, | ||
parseCustomProperty: false, | ||
}); | ||
} catch { | ||
return; | ||
} | ||
if (cssAst.type === 'StyleSheet') { | ||
styles.push({ node, parentNode, cssAst }); | ||
} | ||
for (var styleEl of styleEls) { | ||
// values other than the empty string or text/css are not used | ||
if ( | ||
styleEl.attributes.type != null && | ||
styleEl.attributes.type !== '' && | ||
styleEl.attributes.type !== 'text/css' | ||
) { | ||
continue; | ||
} | ||
// skip empty <style/>s or <foreignObject> content. | ||
if ( | ||
styleEl.children.length === 0 || | ||
closestByName(styleEl, 'foreignObject') | ||
) { | ||
continue; | ||
} | ||
// collect selectors | ||
csstree.walk(cssAst, { | ||
visit: 'Selector', | ||
enter(node, item) { | ||
const atrule = this.atrule; | ||
const rule = this.rule; | ||
if (rule == null) { | ||
return; | ||
} | ||
var cssStr = cssTools.getCssStr(styleEl); | ||
// skip media queries not included into useMqs param | ||
let mq = ''; | ||
if (atrule != null) { | ||
mq = atrule.name; | ||
if (atrule.prelude != null) { | ||
mq += ` ${csstree.generate(atrule.prelude)}`; | ||
} | ||
} | ||
if (useMqs.includes(mq) === false) { | ||
return; | ||
} | ||
// collect <style/>s and their css ast | ||
var cssAst = {}; | ||
try { | ||
cssAst = csstree.parse(cssStr, { | ||
parseValue: false, | ||
parseCustomProperty: false, | ||
}); | ||
} catch (parseError) { | ||
// console.warn('Warning: Parse error of styles of <style/> element, skipped. Error details: ' + parseError); | ||
continue; | ||
} | ||
/** | ||
* @type {Array<{ | ||
* item: csstree.ListItem<csstree.CssNode>, | ||
* list: csstree.List<csstree.CssNode> | ||
* }>} | ||
*/ | ||
const pseudos = []; | ||
if (node.type === 'Selector') { | ||
node.children.each((childNode, childItem, childList) => { | ||
if ( | ||
childNode.type === 'PseudoClassSelector' || | ||
childNode.type === 'PseudoElementSelector' | ||
) { | ||
pseudos.push({ item: childItem, list: childList }); | ||
} | ||
}); | ||
} | ||
styles.push({ | ||
styleEl: styleEl, | ||
cssAst: cssAst, | ||
}); | ||
// skip pseudo classes and pseudo elements not includes into usePseudos param | ||
const pseudoSelectors = csstree.generate({ | ||
type: 'Selector', | ||
children: new csstree.List().fromArray( | ||
pseudos.map((pseudo) => pseudo.item.data) | ||
), | ||
}); | ||
if (usePseudos.includes(pseudoSelectors) === false) { | ||
return; | ||
} | ||
selectors = selectors.concat(cssTools.flattenToSelectors(cssAst)); | ||
} | ||
// remove pseudo classes and elements to allow querySelector match elements | ||
// TODO this is not very accurate since some pseudo classes like first-child | ||
// are used for selection | ||
for (const pseudo of pseudos) { | ||
pseudo.list.remove(pseudo.item); | ||
} | ||
// filter for mediaqueries to be used or without any mediaquery | ||
var selectorsMq = cssTools.filterByMqs(selectors, opts.useMqs); | ||
selectors.push({ node, item, rule }); | ||
}, | ||
}); | ||
}, | ||
}, | ||
// filter for pseudo elements to be used | ||
var selectorsPseudo = cssTools.filterByPseudos(selectorsMq, opts.usePseudos); | ||
root: { | ||
exit: () => { | ||
if (styles.length === 0) { | ||
return; | ||
} | ||
// stable sort selectors | ||
const sortedSelectors = stable(selectors, (a, b) => { | ||
const aSpecificity = specificity(a.item.data); | ||
const bSpecificity = specificity(b.item.data); | ||
return compareSpecificity(aSpecificity, bSpecificity); | ||
}).reverse(); | ||
// remove PseudoClass from its SimpleSelector for proper matching | ||
cssTools.cleanPseudos(selectorsPseudo); | ||
for (const selector of sortedSelectors) { | ||
// match selectors | ||
const selectorText = csstree.generate(selector.item.data); | ||
/** | ||
* @type {Array<XastElement>} | ||
*/ | ||
const matchedElements = []; | ||
try { | ||
for (const node of querySelectorAll(root, selectorText)) { | ||
if (node.type === 'element') { | ||
matchedElements.push(node); | ||
} | ||
} | ||
} catch (selectError) { | ||
continue; | ||
} | ||
// nothing selected | ||
if (matchedElements.length === 0) { | ||
continue; | ||
} | ||
// stable sort selectors | ||
var sortedSelectors = cssTools.sortSelectors(selectorsPseudo).reverse(); | ||
// apply styles to matched elements | ||
// skip selectors that match more than once if option onlyMatchedOnce is enabled | ||
if (onlyMatchedOnce && matchedElements.length > 1) { | ||
continue; | ||
} | ||
var selector, selectedEl; | ||
// apply <style/> to matched elements | ||
for (const selectedEl of matchedElements) { | ||
const styleDeclarationList = csstree.parse( | ||
selectedEl.attributes.style == null | ||
? '' | ||
: selectedEl.attributes.style, | ||
{ | ||
context: 'declarationList', | ||
parseValue: false, | ||
} | ||
); | ||
if (styleDeclarationList.type !== 'DeclarationList') { | ||
continue; | ||
} | ||
const styleDeclarationItems = new Map(); | ||
csstree.walk(styleDeclarationList, { | ||
visit: 'Declaration', | ||
enter(node, item) { | ||
styleDeclarationItems.set(node.property, item); | ||
}, | ||
}); | ||
// merge declarations | ||
csstree.walk(selector.rule, { | ||
visit: 'Declaration', | ||
enter(ruleDeclaration) { | ||
// existing inline styles have higher priority | ||
// no inline styles, external styles, external styles used | ||
// inline styles, external styles same priority as inline styles, inline styles used | ||
// inline styles, external styles higher priority than inline styles, external styles used | ||
const matchedItem = styleDeclarationItems.get( | ||
ruleDeclaration.property | ||
); | ||
const ruleDeclarationItem = | ||
styleDeclarationList.children.createItem(ruleDeclaration); | ||
if (matchedItem == null) { | ||
styleDeclarationList.children.append(ruleDeclarationItem); | ||
} else if ( | ||
matchedItem.data.important !== true && | ||
ruleDeclaration.important === true | ||
) { | ||
styleDeclarationList.children.replace( | ||
matchedItem, | ||
ruleDeclarationItem | ||
); | ||
styleDeclarationItems.set( | ||
ruleDeclaration.property, | ||
ruleDeclarationItem | ||
); | ||
} | ||
}, | ||
}); | ||
selectedEl.attributes.style = | ||
csstree.generate(styleDeclarationList); | ||
} | ||
// match selectors | ||
for (selector of sortedSelectors) { | ||
var selectorStr = csstree.generate(selector.item.data), | ||
selectedEls = null; | ||
try { | ||
selectedEls = querySelectorAll(root, selectorStr); | ||
} catch (selectError) { | ||
// console.warn('Warning: Syntax error when trying to select \n\n' + selectorStr + '\n\n, skipped. Error details: ' + selectError); | ||
continue; | ||
} | ||
if (selectedEls.length === 0) { | ||
// nothing selected | ||
continue; | ||
} | ||
selector.selectedEls = selectedEls; | ||
} | ||
// apply <style/> styles to matched elements | ||
for (selector of sortedSelectors) { | ||
if (!selector.selectedEls) { | ||
continue; | ||
} | ||
if ( | ||
opts.onlyMatchedOnce && | ||
selector.selectedEls !== null && | ||
selector.selectedEls.length > 1 | ||
) { | ||
// skip selectors that match more than once if option onlyMatchedOnce is enabled | ||
continue; | ||
} | ||
// apply <style/> to matched elements | ||
for (selectedEl of selector.selectedEls) { | ||
if (selector.rule === null) { | ||
continue; | ||
} | ||
const styleDeclarationList = csstree.parse( | ||
selectedEl.attributes.style == null ? '' : selectedEl.attributes.style, | ||
{ | ||
context: 'declarationList', | ||
parseValue: false, | ||
} | ||
); | ||
const styleDeclarationItems = new Map(); | ||
csstree.walk(styleDeclarationList, { | ||
visit: 'Declaration', | ||
enter(node, item) { | ||
styleDeclarationItems.set(node.property, item); | ||
}, | ||
}); | ||
// merge declarations | ||
csstree.walk(selector.rule, { | ||
visit: 'Declaration', | ||
enter(ruleDeclaration) { | ||
// existing inline styles have higher priority | ||
// no inline styles, external styles, external styles used | ||
// inline styles, external styles same priority as inline styles, inline styles used | ||
// inline styles, external styles higher priority than inline styles, external styles used | ||
const matchedItem = styleDeclarationItems.get( | ||
ruleDeclaration.property | ||
); | ||
const ruleDeclarationItem = | ||
styleDeclarationList.children.createItem(ruleDeclaration); | ||
if (matchedItem == null) { | ||
styleDeclarationList.children.append(ruleDeclarationItem); | ||
} else if ( | ||
matchedItem.data.important !== true && | ||
ruleDeclaration.important === true | ||
if ( | ||
removeMatchedSelectors && | ||
matchedElements.length !== 0 && | ||
selector.rule.prelude.type === 'SelectorList' | ||
) { | ||
styleDeclarationList.children.replace( | ||
matchedItem, | ||
ruleDeclarationItem | ||
); | ||
styleDeclarationItems.set( | ||
ruleDeclaration.property, | ||
ruleDeclarationItem | ||
); | ||
// clean up matching simple selectors if option removeMatchedSelectors is enabled | ||
selector.rule.prelude.children.remove(selector.item); | ||
} | ||
}, | ||
}); | ||
selectedEl.attributes.style = csstree.generate(styleDeclarationList); | ||
} | ||
selector.matchedElements = matchedElements; | ||
} | ||
if ( | ||
opts.removeMatchedSelectors && | ||
selector.selectedEls !== null && | ||
selector.selectedEls.length > 0 | ||
) { | ||
// clean up matching simple selectors if option removeMatchedSelectors is enabled | ||
selector.rule.prelude.children.remove(selector.item); | ||
} | ||
} | ||
// no further processing required | ||
if (removeMatchedSelectors === false) { | ||
return; | ||
} | ||
if (!opts.removeMatchedSelectors) { | ||
return root; // no further processing required | ||
} | ||
// clean up matched class + ID attribute values | ||
for (const selector of sortedSelectors) { | ||
if (selector.matchedElements == null) { | ||
continue; | ||
} | ||
// clean up matched class + ID attribute values | ||
for (selector of sortedSelectors) { | ||
if (!selector.selectedEls) { | ||
continue; | ||
} | ||
if (onlyMatchedOnce && selector.matchedElements.length > 1) { | ||
// skip selectors that match more than once if option onlyMatchedOnce is enabled | ||
continue; | ||
} | ||
if ( | ||
opts.onlyMatchedOnce && | ||
selector.selectedEls !== null && | ||
selector.selectedEls.length > 1 | ||
) { | ||
// skip selectors that match more than once if option onlyMatchedOnce is enabled | ||
continue; | ||
} | ||
for (const selectedEl of selector.matchedElements) { | ||
// class | ||
const classList = new Set( | ||
selectedEl.attributes.class == null | ||
? null | ||
: selectedEl.attributes.class.split(' ') | ||
); | ||
const firstSubSelector = selector.node.children.first(); | ||
if ( | ||
firstSubSelector != null && | ||
firstSubSelector.type === 'ClassSelector' | ||
) { | ||
classList.delete(firstSubSelector.name); | ||
} | ||
if (classList.size === 0) { | ||
delete selectedEl.attributes.class; | ||
} else { | ||
selectedEl.attributes.class = Array.from(classList).join(' '); | ||
} | ||
for (selectedEl of selector.selectedEls) { | ||
// class | ||
const classList = new Set( | ||
selectedEl.attributes.class == null | ||
? null | ||
: selectedEl.attributes.class.split(' ') | ||
); | ||
const firstSubSelector = selector.item.data.children.first(); | ||
if (firstSubSelector.type === 'ClassSelector') { | ||
classList.delete(firstSubSelector.name); | ||
} | ||
if (classList.size === 0) { | ||
delete selectedEl.attributes.class; | ||
} else { | ||
selectedEl.attributes.class = Array.from(classList).join(' '); | ||
} | ||
// ID | ||
if (firstSubSelector.type === 'IdSelector') { | ||
if (selectedEl.attributes.id === firstSubSelector.name) { | ||
delete selectedEl.attributes.id; | ||
// ID | ||
if ( | ||
firstSubSelector != null && | ||
firstSubSelector.type === 'IdSelector' | ||
) { | ||
if (selectedEl.attributes.id === firstSubSelector.name) { | ||
delete selectedEl.attributes.id; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
// clean up now empty elements | ||
for (var style of styles) { | ||
csstree.walk(style.cssAst, { | ||
visit: 'Rule', | ||
enter: function (node, item, list) { | ||
// clean up <style/> atrules without any rulesets left | ||
if ( | ||
node.type === 'Atrule' && | ||
// only Atrules containing rulesets | ||
node.block !== null && | ||
node.block.children.isEmpty() | ||
) { | ||
list.remove(item); | ||
return; | ||
} | ||
for (const style of styles) { | ||
csstree.walk(style.cssAst, { | ||
visit: 'Rule', | ||
enter: function (node, item, list) { | ||
// clean up <style/> rulesets without any css selectors left | ||
if ( | ||
node.type === 'Rule' && | ||
node.prelude.type === 'SelectorList' && | ||
node.prelude.children.isEmpty() | ||
) { | ||
list.remove(item); | ||
} | ||
}, | ||
}); | ||
// clean up <style/> rulesets without any css selectors left | ||
if (node.type === 'Rule' && node.prelude.children.isEmpty()) { | ||
list.remove(item); | ||
if (style.cssAst.children.isEmpty()) { | ||
// remove emtpy style element | ||
detachNodeFromParent(style.node, style.parentNode); | ||
} else { | ||
// update style element if any styles left | ||
const firstChild = style.node.children[0]; | ||
if (firstChild.type === 'text' || firstChild.type === 'cdata') { | ||
firstChild.value = csstree.generate(style.cssAst); | ||
} | ||
} | ||
} | ||
}, | ||
}); | ||
if (style.cssAst.children.isEmpty()) { | ||
// clean up now emtpy <style/>s | ||
var styleParentEl = style.styleEl.parentNode; | ||
styleParentEl.spliceContent( | ||
styleParentEl.children.indexOf(style.styleEl), | ||
1 | ||
); | ||
if ( | ||
styleParentEl.name === 'defs' && | ||
styleParentEl.children.length === 0 | ||
) { | ||
// also clean up now empty <def/>s | ||
var defsParentEl = styleParentEl.parentNode; | ||
defsParentEl.spliceContent( | ||
defsParentEl.children.indexOf(styleParentEl), | ||
1 | ||
); | ||
} | ||
continue; | ||
} | ||
// update existing, left over <style>s | ||
cssTools.setCssStr(style.styleEl, csstree.generate(style.cssAst)); | ||
} | ||
return root; | ||
}, | ||
}; | ||
}; |
@@ -209,11 +209,12 @@ 'use strict'; | ||
) { | ||
// 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})`; | ||
node.attributes[name] = node.attributes[name].replace( | ||
/url\((.*?)\)/gi, | ||
(match, url) => { | ||
const prefixed = prefixReference(prefix, url); | ||
if (prefixed == null) { | ||
return match; | ||
} | ||
return `url(${prefixed})`; | ||
} | ||
} | ||
); | ||
} | ||
@@ -220,0 +221,0 @@ } |
@@ -5,8 +5,5 @@ 'use strict'; | ||
exports.type = 'visitor'; | ||
exports.name = 'removeEmptyAttrs'; | ||
exports.type = 'perItem'; | ||
exports.active = true; | ||
exports.description = 'removes empty attributes'; | ||
@@ -17,19 +14,22 @@ | ||
* | ||
* @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 (item) { | ||
if (item.type === 'element') { | ||
for (const [name, value] of Object.entries(item.attributes)) { | ||
if ( | ||
value === '' && | ||
// empty conditional processing attributes prevents elements from rendering | ||
attrsGroups.conditionalProcessing.includes(name) === false | ||
) { | ||
delete item.attributes[name]; | ||
} | ||
} | ||
} | ||
exports.fn = () => { | ||
return { | ||
element: { | ||
enter: (node) => { | ||
for (const [name, value] of Object.entries(node.attributes)) { | ||
if ( | ||
value === '' && | ||
// empty conditional processing attributes prevents elements from rendering | ||
attrsGroups.conditionalProcessing.includes(name) === false | ||
) { | ||
delete node.attributes[name]; | ||
} | ||
} | ||
}, | ||
}, | ||
}; | ||
}; |
@@ -28,3 +28,4 @@ 'use strict'; | ||
delete item.attributes.xmlns; | ||
delete item.attributes['xmlns:xlink']; | ||
} | ||
}; |
@@ -66,12 +66,14 @@ <div align="center"> | ||
// enable a built-in plugin by name | ||
'builtinPluginName', | ||
'prefixIds', | ||
// or by expanded version | ||
{ | ||
name: 'builtinPluginName', | ||
name: 'prefixIds', | ||
}, | ||
// some plugins allow/require to pass options | ||
{ | ||
name: 'builtinPluginName', | ||
name: 'prefixIds', | ||
params: { | ||
optionName: 'optionValue', | ||
prefix: 'my-prefix', | ||
}, | ||
@@ -94,17 +96,20 @@ }, | ||
// customize options for plugins included in preset | ||
builtinPluginName: { | ||
optionName: 'optionValue', | ||
inlineStyles: { | ||
onlyMatchedOnce: false, | ||
}, | ||
// or disable plugins | ||
anotherBuiltinPlugin: false, | ||
removeDoctype: false, | ||
}, | ||
}, | ||
}, | ||
// Enable builtin plugin not included in preset | ||
'moreBuiltinPlugin', | ||
// Enable and configure builtin plugin not included in preset | ||
// enable builtin plugin not included in default preset | ||
'prefixIds', | ||
// enable and configure builtin plugin not included in preset | ||
{ | ||
name: 'manyBuiltInPlugin', | ||
name: 'sortAttrs', | ||
params: { | ||
optionName: 'value', | ||
xmlnsOrder: 'alphabetical', | ||
}, | ||
@@ -111,0 +116,0 @@ }, |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
975338
18
15186
295
4
+ Addedpicocolors@^1.0.0
+ Addedpicocolors@1.0.0(transitive)
- Removednanocolors@^0.1.12
- Removednanocolors@0.1.12(transitive)