Add support for lazy loading images (#2211)
* 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.
This commit is contained in:
parent
0a9bfa16c2
commit
22dc32a589
@ -51,6 +51,8 @@ pub struct Markdown {
|
|||||||
/// The compiled extra themes into a theme set
|
/// The compiled extra themes into a theme set
|
||||||
#[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are need
|
#[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are need
|
||||||
pub extra_theme_set: Arc<Option<ThemeSet>>,
|
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 {
|
impl Markdown {
|
||||||
@ -204,6 +206,7 @@ impl Default for Markdown {
|
|||||||
extra_syntaxes_and_themes: vec![],
|
extra_syntaxes_and_themes: vec![],
|
||||||
extra_syntax_set: None,
|
extra_syntax_set: None,
|
||||||
extra_theme_set: Arc::new(None),
|
extra_theme_set: Arc::new(None),
|
||||||
|
lazy_async_image: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -252,6 +252,8 @@ pub fn markdown_to_html(
|
|||||||
|
|
||||||
let mut stop_next_end_p = false;
|
let mut stop_next_end_p = false;
|
||||||
|
|
||||||
|
let lazy_async_image = context.config.markdown.lazy_async_image;
|
||||||
|
|
||||||
let mut opts = Options::empty();
|
let mut opts = Options::empty();
|
||||||
let mut has_summary = false;
|
let mut has_summary = false;
|
||||||
opts.insert(Options::ENABLE_TABLES);
|
opts.insert(Options::ENABLE_TABLES);
|
||||||
@ -387,13 +389,35 @@ pub fn markdown_to_html(
|
|||||||
events.push(Event::Html("</code></pre>\n".into()));
|
events.push(Event::Html("</code></pre>\n".into()));
|
||||||
}
|
}
|
||||||
Event::Start(Tag::Image(link_type, src, title)) => {
|
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);
|
let link = format!("{}{}", context.current_page_permalink, &*src);
|
||||||
events.push(Event::Start(Tag::Image(link_type, link.into(), title)));
|
link.into()
|
||||||
} else {
|
} 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() => {
|
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"));
|
error = Some(Error::msg("There is a link that is missing a URL"));
|
||||||
events.push(Event::Start(Tag::Link(link_type, "#".into(), title)));
|
events.push(Event::Start(Tag::Link(link_type, "#".into(), title)));
|
||||||
|
33
components/markdown/tests/img.rs
Normal file
33
components/markdown/tests/img.rs
Normal file
@ -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);
|
||||||
|
}
|
@ -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"h>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>
|
||||||
|
|
@ -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"h>a" />
|
||||||
|
<img src="https://example.com/abc.jpg" alt="haha" />
|
||||||
|
<img src="https://example.com/abc.jpg" alt="haha" /></p>
|
||||||
|
|
@ -125,6 +125,11 @@ external_links_no_referrer = false
|
|||||||
# For example, `...` into `…`, `"quote"` into `“curly”` etc
|
# For example, `...` into `…`, `"quote"` into `“curly”` etc
|
||||||
smart_punctuation = false
|
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.
|
# Configuration of the link checker.
|
||||||
[link_checker]
|
[link_checker]
|
||||||
# Skip link checking for external URLs that start with these prefixes
|
# Skip link checking for external URLs that start with these prefixes
|
||||||
|
Loading…
Reference in New Issue
Block a user