You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
245 lines
7.7 KiB
245 lines
7.7 KiB
'use strict'; |
|
|
|
exports.type = 'full'; |
|
|
|
exports.active = true; |
|
|
|
exports.params = { |
|
onlyMatchedOnce: true, |
|
removeMatchedSelectors: true, |
|
useMqs: ['', 'screen'], |
|
usePseudos: [''] |
|
}; |
|
|
|
exports.description = 'inline styles (additional options)'; |
|
|
|
|
|
var csstree = require('css-tree'), |
|
cssTools = require('../lib/css-tools'); |
|
|
|
/** |
|
* Moves + merges styles from style elements to element styles |
|
* |
|
* Options |
|
* onlyMatchedOnce (default: true) |
|
* inline only selectors that match once |
|
* |
|
* removeMatchedSelectors (default: true) |
|
* clean up matched selectors, |
|
* leave selectors that hadn't matched |
|
* |
|
* useMqs (default: ['', 'screen']) |
|
* what media queries to be used |
|
* empty string element for styles outside media queries |
|
* |
|
* usePseudos (default: ['']) |
|
* what pseudo-classes/-elements to be used |
|
* empty string element for all non-pseudo-classes and/or -elements |
|
* |
|
* @param {Object} document document element |
|
* @param {Object} opts plugin params |
|
* |
|
* @author strarsis <strarsis@gmail.com> |
|
*/ |
|
exports.fn = function(document, opts) { |
|
|
|
// collect <style/>s |
|
var styleEls = document.querySelectorAll('style'); |
|
|
|
//no <styles/>s, nothing to do |
|
if (styleEls === null) { |
|
return document; |
|
} |
|
|
|
var styles = [], |
|
selectors = []; |
|
|
|
for (var styleEl of styleEls) { |
|
if (styleEl.isEmpty() || styleEl.closestElem('foreignObject')) { |
|
// skip empty <style/>s or <foreignObject> content. |
|
continue; |
|
} |
|
|
|
var cssStr = cssTools.getCssStr(styleEl); |
|
|
|
// 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; |
|
} |
|
|
|
styles.push({ |
|
styleEl: styleEl, |
|
cssAst: cssAst |
|
}); |
|
|
|
selectors = selectors.concat(cssTools.flattenToSelectors(cssAst)); |
|
} |
|
|
|
|
|
// filter for mediaqueries to be used or without any mediaquery |
|
var selectorsMq = cssTools.filterByMqs(selectors, opts.useMqs); |
|
|
|
|
|
// filter for pseudo elements to be used |
|
var selectorsPseudo = cssTools.filterByPseudos(selectorsMq, opts.usePseudos); |
|
|
|
// remove PseudoClass from its SimpleSelector for proper matching |
|
cssTools.cleanPseudos(selectorsPseudo); |
|
|
|
|
|
// stable sort selectors |
|
var sortedSelectors = cssTools.sortSelectors(selectorsPseudo).reverse(); |
|
|
|
|
|
var selector, |
|
selectedEl; |
|
|
|
// match selectors |
|
for (selector of sortedSelectors) { |
|
var selectorStr = csstree.generate(selector.item.data), |
|
selectedEls = null; |
|
|
|
try { |
|
selectedEls = document.querySelectorAll(selectorStr); |
|
} catch (selectError) { |
|
if (selectError.constructor === SyntaxError) { |
|
// console.warn('Warning: Syntax error when trying to select \n\n' + selectorStr + '\n\n, skipped. Error details: ' + selectError); |
|
continue; |
|
} |
|
throw selectError; |
|
} |
|
|
|
if (selectedEls === null) { |
|
// 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; |
|
} |
|
|
|
// merge declarations |
|
csstree.walk(selector.rule, {visit: 'Declaration', enter: function(styleCsstreeDeclaration) { |
|
|
|
// 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 |
|
var styleDeclaration = cssTools.csstreeToStyleDeclaration(styleCsstreeDeclaration); |
|
if (selectedEl.style.getPropertyValue(styleDeclaration.name) !== null && |
|
selectedEl.style.getPropertyPriority(styleDeclaration.name) >= styleDeclaration.priority) { |
|
return; |
|
} |
|
selectedEl.style.setProperty(styleDeclaration.name, styleDeclaration.value, styleDeclaration.priority); |
|
}}); |
|
} |
|
|
|
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); |
|
} |
|
} |
|
|
|
|
|
if (!opts.removeMatchedSelectors) { |
|
return document; // no further processing required |
|
} |
|
|
|
|
|
// clean up matched class + ID attribute values |
|
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; |
|
} |
|
|
|
for (selectedEl of selector.selectedEls) { |
|
// class |
|
var firstSubSelector = selector.item.data.children.first(); |
|
if(firstSubSelector.type === 'ClassSelector') { |
|
selectedEl.class.remove(firstSubSelector.name); |
|
} |
|
// clean up now empty class attributes |
|
if(typeof selectedEl.class.item(0) === 'undefined') { |
|
selectedEl.removeAttr('class'); |
|
} |
|
|
|
// ID |
|
if(firstSubSelector.type === 'IdSelector') { |
|
selectedEl.removeAttr('id', firstSubSelector.name); |
|
} |
|
} |
|
} |
|
|
|
|
|
// 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; |
|
} |
|
|
|
// 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()) { |
|
// clean up now emtpy <style/>s |
|
var styleParentEl = style.styleEl.parentNode; |
|
styleParentEl.spliceContent(styleParentEl.content.indexOf(style.styleEl), 1); |
|
|
|
if (styleParentEl.elem === 'defs' && |
|
styleParentEl.content.length === 0) { |
|
// also clean up now empty <def/>s |
|
var defsParentEl = styleParentEl.parentNode; |
|
defsParentEl.spliceContent(defsParentEl.content.indexOf(styleParentEl), 1); |
|
} |
|
|
|
continue; |
|
} |
|
|
|
|
|
// update existing, left over <style>s |
|
cssTools.setCssStr(style.styleEl, csstree.generate(style.cssAst)); |
|
} |
|
|
|
|
|
return document; |
|
};
|
|
|