diff --git a/crates/mdbook-html/front-end/css/general.css b/crates/mdbook-html/front-end/css/general.css
index 80d0d9bf2c..06a7f42786 100644
--- a/crates/mdbook-html/front-end/css/general.css
+++ b/crates/mdbook-html/front-end/css/general.css
@@ -409,3 +409,56 @@ dd > p {
/* Add some space between the icon and the text. */
margin-right: 8px;
}
+
+/* All this code is to handle the "zoomable" images feature. */
+
+/* This one makes the checkbox invisible while keeping it selectable with the keyboard (with the
+ "tab" key). */
+.content .checkbox-img {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+/* When the input is focused, we make it appear to the user by adding an outline to the img
+ instead */
+.content .checkbox-img:focus + img {
+ outline: auto;
+}
+/* The "zoomed-in" image is not displayed until the checkbox is "ticked". */
+.content .checkbox-img:not(:checked) ~ .img-wrapper {
+ display: none;
+}
+.content .checkbox-label {
+ display: block;
+ max-width: 100%;
+}
+.content .checkbox-label img {
+ cursor: zoom-in;
+}
+.content .checkbox-img:checked ~ .img-wrapper {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background-color: rgba(0, 0, 0, 0.4);
+ z-index: 1001;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: zoom-out;
+}
+.content .checkbox-img:checked ~ .img-wrapper > img {
+ --img-padding: 5px;
+ padding: var(--img-padding);
+ background: #999;
+ cursor: zoom-out;
+ max-width: calc(100% - (var(--img-padding) * 2));
+ max-height: calc(100% - (var(--img-padding) * 2));
+}
diff --git a/crates/mdbook-html/front-end/js/book.js b/crates/mdbook-html/front-end/js/book.js
index fe74b7106f..c62124cb32 100644
--- a/crates/mdbook-html/front-end/js/book.js
+++ b/crates/mdbook-html/front-end/js/book.js
@@ -27,6 +27,10 @@ function mdbook_something_else_has_focus(e) {
// Check composedPath in case the event happened from something generated
// from the shadowDOM.
const target = e.composedPath()[0] || e.target;
+ // If this is the `checkbox-img` input which has the focus, we want to handle it here.
+ if (target.classList.contains('checkbox-img')) {
+ return false;
+ }
return /^(?:input|select|textarea)$/i.test(target.nodeName);
}
@@ -658,6 +662,12 @@ aria-label="Show hidden lines">';
})();
(function chapterNavigation() {
+ function zoomOutImages() {
+ for (const elem of Array.from(document.querySelectorAll('input.checkbox-img'))) {
+ elem.checked = false;
+ }
+ }
+
document.addEventListener('keydown', function(e) {
if (e.altKey ||
e.ctrlKey ||
@@ -724,6 +734,9 @@ aria-label="Show hidden lines">';
e.preventDefault();
showHelp();
break;
+ case 'Escape':
+ zoomOutImages();
+ break;
}
// Rest of the keys are only active when the Shift key is not pressed
diff --git a/crates/mdbook-html/src/html/tree.rs b/crates/mdbook-html/src/html/tree.rs
index 5cb97ce378..c4abd2f266 100644
--- a/crates/mdbook-html/src/html/tree.rs
+++ b/crates/mdbook-html/src/html/tree.rs
@@ -9,7 +9,7 @@ use super::tokenizer::parse_html;
use super::{HtmlRenderOptions, hide_lines, wrap_rust_main};
use crate::utils::{id_from_content, unique_id};
use ego_tree::{NodeId, NodeRef, Tree};
-use html5ever::tendril::StrTendril;
+use html5ever::tendril::{SliceExt, StrTendril};
use html5ever::tokenizer::{TagKind, Token};
use html5ever::{LocalName, QualName};
use indexmap::IndexMap;
@@ -69,7 +69,7 @@ impl Node {
}
/// An HTML element.
-#[derive(Debug)]
+#[derive(Debug, Clone)]
pub(crate) struct Element {
/// The tag name.
pub(crate) name: QualName,
@@ -565,8 +565,42 @@ where
}
// This will eat TagEnd::Image
let alt = self.text_for_img_alt();
- img.insert_attr("alt", alt.into());
- self.append(Node::Element(img));
+ img.insert_attr("alt", alt.to_tendril());
+
+ // If the image is not being rendered inside a link, we can enable the "zoom-in"
+ // feature.
+ if !self
+ .tag_stack
+ .iter()
+ .filter_map(|node_id| {
+ self.tree
+ .get(*node_id)
+ .and_then(|el| el.value().as_element())
+ })
+ .any(|el| *el.name.local == *"a")
+ {
+ let mut label = Element::new("label");
+ label.insert_attr("class", "checkbox-label".to_tendril());
+ self.push(Node::Element(label));
+
+ let mut input = Element::new("input");
+ input.insert_attr("class", "checkbox-img".to_tendril());
+ input.insert_attr("type", "checkbox".to_tendril());
+ self.append(Node::Element(input));
+
+ self.append(Node::Element(img.clone()));
+
+ let mut wrapper = Element::new("span");
+ wrapper.insert_attr("class", "img-wrapper".to_tendril());
+ self.push_no_stack(Node::Element(wrapper));
+
+ self.append(Node::Element(img));
+
+ // We exit the `label` and the `span` (which used push_no_stack).
+ self.pop();
+ } else {
+ self.append(Node::Element(img));
+ }
return;
}
Tag::MetadataBlock(_) => {
diff --git a/guide/src/format/markdown.md b/guide/src/format/markdown.md
index c42c8d1af6..bebc039c6d 100644
--- a/guide/src/format/markdown.md
+++ b/guide/src/format/markdown.md
@@ -318,3 +318,7 @@ This feature is enabled by default.
To disable it, see the [`output.html.admonitions`] config option.
[`output.html.admonitions`]: configuration/renderers.md#html-renderer-options
+
+## Zoom-in
+
+All images in the chapters content have a "zoom-in" feature: you can click on it to make it bigger, and click it again to zoom out. You can focus the image with the keyboard as well, and press the spacebar to zoom in and out as well.
diff --git a/tests/gui/books/basic/src/chapter_1.md b/tests/gui/books/basic/src/chapter_1.md
index 9aa8f00e40..a92701ab0e 100644
--- a/tests/gui/books/basic/src/chapter_1.md
+++ b/tests/gui/books/basic/src/chapter_1.md
@@ -1,3 +1,9 @@
# Chapter 1
Side by side [](link) [](link).
+
+Some text [blob](a).
+
+
+
+[](https://rust-lang.org/)
diff --git a/tests/gui/image-zoom.goml b/tests/gui/image-zoom.goml
new file mode 100644
index 0000000000..398d7cb47a
--- /dev/null
+++ b/tests/gui/image-zoom.goml
@@ -0,0 +1,87 @@
+// This test ensures that the image zoom-in/zoom-out works as expected.
+
+define-function: (
+ "check-image-not-zoomed-in",
+ [],
+ block {
+ // The "zoomed in" image should not be displayed.
+ assert-css: (".checkbox-label > .img-wrapper", {"display": "none"})
+ // The cursor for the image should be "zoom-in".
+ assert-css: (".checkbox-label > img", {"cursor": "zoom-in"})
+ },
+)
+
+define-function: (
+ "check-image-zoomed-in",
+ [],
+ block {
+ // The image wrapper should now be displayed and have a "zoom-out" cursor.
+ assert-css: (".checkbox-label > .img-wrapper", {"display": "flex", "cursor": "zoom-out"})
+ // Same for the image it contains.
+ assert-css: (
+ ".checkbox-label > .img-wrapper img",
+ {"display": "block", "cursor": "zoom-out"},
+ )
+ },
+)
+
+go-to: |DOC_PATH| + "basic/index.html"
+show-text: true
+call-function: ("check-image-not-zoomed-in", {})
+
+// We click on the image to "zoom in".
+click: ".checkbox-label > img"
+call-function: ("check-image-zoomed-in", {})
+
+// We click on the wrapper to "zoom out".
+click: ".checkbox-label > img"
+// We check that everything is back to the previous state.
+call-function: ("check-image-not-zoomed-in", {})
+
+// We test that the "escape" key also hides the "zoomed in" image.
+// We click on the image to "zoom in".
+click: ".checkbox-label > img"
+call-function: ("check-image-zoomed-in", {})
+press-key: "Escape"
+// We check that everything is back to the previous state.
+call-function: ("check-image-not-zoomed-in", {})
+
+// We test that we can zoom in and out using only the keyboard.
+// First we focus on the link just before the image we want to zoom in.
+focus: "a[href='a']"
+assert: "a[href='a']:focus"
+press-key: "Tab"
+// We check that the checkbox to "zoom in" is focused.
+assert: ".checkbox-img:focus"
+// We check that the image about to be zoomed in has the outline.
+store-css: (".checkbox-img:focus", {"outline": outline})
+// Because we're using "auto" to match the system outline, we cannot check the whole value directly
+// so instead we check if "auto" is present.
+assert-variable: (outline, "auto", CONTAINS)
+// We zoom in.
+press-key: "Space"
+call-function: ("check-image-zoomed-in", {})
+// We press the key again to zoom out.
+press-key: "Space"
+call-function: ("check-image-not-zoomed-in", {})
+// We now check it works with the "Escape" key as well.
+press-key: "Space"
+call-function: ("check-image-zoomed-in", {})
+press-key: "Escape"
+call-function: ("check-image-not-zoomed-in", {})
+
+// Now we check that images in links (in `` elements) don't get the "zoom-in" feature enabled.
+// There is only one zoomable image.
+assert-count: ("main .checkbox-img", 1)
+// We should have 5 images: 4 "normal" ones visible in the rendered book and one hidden which is
+// used only for the "zoom-in" feature.
+assert-count: ("main img", 5)
+// We check exact paths to ensure the 5 ` ` are different.
+assert: ("main > p > label.checkbox-label > img:nth-of-type(1)")
+assert: ("main > p > label.checkbox-label > span.img-wrapper > img")
+assert: ("main > p:nth-of-type(1) > a:nth-of-type(1) > img")
+assert: ("main > p:nth-of-type(1) > a:nth-of-type(2) > img")
+assert: ("main > p:not(:nth-of-type(2)) > a > img")
+
+// The cursor for the image in link should not have "zoom-in".
+assert-css: ("main > p > a > img", {"cursor": "pointer"})
diff --git a/tests/testsuite/markdown/basic_markdown/expected/images.html b/tests/testsuite/markdown/basic_markdown/expected/images.html
index 4dfdf1657a..c56a085d24 100644
--- a/tests/testsuite/markdown/basic_markdown/expected/images.html
+++ b/tests/testsuite/markdown/basic_markdown/expected/images.html
@@ -1,3 +1,3 @@
Images
-

inside but doesn’t exist with anchor.
Link inside to html.
Link inside to html with anchor.
