123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- 'use strict';
- var utils = require('./utils');
- var numbers = require('./numbers');
- module.exports = function makeJuiceClient(juiceClient) {
- juiceClient.ignoredPseudos = ['hover', 'active', 'focus', 'visited', 'link'];
- juiceClient.widthElements = ['TABLE', 'TD', 'TH', 'IMG'];
- juiceClient.heightElements = ['TABLE', 'TD', 'TH', 'IMG'];
- juiceClient.tableElements = ['TABLE', 'TH', 'TR', 'TD', 'CAPTION', 'COLGROUP', 'COL', 'THEAD', 'TBODY', 'TFOOT'];
- juiceClient.nonVisualElements = [ 'HEAD', 'TITLE', 'BASE', 'LINK', 'STYLE', 'META', 'SCRIPT', 'NOSCRIPT' ];
- juiceClient.styleToAttribute = {
- 'background-color': 'bgcolor',
- 'background-image': 'background',
- 'text-align': 'align',
- 'vertical-align': 'valign'
- };
- juiceClient.excludedProperties = [];
- juiceClient.juiceDocument = juiceDocument;
- juiceClient.inlineDocument = inlineDocument;
- function inlineDocument($, css, options) {
- options = options || {};
- var rules = utils.parseCSS(css);
- var editedElements = [];
- var styleAttributeName = 'style';
- var counters = {};
- if (options.styleAttributeName) {
- styleAttributeName = options.styleAttributeName;
- }
- rules.forEach(handleRule);
- editedElements.forEach(setStyleAttrs);
- if (options.inlinePseudoElements) {
- editedElements.forEach(inlinePseudoElements);
- }
- if (options.applyWidthAttributes) {
- editedElements.forEach(function(el) {
- setDimensionAttrs(el, 'width');
- });
- }
- if (options.applyHeightAttributes) {
- editedElements.forEach(function(el) {
- setDimensionAttrs(el, 'height');
- });
- }
- if (options.applyAttributesTableElements) {
- editedElements.forEach(setAttributesOnTableElements);
- }
- if (options.insertPreservedExtraCss && options.extraCss) {
- var preservedText = utils.getPreservedText(options.extraCss, {
- mediaQueries: options.preserveMediaQueries,
- fontFaces: options.preserveFontFaces,
- keyFrames: options.preserveKeyFrames
- });
- if (preservedText) {
- var $appendTo = null;
- if (options.insertPreservedExtraCss !== true) {
- $appendTo = $(options.insertPreservedExtraCss);
- } else {
- $appendTo = $('head');
- if (!$appendTo.length) { $appendTo = $('body'); }
- if (!$appendTo.length) { $appendTo = $.root(); }
- }
- $appendTo.first().append('<style>' + preservedText + '</style>');
- }
- }
- function handleRule(rule) {
- var sel = rule[0];
- var style = rule[1];
- var selector = new utils.Selector(sel);
- var parsedSelector = selector.parsed();
- if (!parsedSelector) {
- return;
- }
- var pseudoElementType = getPseudoElementType(parsedSelector);
- // skip rule if the selector has any pseudos which are ignored
- for (var i = 0; i < parsedSelector.length; ++i) {
- var subSel = parsedSelector[i];
- if (subSel.pseudos) {
- for (var j = 0; j < subSel.pseudos.length; ++j) {
- var subSelPseudo = subSel.pseudos[j];
- if (juiceClient.ignoredPseudos.indexOf(subSelPseudo.name) >= 0) {
- return;
- }
- }
- }
- }
- if (pseudoElementType) {
- var last = parsedSelector[parsedSelector.length - 1];
- var pseudos = last.pseudos;
- last.pseudos = filterElementPseudos(last.pseudos);
- sel = parsedSelector.toString();
- last.pseudos = pseudos;
- }
- var els;
- try {
- els = $(sel);
- } catch (err) {
- // skip invalid selector
- return;
- }
- els.each(function() {
- var el = this;
- if (el.name && juiceClient.nonVisualElements.indexOf(el.name.toUpperCase()) >= 0) {
- return;
- }
- if (pseudoElementType) {
- var pseudoElPropName = 'pseudo' + pseudoElementType;
- var pseudoEl = el[pseudoElPropName];
- if (!pseudoEl) {
- pseudoEl = el[pseudoElPropName] = $('<span />').get(0);
- pseudoEl.pseudoElementType = pseudoElementType;
- pseudoEl.pseudoElementParent = el;
- pseudoEl.counterProps = el.counterProps;
- el[pseudoElPropName] = pseudoEl;
- }
- el = pseudoEl;
- }
- if (!el.styleProps) {
- el.styleProps = {};
- // if the element has inline styles, fake selector with topmost specificity
- if ($(el).attr(styleAttributeName)) {
- var cssText = '* { ' + $(el).attr(styleAttributeName) + ' } ';
- addProps(utils.parseCSS(cssText)[0][1], new utils.Selector('<style>', true));
- }
- // store reference to an element we need to compile style="" attr for
- editedElements.push(el);
- }
- if (!el.counterProps) {
- el.counterProps = el.parent && el.parent.counterProps
- ? Object.create(el.parent.counterProps)
- : {};
- }
- function resetCounter(el, value) {
- var tokens = value.split(/\s+/);
- for (var j = 0; j < tokens.length; j++) {
- var counter = tokens[j];
- var resetval = parseInt(tokens[j+1], 10);
- isNaN(resetval)
- ? el.counterProps[counter] = counters[counter] = 0
- : el.counterProps[counter] = counters[tokens[j++]] = resetval;
- }
- }
- function incrementCounter(el, value) {
- var tokens = value.split(/\s+/);
- for (var j = 0; j < tokens.length; j++) {
- var counter = tokens[j];
- if (el.counterProps[counter] === undefined) {
- continue;
- }
- var incrval = parseInt(tokens[j+1], 10);
- isNaN(incrval)
- ? el.counterProps[counter] = counters[counter] += 1
- : el.counterProps[counter] = counters[tokens[j++]] += incrval;
- }
- }
- // go through the properties
- function addProps(style, selector) {
- for (var i = 0, l = style.length; i < l; i++) {
- if (style[i].type == 'property') {
- var name = style[i].name;
- var value = style[i].value;
- if (name === 'counter-reset') {
- resetCounter(el, value);
- }
- if (name === 'counter-increment') {
- incrementCounter(el, value);
- }
- var important = value.match(/!important$/) !== null;
- if (important && !options.preserveImportant) value = removeImportant(value);
- // adds line number and column number for the properties as "additionalPriority" to the
- // properties because in CSS the position directly affect the priority.
- var additionalPriority = [style[i].position.start.line, style[i].position.start.col];
- var prop = new utils.Property(name, value, selector, important ? 2 : 0, additionalPriority);
- var existing = el.styleProps[name];
- // if property name is not in the excluded properties array
- if (juiceClient.excludedProperties.indexOf(name) < 0) {
- if (existing && existing.compare(prop) === prop || !existing) {
- // deleting a property let us change the order (move it to the end in the setStyleAttrs loop)
- if (existing && existing.selector !== selector) {
- delete el.styleProps[name];
- } else if (existing) {
- // make "prop" a special composed property.
- prop.nextProp = existing;
- }
- el.styleProps[name] = prop;
- }
- }
- }
- }
- }
- addProps(style, selector);
- });
- }
- function setStyleAttrs(el) {
- var l = Object.keys(el.styleProps).length;
- var props = [];
- // Here we loop each property and make sure to "expand"
- // linked "nextProp" properties happening when the same property
- // is declared multiple times in the same selector.
- Object.keys(el.styleProps).forEach(function(key) {
- var np = el.styleProps[key];
- while (typeof np !== 'undefined') {
- props.push(np);
- np = np.nextProp;
- }
- });
- // sort properties by their originating selector's specificity so that
- // props like "padding" and "padding-bottom" are resolved as expected.
- props.sort(function(a, b) {
- return a.compareFunc(b);
- });
- var string = props
- .filter(function(prop) {
- // Content becomes the innerHTML of pseudo elements, not used as a
- // style property
- return prop.prop !== 'content';
- })
- .map(function(prop) {
- return prop.prop + ': ' + prop.value.replace(/["]/g, '\'') + ';';
- })
- .join(' ');
- if (string) {
- $(el).attr(styleAttributeName, string);
- }
- }
- function inlinePseudoElements(el) {
- if (el.pseudoElementType && el.styleProps.content) {
- var parsed = parseContent(el);
- if (parsed.img) {
- el.name = 'img';
- $(el).attr('src', parsed.img);
- } else {
- $(el).text(parsed);
- }
- var parent = el.pseudoElementParent;
- if (el.pseudoElementType === 'before') {
- $(parent).prepend(el);
- } else {
- $(parent).append(el);
- }
- }
- }
- function setDimensionAttrs(el, dimension) {
- if (!el.name) { return; }
- var elName = el.name.toUpperCase();
- if (juiceClient[dimension + 'Elements'].indexOf(elName) > -1) {
- for (var i in el.styleProps) {
- if (el.styleProps[i].prop === dimension) {
- var value = el.styleProps[i].value;
- if (options.preserveImportant) {
- value = removeImportant(value);
- }
- if (value.match(/px/)) {
- var pxSize = value.replace('px', '');
- $(el).attr(dimension, pxSize);
- return;
- }
- if (juiceClient.tableElements.indexOf(elName) > -1 && value.match(/\%/)) {
- $(el).attr(dimension, value);
- return;
- }
- }
- }
- }
- }
- function extractBackgroundUrl(value) {
- return value.indexOf('url(') !== 0
- ? value
- : value.replace(/^url\((["'])?([^"']+)\1\)$/, '$2');
- }
- function setAttributesOnTableElements(el) {
- if (!el.name) { return; }
- var elName = el.name.toUpperCase();
- var styleProps = Object.keys(juiceClient.styleToAttribute);
- if (juiceClient.tableElements.indexOf(elName) > -1) {
- for (var i in el.styleProps) {
- if (styleProps.indexOf(el.styleProps[i].prop) > -1) {
- var prop = juiceClient.styleToAttribute[el.styleProps[i].prop];
- var value = el.styleProps[i].value;
- if (options.preserveImportant) {
- value = removeImportant(value);
- }
- if (prop === 'background') {
- value = extractBackgroundUrl(value);
- }
- if (/(linear|radial)-gradient\(/i.test(value)) {
- continue;
- }
- $(el).attr(prop, value);
- }
- }
- }
- }
- }
- function removeImportant(value) {
- return value.replace(/\s*!important$/, '')
- }
- function findVariableValue(el, variable) {
- while (el) {
- if (variable in el.styleProps) {
- return el.styleProps[variable].value;
- }
- var el = el.pseudoElementParent || el.parent;
- }
- }
- function applyCounterStyle(counter, style) {
- switch (style) {
- case 'lower-roman':
- return numbers.romanize(counter).toLowerCase();
- case 'upper-roman':
- return numbers.romanize(counter);
- case 'lower-latin':
- case 'lower-alpha':
- return numbers.alphanumeric(counter).toLowerCase();
- case 'upper-latin':
- case 'upper-alpha':
- return numbers.alphanumeric(counter);
- // TODO support more counter styles
- default:
- return counter.toString();
- }
- }
- function parseContent(el) {
- var content = el.styleProps.content.value;
- if (content === 'none' || content === 'normal') {
- return '';
- }
- var imageUrlMatch = content.match(/^\s*url\s*\(\s*(.*?)\s*\)\s*$/i);
- if (imageUrlMatch) {
- var url = imageUrlMatch[1].replace(/^['"]|['"]$/g, '');
- return { img: url };
- }
- var parsed = [];
- var tokens = content.split(/['"]/);
- for (var i = 0; i < tokens.length; i++) {
- if (tokens[i] === '') continue;
- var varMatch = tokens[i].match(/var\s*\(\s*(.*?)\s*(,\s*(.*?)\s*)?\s*\)/i);
- if (varMatch) {
- var variable = findVariableValue(el, varMatch[1]) || varMatch[2];
- parsed.push(variable.replace(/^['"]|['"]$/g, ''));
- continue;
- }
- var counterMatch = tokens[i].match(/counter\s*\(\s*(.*?)\s*(,\s*(.*?)\s*)?\s*\)/i);
- if (counterMatch && counterMatch[1] in el.counterProps) {
- var counter = el.counterProps[counterMatch[1]];
- parsed.push(applyCounterStyle(counter, counterMatch[3]));
- continue;
- }
- var attrMatch = tokens[i].match(/attr\s*\(\s*(.*?)\s*\)/i);
- if (attrMatch) {
- var attr = attrMatch[1];
- parsed.push(el.pseudoElementParent
- ? el.pseudoElementParent.attribs[attr]
- : el.attribs[attr]
- );
- continue;
- }
- parsed.push(tokens[i]);
- }
- content = parsed.join('');
- // Naive unescape, assume no unicode char codes
- content = content.replace(/\\/g, '');
- return content;
- }
- // Return "before" or "after" if the given selector is a pseudo element (e.g.,
- // a::after).
- function getPseudoElementType(selector) {
- if (selector.length === 0) {
- return;
- }
- var pseudos = selector[selector.length - 1].pseudos;
- if (!pseudos) {
- return;
- }
- for (var i = 0; i < pseudos.length; i++) {
- if (isPseudoElementName(pseudos[i])) {
- return pseudos[i].name;
- }
- }
- }
- function isPseudoElementName(pseudo) {
- return pseudo.name === 'before' || pseudo.name === 'after';
- }
- function filterElementPseudos(pseudos) {
- return pseudos.filter(function(pseudo) {
- return !isPseudoElementName(pseudo);
- });
- }
- function juiceDocument($, options) {
- options = utils.getDefaultOptions(options);
- var css = extractCssFromDocument($, options);
- css += '\n' + options.extraCss;
- inlineDocument($, css, options);
- return $;
- }
- function getStylesData($, options) {
- var results = [];
- var stylesList = $('style');
- var styleDataList, styleData, styleElement;
- stylesList.each(function() {
- styleElement = this;
- // the API for Cheerio using parse5 (default) and htmlparser2 are slightly different
- // detect this by checking if .childNodes exist (as opposed to .children)
- var usingParse5 = !!styleElement.childNodes;
- styleDataList = usingParse5 ? styleElement.childNodes : styleElement.children;
- if (styleDataList.length !== 1) {
- if (options.removeStyleTags) {
- $(styleElement).remove();
- }
- return;
- }
- styleData = styleDataList[0].data;
- if (options.applyStyleTags && $(styleElement).attr('data-embed') === undefined) {
- results.push(styleData);
- }
- if (options.removeStyleTags && $(styleElement).attr('data-embed') === undefined) {
- var text = usingParse5 ? styleElement.childNodes[0].nodeValue : styleElement.children[0].data;
- var preservedText = utils.getPreservedText(text, {
- mediaQueries: options.preserveMediaQueries,
- fontFaces: options.preserveFontFaces,
- keyFrames: options.preserveKeyFrames,
- pseudos: options.preservePseudos
- }, juiceClient.ignoredPseudos);
- if (preservedText) {
- if (usingParse5) {
- styleElement.childNodes[0].nodeValue = preservedText;
- } else {
- styleElement.children[0].data = preservedText;
- }
- } else {
- $(styleElement).remove();
- }
- }
- $(styleElement).removeAttr('data-embed');
- });
- return results;
- }
- function extractCssFromDocument($, options) {
- var results = getStylesData($, options);
- var css = results.join('\n');
- return css;
- }
- return juiceClient;
- };
|