Skip to content

Commit 0494dc1

Browse files
committed
Make print page (print.html) links link to anchors on the print page
Let all the anchors id on the print page to have a path id prefix to help locate. e.g. bar/foo.md#abc -> #bar-foo-abc Also append a dummy div to the start of the original page to make sure that original page links without an anchor can also be located. Fix to remove all the `./` in the normalized path id so that for "./foo/bar.html#abc" we still get "#foo-bar-abc" Add support for redirect link anchors in print page so that anchors can also be redirected, also handle URL redirect links on print page Handle all the elements id to add a path prefix, also make path id to all be the lower case Fix for print page footnote links by adding the path id prefix Signed-off-by: Hollow Man <[email protected]>
1 parent 5a99af2 commit 0494dc1

File tree

4 files changed

+390
-71
lines changed

4 files changed

+390
-71
lines changed

crates/mdbook-core/src/utils/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ pub fn normalize_id(content: &str) -> String {
4343

4444
/// Generate an ID for use with anchors which is derived from a "normalised"
4545
/// string.
46-
fn id_from_content(content: &str) -> String {
46+
pub fn id_from_content(content: &str) -> String {
4747
let mut content = content.to_string();
4848

4949
// Skip any tags or html-encoded stuff

crates/mdbook-html/src/html_handlebars/hbs_renderer.rs

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use mdbook_core::book::{Book, BookItem, Chapter};
88
use mdbook_core::config::{BookConfig, Code, Config, HtmlConfig, Playground, RustEdition};
99
use mdbook_core::utils;
1010
use mdbook_core::utils::fs::get_404_output_file;
11-
use mdbook_markdown::{render_markdown, render_markdown_with_path};
11+
use mdbook_markdown::{render_markdown, render_markdown_with_path_and_redirects};
1212
use mdbook_renderer::{RenderContext, Renderer};
1313
use regex::{Captures, Regex};
1414
use serde_json::json;
@@ -58,16 +58,38 @@ impl HtmlHandlebars {
5858

5959
let content = render_markdown(&ch.content, ctx.html_config.smart_punctuation);
6060

61-
let fixed_content =
62-
render_markdown_with_path(&ch.content, ctx.html_config.smart_punctuation, Some(path));
61+
let printed_item = render_markdown_with_path_and_redirects(
62+
&ch.content,
63+
ctx.html_config.smart_punctuation,
64+
Some(path),
65+
&ctx.html_config.redirect,
66+
);
6367
if prev_ch.is_some() && ctx.html_config.print.page_break {
6468
// Add page break between chapters
6569
// See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before
6670
// Add both two CSS properties because of the compatibility issue
6771
print_content
6872
.push_str(r#"<div style="break-before: page; page-break-before: always;"></div>"#);
6973
}
70-
print_content.push_str(&fixed_content);
74+
let print_page_id = {
75+
let mut base = path.display().to_string();
76+
if base.ends_with(".md") {
77+
base.truncate(base.len() - 3);
78+
}
79+
&base
80+
.replace("/", "-")
81+
.replace("\\", "-")
82+
.to_ascii_lowercase()
83+
};
84+
85+
// We have to build header links in advance so that we can know the ranges
86+
// for the headers in one page.
87+
// Insert a dummy div to make sure that we can locate the specific page.
88+
print_content.push_str(&(format!(r#"<div id="{print_page_id}"></div>"#)));
89+
print_content.push_str(&build_header_links(
90+
&build_print_element_id(&printed_item, &print_page_id),
91+
Some(print_page_id),
92+
));
7193

7294
// Update the context with data for this file
7395
let ctx_path = path
@@ -238,7 +260,23 @@ impl HtmlHandlebars {
238260
code_config: &Code,
239261
edition: Option<RustEdition>,
240262
) -> String {
241-
let rendered = build_header_links(&rendered);
263+
let rendered = build_header_links(&rendered, None);
264+
let rendered = self.post_process_common(rendered, &playground_config, code_config, edition);
265+
266+
rendered
267+
}
268+
269+
/// Applies some post-processing to the HTML to apply some adjustments.
270+
///
271+
/// This common function is used for both normal chapters (via
272+
/// `post_process`) and the combined print page.
273+
fn post_process_common(
274+
&self,
275+
rendered: String,
276+
playground_config: &Playground,
277+
code_config: &Code,
278+
edition: Option<RustEdition>,
279+
) -> String {
242280
let rendered = fix_code_blocks(&rendered);
243281
let rendered = add_playground_pre(&rendered, playground_config, edition);
244282
let rendered = hide_lines(&rendered, code_config);
@@ -496,7 +534,7 @@ impl Renderer for HtmlHandlebars {
496534
debug!("Render template");
497535
let rendered = handlebars.render("index", &data)?;
498536

499-
let rendered = self.post_process(
537+
let rendered = self.post_process_common(
500538
rendered,
501539
&html_config.playground,
502540
&html_config.code,
@@ -694,9 +732,35 @@ fn make_data(
694732
Ok(data)
695733
}
696734

735+
/// Go through the rendered print page HTML,
736+
/// add path id prefix to all the elements id as well as footnote links.
737+
fn build_print_element_id(html: &str, print_page_id: &str) -> String {
738+
static ALL_ID: LazyLock<Regex> =
739+
LazyLock::new(|| Regex::new(r#"(<[^>]*?id=")([^"]+?)""#).unwrap());
740+
static FOOTNOTE_ID: LazyLock<Regex> = LazyLock::new(|| {
741+
Regex::new(
742+
r##"(<sup [^>]*?class="footnote-reference"[^>]*?>[^<]*?<a [^>]*?href="#)([^"]+?)""##,
743+
)
744+
.unwrap()
745+
});
746+
747+
let temp_html = ALL_ID.replace_all(html, |caps: &Captures<'_>| {
748+
format!("{}{}-{}\"", &caps[1], print_page_id, &caps[2])
749+
});
750+
751+
FOOTNOTE_ID
752+
.replace_all(&temp_html, |caps: &Captures<'_>| {
753+
format!("{}{}-{}\"", &caps[1], print_page_id, &caps[2])
754+
})
755+
.into_owned()
756+
}
757+
697758
/// Goes through the rendered HTML, making sure all header tags have
698759
/// an anchor respectively so people can link to sections directly.
699-
fn build_header_links(html: &str) -> String {
760+
///
761+
/// `print_page_id` should be set to the print page ID prefix when adjusting the
762+
/// print page.
763+
fn build_header_links(html: &str, print_page_id: Option<&str>) -> String {
700764
static BUILD_HEADER_LINKS: LazyLock<Regex> = LazyLock::new(|| {
701765
Regex::new(r#"<h(\d)(?: id="([^"]+)")?(?: class="([^"]+)")?>(.*?)</h\d>"#).unwrap()
702766
});
@@ -725,21 +789,34 @@ fn build_header_links(html: &str) -> String {
725789
caps.get(2).map(|x| x.as_str().to_string()),
726790
caps.get(3).map(|x| x.as_str().to_string()),
727791
&mut id_counter,
792+
print_page_id,
728793
)
729794
})
730795
.into_owned()
731796
}
732797

733798
/// Insert a single link into a header, making sure each link gets its own
734799
/// unique ID by appending an auto-incremented number (if necessary).
800+
///
801+
/// For `print.html`, we will add a path id prefix.
735802
fn insert_link_into_header(
736803
level: usize,
737804
content: &str,
738805
id: Option<String>,
739806
classes: Option<String>,
740807
id_counter: &mut HashMap<String, usize>,
808+
print_page_id: Option<&str>,
741809
) -> String {
742-
let id = id.unwrap_or_else(|| utils::unique_id_from_content(content, id_counter));
810+
let id = if let Some(print_page_id) = print_page_id {
811+
let content_id = {
812+
#[allow(deprecated)]
813+
utils::id_from_content(content)
814+
};
815+
let with_prefix = format!("{} {}", print_page_id, content_id);
816+
id.unwrap_or_else(|| utils::unique_id_from_content(&with_prefix, id_counter))
817+
} else {
818+
id.unwrap_or_else(|| utils::unique_id_from_content(content, id_counter))
819+
};
743820
let classes = classes
744821
.map(|s| format!(" class=\"{s}\""))
745822
.unwrap_or_default();
@@ -1122,7 +1199,7 @@ mod tests {
11221199
];
11231200

11241201
for (src, should_be) in inputs {
1125-
let got = build_header_links(src);
1202+
let got = build_header_links(src, None);
11261203
assert_eq!(got, should_be);
11271204
}
11281205
}

0 commit comments

Comments
 (0)