diff --git a/History.md b/History.md
index bf41b80..86d29dc 100644
--- a/History.md
+++ b/History.md
@@ -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
==================
diff --git a/README.md b/README.md
index 587e115..4d6450d 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/lib/client.js b/lib/client.js
new file mode 100644
index 0000000..2cdeaf4
--- /dev/null
+++ b/lib/client.js
@@ -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] = $("").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;
+}
diff --git a/lib/juice.js b/lib/juice.js
index 9d74e76..0c658b8 100644
--- a/lib/juice.js
+++ b/lib/juice.js
@@ -12,308 +12,57 @@
*/
var utils = require('./utils');
-var Selector = require('./selector');
-var Property = require('./property');
var packageJson = require('../package');
var fs = require('fs');
var path = require('path');
-var styleSelector = new Selector('