Skip to content

Commit

Permalink
Add support for lazy loading images (#2211)
Browse files Browse the repository at this point in the history
* Add optional decoding="async" loading="lazy" for img

In theory, they can make the page load faster and show content faster.

There’s one problem: CommonMark allows arbitrary inline elements in alt text.
If I want to get the correct alt text, I need to match every inline event.

I think most people will only use plain text, so I only match Event::Text.

* Add very basic test for img

This is the reason why we should use plain text when lazy_async_image is enabled.

* Explain lazy_async_image in documentation

* Add test with empty alt and special characters

I totaly forgot one can leave the alt text empty.
I thought I need to eliminate the alt attribute in that case,
but actually empty alt text is better than not having an alt attribute at all:
https://www.w3.org/TR/WCAG20-TECHS/H67.html
https://www.boia.org/blog/images-that-dont-need-alternative-text-still-need-alt-attributes
Thus I will leave the empty alt text.

Another test is added to ensure alt text is properly escaped.
I will remove the redundant escaping code after this commit.

* Remove manually escaping alt text

After removing the if-else inside the arm of Event::Text(text),
the alt text is still escaped.
Indeed they are redundant.

* Use insta for snapshot testing

`cargo insta review` looks cool!

I wanted to dedup the cases variable,
but my Rust skill is not good enough to declare a global vector.
  • Loading branch information
sinofp authored May 6, 2023
1 parent 1321a83 commit b5a90db
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 4 deletions.
3 changes: 3 additions & 0 deletions components/config/src/config/markup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ pub struct Markdown {
/// The compiled extra themes into a theme set
#[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are need
pub extra_theme_set: Arc<Option<ThemeSet>>,
/// Add loading="lazy" decoding="async" to img tags. When turned on, the alt text must be plain text. Defaults to false
pub lazy_async_image: bool,
}

impl Markdown {
Expand Down Expand Up @@ -204,6 +206,7 @@ impl Default for Markdown {
extra_syntaxes_and_themes: vec![],
extra_syntax_set: None,
extra_theme_set: Arc::new(None),
lazy_async_image: false,
}
}
}
32 changes: 28 additions & 4 deletions components/markdown/src/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ pub fn markdown_to_html(

let mut stop_next_end_p = false;

let lazy_async_image = context.config.markdown.lazy_async_image;

let mut opts = Options::empty();
let mut has_summary = false;
opts.insert(Options::ENABLE_TABLES);
Expand Down Expand Up @@ -387,13 +389,35 @@ pub fn markdown_to_html(
events.push(Event::Html("</code></pre>\n".into()));
}
Event::Start(Tag::Image(link_type, src, title)) => {
if is_colocated_asset_link(&src) {
let link = if is_colocated_asset_link(&src) {
let link = format!("{}{}", context.current_page_permalink, &*src);
events.push(Event::Start(Tag::Image(link_type, link.into(), title)));
link.into()
} else {
events.push(Event::Start(Tag::Image(link_type, src, title)));
}
src
};

events.push(if lazy_async_image {
let mut img_before_alt: String = "<img src=\"".to_string();
cmark::escape::escape_href(&mut img_before_alt, &link)
.expect("Could not write to buffer");
if !title.is_empty() {
img_before_alt
.write_str("\" title=\"")
.expect("Could not write to buffer");
cmark::escape::escape_href(&mut img_before_alt, &title)
.expect("Could not write to buffer");
}
img_before_alt.write_str("\" alt=\"").expect("Could not write to buffer");
Event::Html(img_before_alt.into())
} else {
Event::Start(Tag::Image(link_type, link, title))
});
}
Event::End(Tag::Image(..)) => events.push(if lazy_async_image {
Event::Html("\" loading=\"lazy\" decoding=\"async\" />".into())
} else {
event
}),
Event::Start(Tag::Link(link_type, link, title)) if link.is_empty() => {
error = Some(Error::msg("There is a link that is missing a URL"));
events.push(Event::Start(Tag::Link(link_type, "#".into(), title)));
Expand Down
33 changes: 33 additions & 0 deletions components/markdown/tests/img.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
mod common;
use config::Config;

#[test]
fn can_transform_image() {
let cases = vec![
"![haha](https://example.com/abc.jpg)",
"![](https://example.com/abc.jpg)",
"![ha\"h>a](https://example.com/abc.jpg)",
"![__ha__*ha*](https://example.com/abc.jpg)",
"![ha[ha](https://example.com)](https://example.com/abc.jpg)",
];

let body = common::render(&cases.join("\n")).unwrap().body;
insta::assert_snapshot!(body);
}

#[test]
fn can_add_lazy_loading_and_async_decoding() {
let cases = vec![
"![haha](https://example.com/abc.jpg)",
"![](https://example.com/abc.jpg)",
"![ha\"h>a](https://example.com/abc.jpg)",
"![__ha__*ha*](https://example.com/abc.jpg)",
"![ha[ha](https://example.com)](https://example.com/abc.jpg)",
];

let mut config = Config::default_for_test();
config.markdown.lazy_async_image = true;

let body = common::render_with_config(&cases.join("\n"), config).unwrap().body;
insta::assert_snapshot!(body);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
source: components/markdown/tests/img.rs
expression: body
---
<p><img src="https://example.com/abc.jpg" alt="haha" loading="lazy" decoding="async" />
<img src="https://example.com/abc.jpg" alt="" loading="lazy" decoding="async" />
<img src="https://example.com/abc.jpg" alt="ha&quot;h&gt;a" loading="lazy" decoding="async" />
<img src="https://example.com/abc.jpg" alt="<strong>ha</strong><em>ha</em>" loading="lazy" decoding="async" />
<img src="https://example.com/abc.jpg" alt="ha<a href="https://example.com">ha</a>" loading="lazy" decoding="async" /></p>

10 changes: 10 additions & 0 deletions components/markdown/tests/snapshots/img__can_transform_image.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
source: components/markdown/tests/img.rs
expression: body
---
<p><img src="https://example.com/abc.jpg" alt="haha" />
<img src="https://example.com/abc.jpg" alt="" />
<img src="https://example.com/abc.jpg" alt="ha&quot;h&gt;a" />
<img src="https://example.com/abc.jpg" alt="haha" />
<img src="https://example.com/abc.jpg" alt="haha" /></p>

5 changes: 5 additions & 0 deletions docs/content/documentation/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ external_links_no_referrer = false
# For example, `...` into `…`, `"quote"` into `“curly”` etc
smart_punctuation = false

# Whether to set decoding="async" and loading="lazy" for all images
# When turned on, the alt text must be plain text.
# For example, `![xx](...)` is ok but `![*x*x](...)` isn’t ok
lazy_async_image = false

# Configuration of the link checker.
[link_checker]
# Skip link checking for external URLs that start with these prefixes
Expand Down

0 comments on commit b5a90db

Please sign in to comment.