Repository: Automattic/juice
Pull request #352 (2020-03-16 07:28:19)

Support htmlparser2
We need to inline very large svgs (~100MB). Cheerio uses `parse5` by default, which is very slow and memory intensive for files of this size. Cheerio also supports (and ships with) `htmlparser2`, which has a slightly different API. This PR makes using `htmlparser2` possible by detecting which API is present.

I'm not an expert on this and I'm not sure if the test I wrote is complete enough, but from my tests the only important API difference was that `el.childNodes[0].nodeValue` in `parse5` corresponds to `el.children[0].data` in `htmlparser2`.


Patch:
diff --git a/lib/inline.js b/lib/inline.js
index 2899c04..b4d6b42 100644
--- a/lib/inline.js
+++ b/lib/inline.js
@@ -339,7 +339,10 @@ function getStylesData($, options) {
   var styleDataList, styleData, styleElement;
   stylesList.each(function() {
     styleElement = this;
-    styleDataList = styleElement.childNodes;
+    // 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) {
       return;
     }
@@ -348,14 +351,19 @@ function getStylesData($, options) {
       results.push(styleData);
     }
     if (options.removeStyleTags && $(styleElement).attr('data-embed') === undefined) {
-      var preservedText = utils.getPreservedText(styleElement.childNodes[0].nodeValue, {
+      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) {
-        styleElement.childNodes[0].nodeValue = preservedText;
+        if (usingParse5) {
+          styleElement.childNodes[0].nodeValue = preservedText;
+        } else {
+          styleElement.children[0].data = preservedText;
+        }
       } else {
         $(styleElement).remove();
       }


Test patch:
diff --git a/test/run.js b/test/run.js
index 768545d..e5a5648 100644
--- a/test/run.js
+++ b/test/run.js
@@ -5,6 +5,8 @@ var utils = require('../lib/utils');
 var basename = require('path').basename;
 var fs = require('fs');
 var assert = require('assert');
+var cheerio = require('cheerio');
+var htmlparser2 = require('htmlparser2');
 
 
 /**
@@ -25,6 +27,16 @@ it('juice(html)', function() {
   assert.equal(utils.normalizeLineEndings(actual.trim()), utils.normalizeLineEndings(expected.trim()));
 });
 
+it('juice(document) with htmlparser2', function() {
+  var dom = htmlparser2.parseDOM('<style>div{color:red;}</style><div/>');
+  var $ = cheerio.load(dom);
+
+  var expected = '<div style="color: red;"></div>';
+  juice.juiceDocument($);
+  var actual = $.html();
+  assert.equal(utils.normalizeLineEndings(actual.trim()), utils.normalizeLineEndings(expected.trim()));
+});
+
 var optionFiles = fs.readdirSync(__dirname + '/cases/juice-content');
 
 optionFiles.forEach(function(file) {
diff --git a/test/typescript/juice-tests.ts b/test/typescript/juice-tests.ts
index 13b6e85..92bcff2 100644
--- a/test/typescript/juice-tests.ts
+++ b/test/typescript/juice-tests.ts
@@ -4,6 +4,7 @@
 
 import juice = require('../../');
 import cheerio = require('cheerio');
+import htmlparser2 = require('htmlparser2');
 
 const sample = '<style>div{class: red;}</style><div></div>';
 const someHtml = '<h1 class="x">yo</h1>';
@@ -47,7 +48,10 @@ const allWebResourceOptions = {
   },
 };
 
-const cheerio$ = cheerio.load('<h1>test</h1>');
+const someMoreHtml = '<h1>test</h1>';
+const cheerio$ = cheerio.load(someMoreHtml);
+const htmlparser2dom = htmlparser2.parseDOM(someMoreHtml);
+const cheerioHtmlparser2$ = cheerio.load(htmlparser2dom);
 
 const x: string = juice(someHtml, {});
 
@@ -146,6 +150,11 @@ const c6 = juice.juiceDocument(
   cheerio$,
 );
 
+const c7 = juice.juiceDocument(
+  cheerioHtmlparser2$,
+  noOptions
+);
+
 juice.inlineContent(someHtml, someCss, noOptions);
 
 juice.inlineContent(someHtml, someCss, mostOptions);
@@ -166,6 +175,8 @@ juice.inlineDocument(cheerio$, someCss, someWebResourceOptions);
 
 juice.inlineDocument(cheerio$, someCss, allWebResourceOptions);
 
+juice.inlineDocument(cheerioHtmlparser2$, someCss, noOptions);
+
 // Global settings
 
 juice.codeBlocks['HBS'] = { start: '{{', end: '}}' };