From cda29521ab473a50b09031928722a8f37a8f5d1a Mon Sep 17 00:00:00 2001 From: sinofp Date: Sat, 6 May 2023 22:04:33 +0100 Subject: [PATCH] Add support for lazy loading images (#2211) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- components/config/src/config/markup.rs | 3 ++ components/markdown/src/markdown.rs | 32 +++++++++++++++--- components/markdown/tests/img.rs | 33 +++++++++++++++++++ ...n_add_lazy_loading_and_async_decoding.snap | 10 ++++++ .../snapshots/img__can_transform_image.snap | 10 ++++++ .../getting-started/configuration.md | 5 +++ 6 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 components/markdown/tests/img.rs create mode 100644 components/markdown/tests/snapshots/img__can_add_lazy_loading_and_async_decoding.snap create mode 100644 components/markdown/tests/snapshots/img__can_transform_image.snap diff --git a/components/config/src/config/markup.rs b/components/config/src/config/markup.rs index 7cd03520c..b1ac7fa22 100644 --- a/components/config/src/config/markup.rs +++ b/components/config/src/config/markup.rs @@ -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>, + /// 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 { @@ -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, } } } diff --git a/components/markdown/src/markdown.rs b/components/markdown/src/markdown.rs index d773a21f1..a6cb3beca 100644 --- a/components/markdown/src/markdown.rs +++ b/components/markdown/src/markdown.rs @@ -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); @@ -387,13 +389,35 @@ pub fn markdown_to_html( events.push(Event::Html("\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 = "\"").expect("Could 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))); diff --git a/components/markdown/tests/img.rs b/components/markdown/tests/img.rs new file mode 100644 index 000000000..c34713a9d --- /dev/null +++ b/components/markdown/tests/img.rs @@ -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); +} diff --git a/components/markdown/tests/snapshots/img__can_add_lazy_loading_and_async_decoding.snap b/components/markdown/tests/snapshots/img__can_add_lazy_loading_and_async_decoding.snap new file mode 100644 index 000000000..ed179b404 --- /dev/null +++ b/components/markdown/tests/snapshots/img__can_add_lazy_loading_and_async_decoding.snap @@ -0,0 +1,10 @@ +--- +source: components/markdown/tests/img.rs +expression: body +--- +

haha + +ha"h>a +<strong>ha</strong><em>ha</em> +ha<a href=ha" loading="lazy" decoding="async" />

+ diff --git a/components/markdown/tests/snapshots/img__can_transform_image.snap b/components/markdown/tests/snapshots/img__can_transform_image.snap new file mode 100644 index 000000000..5ad51f586 --- /dev/null +++ b/components/markdown/tests/snapshots/img__can_transform_image.snap @@ -0,0 +1,10 @@ +--- +source: components/markdown/tests/img.rs +expression: body +--- +

haha + +ha"h>a +haha +haha

+ diff --git a/docs/content/documentation/getting-started/configuration.md b/docs/content/documentation/getting-started/configuration.md index b2ff781f6..ac85b7242 100644 --- a/docs/content/documentation/getting-started/configuration.md +++ b/docs/content/documentation/getting-started/configuration.md @@ -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