Skip to content

Commit

Permalink
Merge pull request #171 from jrit/cheerio-node-12
Browse files Browse the repository at this point in the history
reapply client compatible refactoring
  • Loading branch information
jrit committed Nov 3, 2015
2 parents cd94f3a + 4136378 commit bf42963
Show file tree
Hide file tree
Showing 7 changed files with 392 additions and 380 deletions.
7 changes: 7 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
1.7.0 / 2015-11-03
==================

* Refactor to provide browser support at `juice/client`
* Add option `applyHeightAttributes`
* Bump dep `web-resource-inliner`

1.6.0 / 2015-10-26
==================

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ Current CLI options:

- `--css [filepath]` will load and inject CSS into `extraCss`.

### Running Juice in the Browser

Attempting to Browserify `require('juice')` fails because portions of Juice and its dependencies interact with the file system using the standard `require('fs')`. However, you can `require('juice/client')` via Browserify which has support for `juiceDocument`, `inlineDocument`, and `inlineContent`, but not `juiceFile`, `juiceResources`, or `inlineExternal`. *Note that automated tests are not running in the browser yet.*

## Credits

Expand Down
320 changes: 320 additions & 0 deletions lib/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
"use strict";

var utils = require('./utils');

var juiceClient = function (html,options) {
var $ = utils.cheerio(html, { xmlMode: options && options.xmlMode});
var doc = juiceDocument($,options);

if (options && options.xmlMode){
return doc.xml();
}
else {
return utils.decodeEntities(doc.html());
}
};

module.exports = juiceClient;

juiceClient.ignoredPseudos = ['hover', 'active', 'focus', 'visited', 'link'];
juiceClient.widthElements = ['TABLE', 'TD', 'IMG'];
juiceClient.heightElements = ['TABLE', 'TD', 'IMG'];
juiceClient.tableElements = ['TABLE', 'TD', '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.juiceDocument = juiceDocument;
juiceClient.inlineDocument = inlineDocument;
juiceClient.inlineContent = inlineContent;

function inlineDocument($, css, options) {

var rules = utils.parseCSS(css);
var editedElements = [];

rules.forEach(handleRule);
editedElements.forEach(setStyleAttrs);

if (options && options.inlinePseudoElements) {
editedElements.forEach(inlinePseudoElements);
}

if (options && options.applyWidthAttributes) {
editedElements.forEach(function(el) {
setDimensionAttrs(el, 'width');
});
}

if (options && options.applyHeightAttributes) {
editedElements.forEach(function(el) {
setDimensionAttrs(el, 'height');
});
}

if (options && options.applyAttributesTableElements) {
editedElements.forEach(setAttributesOnTableElements);
}

function handleRule(rule) {
var sel = rule[0];
var style = rule[1];
var selector = new utils.Selector(sel);
var parsedSelector = selector.parsed();
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;
el[pseudoElPropName] = pseudoEl;
}
el = pseudoEl;
}

if (!el.styleProps) {
el.styleProps = {};

// if the element has inline styles, fake selector with topmost specificity
if ($(el).attr('style')) {
var cssText = '* { ' + $(el).attr('style') + ' } ';
addProps(utils.parseCSS(cssText)[0][1], utils.styleSelector);
}

// store reference to an element we need to compile style="" attr for
editedElements.push(el);
}

// go through the properties
function addProps (style, selector) {
for (var i = 0, l = style.length; i < l; i++) {
var name = style[i];
var value = style[name] + (options && options.preserveImportant && style._importants[name] ? ' !important' : '');
var sel = style._importants[name] ? utils.importantSelector : selector;
var prop = new utils.Property(name, value, sel);
var existing = el.styleProps[name];

if (existing && existing.compare(prop) === prop || !existing) {
el.styleProps[name] = prop;
}
}
}

addProps(style, selector);
});
}

function setStyleAttrs(el) {
var props = Object.keys(el.styleProps).map(function(key) {
return el.styleProps[key];
});
// 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.selector.specificity().join("").localeCompare(
b.selector.specificity().join(""));
});
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('style', string);
}
}

function inlinePseudoElements(el) {
if (el.pseudoElementType && el.styleProps.content) {
$(el).html(parseContent(el.styleProps.content.value));
var parent = el.pseudoElementParent;
if (el.pseudoElementType === "before") {
$(parent).prepend(el);
}
else {
$(parent).append(el);
}
}
}

function setDimensionAttrs(el, dimension) {
var elName = el.name.toUpperCase();
if (juiceClient[dimension + 'Elements'].indexOf(elName) > -1) {
for (var i in el.styleProps) {
if (el.styleProps[i].prop === dimension) {
if (el.styleProps[i].value.match(/px/)) {
var pxSize = el.styleProps[i].value.replace('px', '');
$(el).attr(dimension, pxSize);
return;
}
if (juiceClient.tableElements.indexOf(elName) > -1 && el.styleProps[i].value.match(/\%/)) {
$(el).attr(dimension, el.styleProps[i].value);
return;
}
}
}
}
}

function setAttributesOnTableElements(el) {
var elName = el.name.toUpperCase(),
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) {
$(el).attr(juiceClient.styleToAttribute[el.styleProps[i].prop], el.styleProps[i].value);
}
}
}
}
}

function parseContent(content) {
if (content === "none" || content === "normal") {
return "";
}

// Naive parsing, assume well-formed value
content = content.slice(1, content.length - 1);
// 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 inlineContent(html, css, options) {
var $ = utils.cheerio(html, { xmlMode: options && options.xmlMode});
inlineDocument($, css, options);

if (options && options.xmlMode){
return $.xml();
}
else {
return utils.decodeEntities($.html());
}
}

function getStylesData($, options) {
var results = [];
var stylesList = $("style");
var styleDataList, styleData, styleElement;
stylesList.each(function () {
styleElement = this;
styleDataList = styleElement.childNodes;
if (styleDataList.length !== 1) {
return;
}
styleData = styleDataList[0].data;
if ( options.applyStyleTags && styleElement.attribs['data-embed'] === undefined ) {
results.push( styleData );
}
if ( options.removeStyleTags && styleElement.attribs['data-embed'] === undefined )
{
var preservedText = utils.getPreservedText( styleElement.childNodes[0].nodeValue, {
mediaQueries: options.preserveMediaQueries,
fontFaces: options.preserveFontFaces
} );
if ( preservedText )
{
styleElement.childNodes[0].nodeValue = preservedText;
}
else
{
$(styleElement).remove();
}
}
delete styleElement.attribs['data-embed'];
});
return results;
}

function extractCssFromDocument($, options) {
var results = getStylesData($, options);
var css = results.join("\n");
return css;
}
Loading

0 comments on commit bf42963

Please sign in to comment.