Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions crates/mdbook-html/front-end/css/general.css
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
13 changes: 13 additions & 0 deletions crates/mdbook-html/front-end/js/book.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -658,6 +662,12 @@ aria-label="Show hidden lines"></button>';
})();

(function chapterNavigation() {
function zoomOutImages() {
for (const elem of Array.from(document.querySelectorAll('input.checkbox-img'))) {
Comment thread
ehuss marked this conversation as resolved.
elem.checked = false;
}
}

document.addEventListener('keydown', function(e) {
if (e.altKey ||
e.ctrlKey ||
Expand Down Expand Up @@ -724,6 +734,9 @@ aria-label="Show hidden lines"></button>';
e.preventDefault();
showHelp();
break;
case 'Escape':
zoomOutImages();
break;
}

// Rest of the keys are only active when the Shift key is not pressed
Expand Down
42 changes: 38 additions & 4 deletions crates/mdbook-html/src/html/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little concerned about this approach of using a duplicated <img> tag. Screen readers may have difficulty handling this. I think at a minimum it should have aria-hidden="true". I'm a bit uncomfortable with this approach in general, though I'm willing to give it a try.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think screen readers all handle display: none, so I think we're fine (although if we could test it, would be even better). However, won't aria-hidden=true be an issue when the image is zoomed-in?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried with my screen reader. It does seem to ignore the "display: none".

It's a little awkward now since the screen reader navigates to the checkbox (and says you are on a checkbox, and tells you the image alt), and then the image (and the alt and title). But I suppose it should be fine. We can adjust it if we get further feedback.


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(_) => {
Expand Down
4 changes: 4 additions & 0 deletions guide/src/format/markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
6 changes: 6 additions & 0 deletions tests/gui/books/basic/src/chapter_1.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Chapter 1

Side by side [![the logo](rust-logo.svg)](link) [![the logo](rust-logo.svg)](link).

Some text [blob](a).

![rust logo](rust-logo.svg)

[![the logo](rust-logo.svg)](https://rust-lang.org/)
87 changes: 87 additions & 0 deletions tests/gui/image-zoom.goml
Original file line number Diff line number Diff line change
@@ -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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intentional that this clicks on the image underneath the wrapper? That is, should this be something like ".image-wrapper > img" or ".image-wrapper" to simulate what the user would actually click on?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, they click on the image, but the image being part of the label, since there is no preventDefault() on the image, the event is propagated to the parent until the parent is the label which then handles the click event and stops its propagation. So it is how users will interact with it.

// 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 `<a>` 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 `<img>` 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"})
4 changes: 2 additions & 2 deletions tests/testsuite/markdown/basic_markdown/expected/images.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<h1 id="images"><a class="header" href="#images">Images</a></h1>
<p><img src="https://rust-lang.org/logos/rust-logo-256x256.png" alt="Image “alt” &amp; &quot; &quot;text&quot; &amp; &lt;stuff&gt; url &lt;em&gt;html&lt;/em&gt; — hard break "></p>
<p><img src="https://rust-lang.org/logos/rust-logo-256x256.png" title="Some title" alt="Image with title"></p>
<p><label class="checkbox-label"><input class="checkbox-img" type="checkbox"><img src="https://rust-lang.org/logos/rust-logo-256x256.png" alt="Image “alt” &amp; &quot; &quot;text&quot; &amp; &lt;stuff&gt; url &lt;em&gt;html&lt;/em&gt; — hard break "><span class="img-wrapper"><img src="https://rust-lang.org/logos/rust-logo-256x256.png" alt="Image “alt” &amp; &quot; &quot;text&quot; &amp; &lt;stuff&gt; url &lt;em&gt;html&lt;/em&gt; — hard break "></span></label></p>
<p><label class="checkbox-label"><input class="checkbox-img" type="checkbox"><img src="https://rust-lang.org/logos/rust-logo-256x256.png" title="Some title" alt="Image with title"><span class="img-wrapper"><img src="https://rust-lang.org/logos/rust-logo-256x256.png" title="Some title" alt="Image with title"></span></label></p>
2 changes: 1 addition & 1 deletion tests/testsuite/print/relative_links/expected/print.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ <h1 id="testing-relative-links-for-the-print-page"><a class="header" href="#test
Link <a href="first/alpha/beta.html#anchor">inside but doesn’t exist with anchor</a>.
Link <a href="first/alpha/gamma.html">inside to html</a>.
Link <a href="first/alpha/gamma.html#anchor">inside to html with anchor</a>.</p>
<p><img src="images/picture.png" alt="Some image"></p>
<p><label class="checkbox-label"><input class="checkbox-img" type="checkbox"><img src="images/picture.png" alt="Some image"><span class="img-wrapper"><img src="images/picture.png" alt="Some image"></span></label></p>
<p><a href="#first-nested">HTML Link</a></p>
<img src="images/picture.png" alt="raw html">
<h2 id="some-section"><a class="header" href="#some-section">Some section</a></h2>
Expand Down
Loading