Skip to content

Commit bf42963

Browse files
committed
Merge pull request #171 from jrit/cheerio-node-12
reapply client compatible refactoring
2 parents cd94f3a + 4136378 commit bf42963

File tree

7 files changed

+392
-380
lines changed

7 files changed

+392
-380
lines changed

History.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
1.7.0 / 2015-11-03
2+
==================
3+
4+
* Refactor to provide browser support at `juice/client`
5+
* Add option `applyHeightAttributes`
6+
* Bump dep `web-resource-inliner`
7+
18
1.6.0 / 2015-10-26
29
==================
310

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ Current CLI options:
153153

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

156+
### Running Juice in the Browser
157+
158+
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.*
156159

157160
## Credits
158161

lib/client.js

Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
"use strict";
2+
3+
var utils = require('./utils');
4+
5+
var juiceClient = function (html,options) {
6+
var $ = utils.cheerio(html, { xmlMode: options && options.xmlMode});
7+
var doc = juiceDocument($,options);
8+
9+
if (options && options.xmlMode){
10+
return doc.xml();
11+
}
12+
else {
13+
return utils.decodeEntities(doc.html());
14+
}
15+
};
16+
17+
module.exports = juiceClient;
18+
19+
juiceClient.ignoredPseudos = ['hover', 'active', 'focus', 'visited', 'link'];
20+
juiceClient.widthElements = ['TABLE', 'TD', 'IMG'];
21+
juiceClient.heightElements = ['TABLE', 'TD', 'IMG'];
22+
juiceClient.tableElements = ['TABLE', 'TD', 'TH', 'TR', 'TD', 'CAPTION', 'COLGROUP', 'COL', 'THEAD', 'TBODY', 'TFOOT'];
23+
juiceClient.nonVisualElements = [ "HEAD", "TITLE", "BASE", "LINK", "STYLE", "META", "SCRIPT", "NOSCRIPT" ];
24+
juiceClient.styleToAttribute = {
25+
'background-color': 'bgcolor',
26+
'background-image': 'background',
27+
'text-align': 'align',
28+
'vertical-align': 'valign'
29+
};
30+
31+
juiceClient.juiceDocument = juiceDocument;
32+
juiceClient.inlineDocument = inlineDocument;
33+
juiceClient.inlineContent = inlineContent;
34+
35+
function inlineDocument($, css, options) {
36+
37+
var rules = utils.parseCSS(css);
38+
var editedElements = [];
39+
40+
rules.forEach(handleRule);
41+
editedElements.forEach(setStyleAttrs);
42+
43+
if (options && options.inlinePseudoElements) {
44+
editedElements.forEach(inlinePseudoElements);
45+
}
46+
47+
if (options && options.applyWidthAttributes) {
48+
editedElements.forEach(function(el) {
49+
setDimensionAttrs(el, 'width');
50+
});
51+
}
52+
53+
if (options && options.applyHeightAttributes) {
54+
editedElements.forEach(function(el) {
55+
setDimensionAttrs(el, 'height');
56+
});
57+
}
58+
59+
if (options && options.applyAttributesTableElements) {
60+
editedElements.forEach(setAttributesOnTableElements);
61+
}
62+
63+
function handleRule(rule) {
64+
var sel = rule[0];
65+
var style = rule[1];
66+
var selector = new utils.Selector(sel);
67+
var parsedSelector = selector.parsed();
68+
var pseudoElementType = getPseudoElementType(parsedSelector);
69+
70+
// skip rule if the selector has any pseudos which are ignored
71+
for (var i = 0; i < parsedSelector.length; ++i) {
72+
var subSel = parsedSelector[i];
73+
if (subSel.pseudos) {
74+
for (var j = 0; j < subSel.pseudos.length; ++j) {
75+
var subSelPseudo = subSel.pseudos[j];
76+
if (juiceClient.ignoredPseudos.indexOf(subSelPseudo.name) >= 0) {
77+
return;
78+
}
79+
}
80+
}
81+
}
82+
83+
if (pseudoElementType) {
84+
var last = parsedSelector[parsedSelector.length - 1];
85+
var pseudos = last.pseudos;
86+
last.pseudos = filterElementPseudos(last.pseudos);
87+
sel = parsedSelector.toString();
88+
last.pseudos = pseudos;
89+
}
90+
91+
var els;
92+
try {
93+
els = $(sel);
94+
} catch (err) {
95+
// skip invalid selector
96+
return;
97+
}
98+
99+
els.each(function () {
100+
var el = this;
101+
102+
if (el.name && juiceClient.nonVisualElements.indexOf(el.name.toUpperCase()) >= 0) {
103+
return;
104+
}
105+
106+
if (pseudoElementType) {
107+
var pseudoElPropName = "pseudo" + pseudoElementType;
108+
var pseudoEl = el[pseudoElPropName];
109+
if (!pseudoEl) {
110+
pseudoEl = el[pseudoElPropName] = $("<span />").get(0);
111+
pseudoEl.pseudoElementType = pseudoElementType;
112+
pseudoEl.pseudoElementParent = el;
113+
el[pseudoElPropName] = pseudoEl;
114+
}
115+
el = pseudoEl;
116+
}
117+
118+
if (!el.styleProps) {
119+
el.styleProps = {};
120+
121+
// if the element has inline styles, fake selector with topmost specificity
122+
if ($(el).attr('style')) {
123+
var cssText = '* { ' + $(el).attr('style') + ' } ';
124+
addProps(utils.parseCSS(cssText)[0][1], utils.styleSelector);
125+
}
126+
127+
// store reference to an element we need to compile style="" attr for
128+
editedElements.push(el);
129+
}
130+
131+
// go through the properties
132+
function addProps (style, selector) {
133+
for (var i = 0, l = style.length; i < l; i++) {
134+
var name = style[i];
135+
var value = style[name] + (options && options.preserveImportant && style._importants[name] ? ' !important' : '');
136+
var sel = style._importants[name] ? utils.importantSelector : selector;
137+
var prop = new utils.Property(name, value, sel);
138+
var existing = el.styleProps[name];
139+
140+
if (existing && existing.compare(prop) === prop || !existing) {
141+
el.styleProps[name] = prop;
142+
}
143+
}
144+
}
145+
146+
addProps(style, selector);
147+
});
148+
}
149+
150+
function setStyleAttrs(el) {
151+
var props = Object.keys(el.styleProps).map(function(key) {
152+
return el.styleProps[key];
153+
});
154+
// sort properties by their originating selector's specificity so that
155+
// props like "padding" and "padding-bottom" are resolved as expected.
156+
props.sort(function(a, b) {
157+
return a.selector.specificity().join("").localeCompare(
158+
b.selector.specificity().join(""));
159+
});
160+
var string = props
161+
.filter(function(prop) {
162+
// Content becomes the innerHTML of pseudo elements, not used as a
163+
// style property
164+
return prop.prop !== "content";
165+
})
166+
.map(function(prop) {
167+
return prop.prop + ": " + prop.value.replace(/["]/g, "'") + ";";
168+
})
169+
.join(" ");
170+
if (string) {
171+
$(el).attr('style', string);
172+
}
173+
}
174+
175+
function inlinePseudoElements(el) {
176+
if (el.pseudoElementType && el.styleProps.content) {
177+
$(el).html(parseContent(el.styleProps.content.value));
178+
var parent = el.pseudoElementParent;
179+
if (el.pseudoElementType === "before") {
180+
$(parent).prepend(el);
181+
}
182+
else {
183+
$(parent).append(el);
184+
}
185+
}
186+
}
187+
188+
function setDimensionAttrs(el, dimension) {
189+
var elName = el.name.toUpperCase();
190+
if (juiceClient[dimension + 'Elements'].indexOf(elName) > -1) {
191+
for (var i in el.styleProps) {
192+
if (el.styleProps[i].prop === dimension) {
193+
if (el.styleProps[i].value.match(/px/)) {
194+
var pxSize = el.styleProps[i].value.replace('px', '');
195+
$(el).attr(dimension, pxSize);
196+
return;
197+
}
198+
if (juiceClient.tableElements.indexOf(elName) > -1 && el.styleProps[i].value.match(/\%/)) {
199+
$(el).attr(dimension, el.styleProps[i].value);
200+
return;
201+
}
202+
}
203+
}
204+
}
205+
}
206+
207+
function setAttributesOnTableElements(el) {
208+
var elName = el.name.toUpperCase(),
209+
styleProps = Object.keys(juiceClient.styleToAttribute);
210+
211+
if (juiceClient.tableElements.indexOf(elName) > -1) {
212+
for (var i in el.styleProps) {
213+
if (styleProps.indexOf(el.styleProps[i].prop) > -1) {
214+
$(el).attr(juiceClient.styleToAttribute[el.styleProps[i].prop], el.styleProps[i].value);
215+
}
216+
}
217+
}
218+
}
219+
}
220+
221+
function parseContent(content) {
222+
if (content === "none" || content === "normal") {
223+
return "";
224+
}
225+
226+
// Naive parsing, assume well-formed value
227+
content = content.slice(1, content.length - 1);
228+
// Naive unescape, assume no unicode char codes
229+
content = content.replace(/\\/g, "");
230+
return content;
231+
}
232+
233+
// Return "before" or "after" if the given selector is a pseudo element (e.g.,
234+
// a::after).
235+
function getPseudoElementType(selector) {
236+
if (selector.length === 0) {
237+
return;
238+
}
239+
240+
var pseudos = selector[selector.length - 1].pseudos;
241+
if (!pseudos) {
242+
return;
243+
}
244+
245+
for (var i = 0; i < pseudos.length; i++) {
246+
if (isPseudoElementName(pseudos[i])) {
247+
return pseudos[i].name;
248+
}
249+
}
250+
}
251+
252+
function isPseudoElementName(pseudo) {
253+
return pseudo.name === "before" || pseudo.name === "after";
254+
}
255+
256+
function filterElementPseudos(pseudos) {
257+
return pseudos.filter(function(pseudo) {
258+
return !isPseudoElementName(pseudo);
259+
});
260+
}
261+
262+
function juiceDocument($, options) {
263+
options = utils.getDefaultOptions(options);
264+
var css = extractCssFromDocument($, options);
265+
css += "\n" + options.extraCss;
266+
inlineDocument($, css, options);
267+
return $;
268+
}
269+
270+
function inlineContent(html, css, options) {
271+
var $ = utils.cheerio(html, { xmlMode: options && options.xmlMode});
272+
inlineDocument($, css, options);
273+
274+
if (options && options.xmlMode){
275+
return $.xml();
276+
}
277+
else {
278+
return utils.decodeEntities($.html());
279+
}
280+
}
281+
282+
function getStylesData($, options) {
283+
var results = [];
284+
var stylesList = $("style");
285+
var styleDataList, styleData, styleElement;
286+
stylesList.each(function () {
287+
styleElement = this;
288+
styleDataList = styleElement.childNodes;
289+
if (styleDataList.length !== 1) {
290+
return;
291+
}
292+
styleData = styleDataList[0].data;
293+
if ( options.applyStyleTags && styleElement.attribs['data-embed'] === undefined ) {
294+
results.push( styleData );
295+
}
296+
if ( options.removeStyleTags && styleElement.attribs['data-embed'] === undefined )
297+
{
298+
var preservedText = utils.getPreservedText( styleElement.childNodes[0].nodeValue, {
299+
mediaQueries: options.preserveMediaQueries,
300+
fontFaces: options.preserveFontFaces
301+
} );
302+
if ( preservedText )
303+
{
304+
styleElement.childNodes[0].nodeValue = preservedText;
305+
}
306+
else
307+
{
308+
$(styleElement).remove();
309+
}
310+
}
311+
delete styleElement.attribs['data-embed'];
312+
});
313+
return results;
314+
}
315+
316+
function extractCssFromDocument($, options) {
317+
var results = getStylesData($, options);
318+
var css = results.join("\n");
319+
return css;
320+
}

0 commit comments

Comments
 (0)