Repository: zestedesavoir/zmarkdown (js)

Issue:
HTML export: should we add a lazy-loading attribute to all images and iframes?
The HTML Living Standard now includes a `loading` attribute for [images](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#loading) and [iframes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#loading), allowing them to be loaded only when they are close to the viewing window. This is an easy way to reduce the initial rendering time of some huge web pages full of images and iframes - think of content packed with JSFiddles on Zeste de Savoir.

The `loading="lazy"` attribute is [now supported everywhere](https://caniuse.com/loading-lazy-attr), except for iframes on Firefox (support is coming with Firefox 121, current nightly).

Adding a `loading="lazy"` attribute to all images and iframes might therefore be a good idea. I’d be open to contributing, if this sounds like a good idea to anyone other than myself.

Pull request description:
[zmarkdown] Lazy-load images
Fixes #496 

Lazy-loading for iframes was added by commit https://github.com/zestedesavoir/zmarkdown/commit/28b2196031cefd1fb8da480bf031fff2814c7187 , this PR aims at lazy-loading all images for ZMarkdown.

Since parsing of images is done by remark directly, implementation was done using a postprocessor, which adds an attribute `loading=lazy` for all images, except emoticons. Since these are guaranteed to be lightweight and served from our servers, I considered they could be eagerly loaded (see condition in postprocessor).

As it is the case with all postprocessors, this one can be easily disabled with a configuration option (`lazyLoadImages`). Since this feature should be ignored by browser not supporting it, I feel like we do not need a major version for enforcing this setting in the default configuration.

Patch:
diff --git a/packages/zmarkdown/config/html/index.js b/packages/zmarkdown/config/html/index.js
index 9108c7c9..1c57ea04 100644
--- a/packages/zmarkdown/config/html/index.js
+++ b/packages/zmarkdown/config/html/index.js
@@ -33,6 +33,7 @@ module.exports = {
   postProcessors: {
     iframeWrappers:   require('./iframe-wrappers'),
     footnotesReorder: true,
+    lazyLoadImages:   true,
     wrapCode:         true,
   },
 }
diff --git a/packages/zmarkdown/postprocessors/html-lazy-load-images.js b/packages/zmarkdown/postprocessors/html-lazy-load-images.js
new file mode 100644
index 00000000..834880cf
--- /dev/null
+++ b/packages/zmarkdown/postprocessors/html-lazy-load-images.js
@@ -0,0 +1,15 @@
+const visit = require('unist-util-visit')
+
+module.exports = () => tree => {
+  visit(tree, node => {
+    if (node.type !== 'element' ||
+        node.tagName !== 'img' ||
+        !node.properties.src) return
+
+    // Ignore smileys, which may be eagerly loaded
+    if (node.properties.class &&
+        node.properties.class.includes('smiley')) return
+
+    node.properties.loading = 'lazy'
+  })
+}
diff --git a/packages/zmarkdown/renderers/html.js b/packages/zmarkdown/renderers/html.js
index f9460088..04ecc712 100644
--- a/packages/zmarkdown/renderers/html.js
+++ b/packages/zmarkdown/renderers/html.js
@@ -18,9 +18,10 @@ const defaultStringifierList = {
 require('katex/dist/contrib/mhchem')
 
 const postProcessorList = {
-  wrapCode:         require('../postprocessors/html-wrap-code'),
-  iframeWrappers:   require('../postprocessors/html-iframe-wrappers'),
   footnotesReorder: require('../postprocessors/html-footnotes-reorder'),
+  iframeWrappers:   require('../postprocessors/html-iframe-wrappers'),
+  lazyLoadImages:   require('../postprocessors/html-lazy-load-images'),
+  wrapCode:         require('../postprocessors/html-wrap-code'),
 }
 
 module.exports = (tokenizer, config) => {


Test patch:
diff --git a/packages/zmarkdown/__tests__/__snapshots__/html-suite.test.js.snap b/packages/zmarkdown/__tests__/__snapshots__/html-suite.test.js.snap
index 4cff6da0..768872b9 100644
--- a/packages/zmarkdown/__tests__/__snapshots__/html-suite.test.js.snap
+++ b/packages/zmarkdown/__tests__/__snapshots__/html-suite.test.js.snap
@@ -84,13 +84,13 @@ exports[`code highlight special cases supports linenostart 1`] = `
 `;
 
 exports[`images become figures: does not apply when a caption is present 1`] = `
-"<figure><img src=\\"http://example.com\\" alt=\\"foo\\"><figcaption>foo</figcaption></figure>
-<figure><img src=\\"http://example.com\\"><figcaption>Caption</figcaption></figure>
-<figure><img src=\\"http://example.com\\" alt=\\"foo\\"><figcaption>Caption</figcaption></figure>
-<figure><img src=\\"http://example.com\\"><figcaption></figcaption></figure>"
+"<figure><img src=\\"http://example.com\\" alt=\\"foo\\" loading=\\"lazy\\"><figcaption>foo</figcaption></figure>
+<figure><img src=\\"http://example.com\\" loading=\\"lazy\\"><figcaption>Caption</figcaption></figure>
+<figure><img src=\\"http://example.com\\" alt=\\"foo\\" loading=\\"lazy\\"><figcaption>Caption</figcaption></figure>
+<figure><img src=\\"http://example.com\\" loading=\\"lazy\\"><figcaption></figcaption></figure>"
 `;
 
-exports[`images become figures: works with only an image 1`] = `"<figure><img src=\\"http://blabla.fr\\" alt=\\"wrapped into _figure_\\"><figcaption>wrapped into _figure_</figcaption></figure>"`;
+exports[`images become figures: works with only an image 1`] = `"<figure><img src=\\"http://blabla.fr\\" alt=\\"wrapped into _figure_\\" loading=\\"lazy\\"><figcaption>wrapped into _figure_</figcaption></figure>"`;
 
 exports[`pedantic mode disabled unordered lists markers 1`] = `
 "<ul>
diff --git a/packages/zmarkdown/__tests__/__snapshots__/legacy-suite.test.js.snap b/packages/zmarkdown/__tests__/__snapshots__/legacy-suite.test.js.snap
index 631c4733..8ed60ada 100644
--- a/packages/zmarkdown/__tests__/__snapshots__/legacy-suite.test.js.snap
+++ b/packages/zmarkdown/__tests__/__snapshots__/legacy-suite.test.js.snap
@@ -56,9 +56,9 @@ Here's an inline \\\\externalLink{link}{http://zestedesavoir.com/script?foo=1\\\
 
 exports[`basic renders angle-links-and-img.txt 1`] = `
 "<p><a href=\\"simple%20link\\" title=\\"my title\\">link</a>
-<img src=\\"http://example.com/image.jpg\\" alt=\\"image\\">
+<img src=\\"http://example.com/image.jpg\\" alt=\\"image\\" loading=\\"lazy\\">
 <a href=\\"http://example.com/(()((())923)(\\">link</a>
-<img src=\\"link(()))(\\" alt=\\"image\\"></p>"
+<img src=\\"link(()))(\\" alt=\\"image\\" loading=\\"lazy\\"></p>"
 `;
 
 exports[`basic renders angle-links-and-img.txt 2`] = `
@@ -4159,15 +4159,15 @@ xxx xxx xxx xxx xxx xxx xxx xxx"
 `;
 
 exports[`misc renders brackets-in-img-title.txt 1`] = `
-"<p><img src=\\"local-img.jpg\\" alt=\\"alt\\">
-<img src=\\"local-img.jpg\\" alt=\\"alt\\">
-<img src=\\"local-img.jpg\\" alt=\\"alt\\" title=\\"normal title\\"></p>
-<p><img src=\\"local-img.jpg\\" alt=\\"alt\\" title=\\"(just title in brackets)\\">
-<img src=\\"local-img.jpg\\" alt=\\"alt\\" title=\\"title with brackets (I think)\\"></p>
-<p><img src=\\"local-img.jpg\\" alt=\\"alt\\" title=\\"(\\">
-<img src=\\"local-img.jpg\\" alt=\\"alt\\" title=\\"(open only\\">
-<img src=\\"local-img.jpg\\" alt=\\"alt\\" title=\\")\\">
-<img src=\\"local-img.jpg\\" alt=\\"alt\\" title=\\"close only)\\"></p>"
+"<p><img src=\\"local-img.jpg\\" alt=\\"alt\\" loading=\\"lazy\\">
+<img src=\\"local-img.jpg\\" alt=\\"alt\\" loading=\\"lazy\\">
+<img src=\\"local-img.jpg\\" alt=\\"alt\\" title=\\"normal title\\" loading=\\"lazy\\"></p>
+<p><img src=\\"local-img.jpg\\" alt=\\"alt\\" title=\\"(just title in brackets)\\" loading=\\"lazy\\">
+<img src=\\"local-img.jpg\\" alt=\\"alt\\" title=\\"title with brackets (I think)\\" loading=\\"lazy\\"></p>
+<p><img src=\\"local-img.jpg\\" alt=\\"alt\\" title=\\"(\\" loading=\\"lazy\\">
+<img src=\\"local-img.jpg\\" alt=\\"alt\\" title=\\"(open only\\" loading=\\"lazy\\">
+<img src=\\"local-img.jpg\\" alt=\\"alt\\" title=\\")\\" loading=\\"lazy\\">
+<img src=\\"local-img.jpg\\" alt=\\"alt\\" title=\\"close only)\\" loading=\\"lazy\\"></p>"
 `;
 
 exports[`misc renders code-first-line.txt 1`] = `
@@ -4401,8 +4401,8 @@ exports[`misc renders h1.txt 2`] = `
 `;
 
 exports[`misc renders image.txt 1`] = `
-"<figure><img src=\\"http://humane_man.jpg\\" alt=\\"Poster\\" title=\\"The most humane man.\\"><figcaption>Poster</figcaption></figure>
-<p><img src=\\"http://humane_man.jpg\\" alt=\\"Poster\\" title=\\"The most humane man.\\"></p>
+"<figure><img src=\\"http://humane_man.jpg\\" alt=\\"Poster\\" title=\\"The most humane man.\\" loading=\\"lazy\\"><figcaption>Poster</figcaption></figure>
+<p><img src=\\"http://humane_man.jpg\\" alt=\\"Poster\\" title=\\"The most humane man.\\" loading=\\"lazy\\"></p>
 <figure><img src=\\"\\" alt=\\"Blank\\"><figcaption>Blank</figcaption></figure>"
 `;
 
@@ -4419,7 +4419,7 @@ exports[`misc renders image.txt 2`] = `
 \\\\image{}[Blank]"
 `;
 
-exports[`misc renders image_in_links.txt 1`] = `"<p><a href=\\"path/to/image.png\\"><img src=\\"path/to/img_thumb.png\\" alt=\\"altname\\"></a></p>"`;
+exports[`misc renders image_in_links.txt 1`] = `"<p><a href=\\"path/to/image.png\\"><img src=\\"path/to/img_thumb.png\\" alt=\\"altname\\" loading=\\"lazy\\"></a></p>"`;
 
 exports[`misc renders image_in_links.txt 2`] = `"\\\\externalLink{\\\\image{path/to/img_thumb.png}}{path/to/image.png}"`;
 
@@ -6482,7 +6482,7 @@ exports[`zds renders emoticons.txt 1`] = `
 <p>Citation</p>
 </blockquote>
 <p><img src=\\"/static/smileys/svg/heureux.svg\\" alt=\\":D\\" class=\\"smiley\\"> Ce n'est pas une légende</p>
-<figure><img src=\\"https://zestedesavoir.com/media/galleries/3014/bee33fae-2216-463a-8b85-d1d9efe2c374.png\\" alt=\\"toto\\"><figcaption>toto</figcaption></figure>
+<figure><img src=\\"https://zestedesavoir.com/media/galleries/3014/bee33fae-2216-463a-8b85-d1d9efe2c374.png\\" alt=\\"toto\\" loading=\\"lazy\\"><figcaption>toto</figcaption></figure>
 <p><img src=\\"/static/smileys/svg/heureux.svg\\" alt=\\":D\\" class=\\"smiley\\"> ce n'est pas une légende non plus</p>"
 `;
 
diff --git a/packages/zmarkdown/__tests__/__snapshots__/server.test.js.snap b/packages/zmarkdown/__tests__/__snapshots__/server.test.js.snap
index fcf0591c..ffa18ff3 100644
--- a/packages/zmarkdown/__tests__/__snapshots__/server.test.js.snap
+++ b/packages/zmarkdown/__tests__/__snapshots__/server.test.js.snap
@@ -29,8 +29,8 @@ exports[`HTML endpoint produces statistics when configured 1`] = `
 "<p>7 chars</p>
 <h3 id=\\"13-chars-here\\">13 chars here<a aria-hidden=\\"true\\" tabindex=\\"-1\\" href=\\"#13-chars-here\\"><span class=\\"icon icon-link\\"></span></a></h3>
 <p><a href=\\"https.//github.com/zestedesavoir/zmarkdown\\">13 chars here</a></p>
-<figure><img src=\\"https.//github.com/zestedesavoir/zmarkdown\\" alt=\\"13 chars here\\"><figcaption>13 chars here</figcaption></figure>
-<figure><img src=\\"https.//github.com/zestedesavoir/zmarkdown\\" alt=\\"no chars here\\"><figcaption>13 chars here</figcaption></figure>"
+<figure><img src=\\"https.//github.com/zestedesavoir/zmarkdown\\" alt=\\"13 chars here\\" loading=\\"lazy\\"><figcaption>13 chars here</figcaption></figure>
+<figure><img src=\\"https.//github.com/zestedesavoir/zmarkdown\\" alt=\\"no chars here\\" loading=\\"lazy\\"><figcaption>13 chars here</figcaption></figure>"
 `;
 
 exports[`LaTeX endpoint accepts POSTed markdown 1`] = `