From b1d3bde48f54b6d1a909e3af87779fce9c8034ce Mon Sep 17 00:00:00 2001 From: Parsha Pourkhomami Date: Wed, 14 Jan 2015 23:59:36 -0800 Subject: [PATCH 1/9] Inline pseudo elements Insert elements into the document in place of pseudo elements. --- History.md | 4 ++ lib/juice.js | 91 ++++++++++++++++++++++++++++++--- test/cases/pseudo-elements.css | 29 +++++++++++ test/cases/pseudo-elements.html | 5 ++ test/cases/pseudo-elements.out | 5 ++ 5 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 test/cases/pseudo-elements.css create mode 100644 test/cases/pseudo-elements.html create mode 100644 test/cases/pseudo-elements.out diff --git a/History.md b/History.md index e8b744c..85c6874 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,7 @@ +Next Release +================== + + * Inline pseudo elements as elements 0.5.0 / 2014-09-08 ================== diff --git a/lib/juice.js b/lib/juice.js index 1853e62..5b266e7 100644 --- a/lib/juice.js +++ b/lib/juice.js @@ -65,7 +65,7 @@ function inlineDocument(document, css, options) { , editedElements = []; rules.forEach(handleRule); - editedElements.forEach(setStyleAttrs); + editedElements.forEach(inlineElementStyles); if (options && options.applyWidthAttributes) { editedElements.forEach(setWidthAttrs); @@ -74,10 +74,11 @@ function inlineDocument(document, css, options) { function handleRule(rule) { var sel = rule[0] , style = rule[1] - , selector = new Selector(sel); + , selector = new Selector(sel) + , parsedSelector = selector.parsed() + , pseudoElementType = getPseudoElementType(parsedSelector); // skip rule if the selector has any pseudos which are ignored - var parsedSelector = selector.parsed(); for (var i = 0; i < parsedSelector.length; ++i) { var subSel = parsedSelector[i]; if (subSel.pseudos) { @@ -88,6 +89,14 @@ function inlineDocument(document, css, options) { } } + 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 = document.querySelectorAll(sel); @@ -96,6 +105,19 @@ function inlineDocument(document, css, options) { return; } utils.toArray(els).forEach(function (el) { + if (pseudoElementType) { + var pseudoElPropName = "pseudo" + pseudoElementType; + var pseudoEl = el[pseudoElPropName]; + if ( ! pseudoEl) { + pseudoEl = el[pseudoElPropName] = document.createElement("span"); + pseudoEl.pseudoElementType = pseudoElementType; + pseudoEl.pseudoElementParent = el; + el["pseudo" + pseudoElementType] = pseudoEl; + editedElements.push(pseudoEl); + } + el = pseudoEl; + } + if (!el.styleProps) { el.styleProps = {} @@ -133,10 +155,12 @@ function inlineDocument(document, css, options) { }); } - function setStyleAttrs(el) { + function inlineElementStyles(el) { var style = []; for (var i in el.styleProps) { - style.push(el.styleProps[i].prop + ": " + el.styleProps[i].value.replace(/["]/g, "'") + ";"); + if (i !== "content") { + style.push(el.styleProps[i].prop + ": " + el.styleProps[i].value.replace(/["]/g, "'") + ";"); + } } // sorting will arrange styles like padding: before padding-bottom: which will preserve the expected styling style = style.sort( function ( a, b ) @@ -145,7 +169,21 @@ function inlineDocument(document, css, options) { var bProp = b.split( ':' )[0]; return ( aProp > bProp ? 1 : aProp < bProp ? -1 : 0 ); } ); - el.setAttribute('style', style.join(' ')); + + if (style.length > 0) { + el.setAttribute('style', style.join(' ')); + } + + if (el.pseudoElementType && el.styleProps.content) { + el.innerHTML = parseContent(el.styleProps.content.value); + var parent = el.pseudoElementParent; + if (el.pseudoElementType === "before") { + parent.insertBefore(el, parent.firstChild); + } + else { + parent.appendChild(el); + } + } } function setWidthAttrs(el) { @@ -161,6 +199,47 @@ function inlineDocument(document, css, options) { } } +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(document, options, callback) { assert.ok(options.url, "options.url is required"); options = getDefaultOptions(options); diff --git a/test/cases/pseudo-elements.css b/test/cases/pseudo-elements.css new file mode 100644 index 0000000..3ca8dd9 --- /dev/null +++ b/test/cases/pseudo-elements.css @@ -0,0 +1,29 @@ +a { + text-decoration: underline; +} + +a:before, +a:after { + content: "a"; +} + +* a:before { + content: '®\+'; +} + +a:after, +a::after { + font-weight: bold; +} + +a:first-child { + color: blue; +} + +b:after { + content: " "; +} + +b:after { + content: "b"; +} diff --git a/test/cases/pseudo-elements.html b/test/cases/pseudo-elements.html new file mode 100644 index 0000000..a528a19 --- /dev/null +++ b/test/cases/pseudo-elements.html @@ -0,0 +1,5 @@ + +Test + + + diff --git a/test/cases/pseudo-elements.out b/test/cases/pseudo-elements.out new file mode 100644 index 0000000..771cf51 --- /dev/null +++ b/test/cases/pseudo-elements.out @@ -0,0 +1,5 @@ + +®+Testa +b +b + From d3bd40903d5d20782b636423101ec99544e2a4a0 Mon Sep 17 00:00:00 2001 From: Parsha Pourkhomami Date: Tue, 24 Feb 2015 21:43:43 -0800 Subject: [PATCH 2/9] Add :last-child test for inlined psuedo elements --- test/cases/pseudo-elements.css | 4 ++++ test/cases/pseudo-elements.html | 2 +- test/cases/pseudo-elements.out | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/test/cases/pseudo-elements.css b/test/cases/pseudo-elements.css index 3ca8dd9..7600778 100644 --- a/test/cases/pseudo-elements.css +++ b/test/cases/pseudo-elements.css @@ -27,3 +27,7 @@ b:after { b:after { content: "b"; } + +b :last-child { + color: red; +} diff --git a/test/cases/pseudo-elements.html b/test/cases/pseudo-elements.html index a528a19..12f7053 100644 --- a/test/cases/pseudo-elements.html +++ b/test/cases/pseudo-elements.html @@ -1,5 +1,5 @@ Test - + diff --git a/test/cases/pseudo-elements.out b/test/cases/pseudo-elements.out index 771cf51..35f3219 100644 --- a/test/cases/pseudo-elements.out +++ b/test/cases/pseudo-elements.out @@ -1,5 +1,5 @@ ®+Testa b -b +b From bb2a3aa4a0091df2aa80a3350e24a23c14dfc9ef Mon Sep 17 00:00:00 2001 From: Parsha Pourkhomami Date: Thu, 26 Feb 2015 10:59:11 -0800 Subject: [PATCH 3/9] Put inlining of pseudo elements behind option --- History.md | 2 +- README.md | 1 + lib/juice.js | 14 +++++-- .../{ => juice-content}/pseudo-elements.css | 0 test/cases/juice-content/pseudo-elements.html | 40 +++++++++++++++++++ test/cases/juice-content/pseudo-elements.json | 5 +++ .../{ => juice-content}/pseudo-elements.out | 1 + test/cases/pseudo-elements.html | 5 --- 8 files changed, 58 insertions(+), 10 deletions(-) rename test/cases/{ => juice-content}/pseudo-elements.css (100%) create mode 100644 test/cases/juice-content/pseudo-elements.html create mode 100644 test/cases/juice-content/pseudo-elements.json rename test/cases/{ => juice-content}/pseudo-elements.out (99%) delete mode 100644 test/cases/pseudo-elements.html diff --git a/History.md b/History.md index e23d3b6..062eeb0 100644 --- a/History.md +++ b/History.md @@ -1,7 +1,7 @@ Next Release ================== - * Inline pseudo elements as elements + * Add option to inline pseudo elements as elements 1.0.0 / 2015-02-12 ================== diff --git a/README.md b/README.md index 26fcda3..4b2c894 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ All juice methods take an options object that can contain any of these propertie * `preserveMediaQueries` - preserves all media queries (and contained styles) within `` tags as a refinement when `removeStyleTags` is `true`. Other styles are removed. Defaults to `false`. * `applyWidthAttributes` - whether to use any CSS pixel widths to create `width` attributes on elements set in `juice.widthElements`. Defaults to `false`. * `webResources` - An options object that will be passed through to web-resource-inliner for juice functions that will get remote resources (`juiceResources` and `juiceFile`). Defaults to `{}`. + * `inlinePseudoElements` - Whether to insert pseudo elements (`::before` and `::after`) as `` into the dom. *Note*: Modifying the dom may conflict with css selectors elsewhere on the page. ### Methods diff --git a/lib/juice.js b/lib/juice.js index 59d104d..b7ed4f5 100644 --- a/lib/juice.js +++ b/lib/juice.js @@ -68,6 +68,10 @@ function inlineDocument($, css, options) { rules.forEach(handleRule); editedElements.forEach(inlineElementStyles); + if (options && options.inlinePseudoElements) { + editedElements.forEach(inlinePseudoElements); + } + if (options && options.applyWidthAttributes) { editedElements.forEach(setWidthAttrs); } @@ -174,6 +178,12 @@ function inlineDocument($, css, options) { return ( aProp > bProp ? 1 : aProp < bProp ? -1 : 0 ); } ); + if (style.length > 0) { + $(el).attr('style', style.join(' ')); + } + } + + function inlinePseudoElements(el) { if (el.pseudoElementType && el.styleProps.content) { el.html(parseContent(el.styleProps.content.value)); var parent = el.pseudoElementParent; @@ -184,10 +194,6 @@ function inlineDocument($, css, options) { $(parent).append(el); } } - - if (style.length > 0) { - $(el).attr('style', style.join(' ')); - } } function setWidthAttrs(el) { diff --git a/test/cases/pseudo-elements.css b/test/cases/juice-content/pseudo-elements.css similarity index 100% rename from test/cases/pseudo-elements.css rename to test/cases/juice-content/pseudo-elements.css diff --git a/test/cases/juice-content/pseudo-elements.html b/test/cases/juice-content/pseudo-elements.html new file mode 100644 index 0000000..0e5274f --- /dev/null +++ b/test/cases/juice-content/pseudo-elements.html @@ -0,0 +1,40 @@ + + +Test + + + diff --git a/test/cases/juice-content/pseudo-elements.json b/test/cases/juice-content/pseudo-elements.json new file mode 100644 index 0000000..1120fc0 --- /dev/null +++ b/test/cases/juice-content/pseudo-elements.json @@ -0,0 +1,5 @@ +{ + "url": "./", + "removeStyleTags": true, + "inlinePseudoElements": true +} diff --git a/test/cases/pseudo-elements.out b/test/cases/juice-content/pseudo-elements.out similarity index 99% rename from test/cases/pseudo-elements.out rename to test/cases/juice-content/pseudo-elements.out index 35f3219..38914c3 100644 --- a/test/cases/pseudo-elements.out +++ b/test/cases/juice-content/pseudo-elements.out @@ -1,4 +1,5 @@ + ®+Testa b b diff --git a/test/cases/pseudo-elements.html b/test/cases/pseudo-elements.html deleted file mode 100644 index 12f7053..0000000 --- a/test/cases/pseudo-elements.html +++ /dev/null @@ -1,5 +0,0 @@ - -Test - - - From a8c639e8c82f23904b2841a35ef67cd03816d218 Mon Sep 17 00:00:00 2001 From: Parsha Pourkhomami Date: Thu, 26 Feb 2015 12:07:50 -0800 Subject: [PATCH 4/9] Style --- lib/juice.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/juice.js b/lib/juice.js index b7ed4f5..d888661 100644 --- a/lib/juice.js +++ b/lib/juice.js @@ -116,7 +116,7 @@ function inlineDocument($, css, options) { if (pseudoElementType) { var pseudoElPropName = "pseudo" + pseudoElementType; var pseudoEl = el[pseudoElPropName]; - if ( ! pseudoEl) { + if (!pseudoEl) { pseudoEl = el[pseudoElPropName] = $(""); pseudoEl.pseudoElementType = pseudoElementType; pseudoEl.pseudoElementParent = el; @@ -230,7 +230,7 @@ function getPseudoElementType(selector) { } var pseudos = selector[selector.length - 1].pseudos; - if ( ! pseudos) { + if (!pseudos) { return; } @@ -247,7 +247,7 @@ function isPseudoElementName(pseudo) { function filterElementPseudos(pseudos) { return pseudos.filter(function(pseudo) { - return ! isPseudoElementName(pseudo); + return !isPseudoElementName(pseudo); }); } From a08aa2de5a855fbc7a31be49e6e0cf6e4e2c8953 Mon Sep 17 00:00:00 2001 From: Parsha Pourkhomami Date: Thu, 26 Feb 2015 14:11:08 -0800 Subject: [PATCH 5/9] Remove unused test file File needs to exist, but content is not used. --- test/cases/juice-content/pseudo-elements.css | 33 -------------------- 1 file changed, 33 deletions(-) diff --git a/test/cases/juice-content/pseudo-elements.css b/test/cases/juice-content/pseudo-elements.css index 7600778..e69de29 100644 --- a/test/cases/juice-content/pseudo-elements.css +++ b/test/cases/juice-content/pseudo-elements.css @@ -1,33 +0,0 @@ -a { - text-decoration: underline; -} - -a:before, -a:after { - content: "a"; -} - -* a:before { - content: '®\+'; -} - -a:after, -a::after { - font-weight: bold; -} - -a:first-child { - color: blue; -} - -b:after { - content: " "; -} - -b:after { - content: "b"; -} - -b :last-child { - color: red; -} From ff1f163ecb686398bc1abfb0ecdc58fc514a621e Mon Sep 17 00:00:00 2001 From: Parsha Pourkhomami Date: Thu, 26 Feb 2015 14:11:17 -0800 Subject: [PATCH 6/9] Add test for additional styles on pseudo element --- test/cases/juice-content/pseudo-elements.html | 1 + test/cases/juice-content/pseudo-elements.out | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/cases/juice-content/pseudo-elements.html b/test/cases/juice-content/pseudo-elements.html index 0e5274f..37bae6b 100644 --- a/test/cases/juice-content/pseudo-elements.html +++ b/test/cases/juice-content/pseudo-elements.html @@ -28,6 +28,7 @@ b:after { content: "b"; + color: green; } b :last-child { diff --git a/test/cases/juice-content/pseudo-elements.out b/test/cases/juice-content/pseudo-elements.out index 38914c3..6e805d4 100644 --- a/test/cases/juice-content/pseudo-elements.out +++ b/test/cases/juice-content/pseudo-elements.out @@ -1,6 +1,6 @@ ®+Testa -b -b +b +b From a629397da5d592f50eb3b646f29ee9bce9096a88 Mon Sep 17 00:00:00 2001 From: Parsha Pourkhomami Date: Thu, 26 Feb 2015 16:34:06 -0800 Subject: [PATCH 7/9] Fix elements being added to editedElements twice el is added to editedElements below and the guard on el.styleProps ensures that this happens only once. --- lib/juice.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/juice.js b/lib/juice.js index d888661..a354808 100644 --- a/lib/juice.js +++ b/lib/juice.js @@ -120,8 +120,7 @@ function inlineDocument($, css, options) { pseudoEl = el[pseudoElPropName] = $(""); pseudoEl.pseudoElementType = pseudoElementType; pseudoEl.pseudoElementParent = el; - el["pseudo" + pseudoElementType] = pseudoEl; - editedElements.push(pseudoEl); + el[pseudoElPropName] = pseudoEl; } el = pseudoEl; } From d39fc3c569181a5045f95c20e3beeba5bb4c045e Mon Sep 17 00:00:00 2001 From: Parsha Pourkhomami Date: Wed, 29 Apr 2015 12:35:40 -0700 Subject: [PATCH 8/9] Change function name back to setStyleAttrs Stick with upstream name, change is unnecessary. --- lib/juice.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/juice.js b/lib/juice.js index 86c7c4c..a3bdab2 100644 --- a/lib/juice.js +++ b/lib/juice.js @@ -73,7 +73,7 @@ function inlineDocument($, css, options) { , editedElements = []; rules.forEach(handleRule); - editedElements.forEach(inlineElementStyles); + editedElements.forEach(setStyleAttrs); if (options && options.inlinePseudoElements) { editedElements.forEach(inlinePseudoElements); @@ -173,7 +173,7 @@ function inlineDocument($, css, options) { }); } - function inlineElementStyles(el) { + function setStyleAttrs(el) { var props = Object.keys(el.styleProps).map(function(key) { return el.styleProps[key]; }); From 19947b4d9ac042283ec9cb65260cb2780aaf7a54 Mon Sep 17 00:00:00 2001 From: Parsha Pourkhomami Date: Wed, 29 Apr 2015 12:38:10 -0700 Subject: [PATCH 9/9] Improve note about pseudo elements in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e1ad0e..43a9084 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ All juice methods take an options object that can contain any of these propertie * `applyWidthAttributes` - whether to use any CSS pixel widths to create `width` attributes on elements set in `juice.widthElements`. Defaults to `false`. * `applyAttributesTableElements` - whether to create attributes for styles in `juice.styleToAttribute` on elements set in `juice.tableElements`. Defaults to `false`. * `webResources` - An options object that will be passed through to web-resource-inliner for juice functions that will get remote resources (`juiceResources` and `juiceFile`). Defaults to `{}`. - * `inlinePseudoElements` - Whether to insert pseudo elements (`::before` and `::after`) as `` into the dom. *Note*: Modifying the dom may conflict with css selectors elsewhere on the page. + * `inlinePseudoElements` - Whether to insert pseudo elements (`::before` and `::after`) as `` into the dom. *Note*: Inserting pseudo elements will modify the dom and may conflict with css selectors elsewhere on the page (e.g., `:last-child`). ### Methods