| '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; |
| }; |