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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
975338
18
15186
295
4
+ Addedpicocolors@^1.0.0
+ Addedpicocolors@1.1.1(transitive)
- Removednanocolors@^0.1.12
- Removednanocolors@0.1.12(transitive)