Skip to content

Commit cf90bc8

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 f9cb988 commit cf90bc8

File tree

7 files changed

+377
-67
lines changed

7 files changed

+377
-67
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: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,25 @@ impl HtmlHandlebars {
6767
print_content
6868
.push_str(r#"<div style="break-before: page; page-break-before: always;"></div>"#);
6969
}
70-
print_content.push_str(&fixed_content);
70+
let print_page_id = {
71+
let mut base = path.display().to_string();
72+
if base.ends_with(".md") {
73+
base.truncate(base.len() - 3);
74+
}
75+
&base
76+
.replace("/", "-")
77+
.replace("\\", "-")
78+
.to_ascii_lowercase()
79+
};
80+
81+
// We have to build header links in advance so that we can know the ranges
82+
// for the headers in one page.
83+
// Insert a dummy div to make sure that we can locate the specific page.
84+
print_content.push_str(&(format!(r#"<div id="{print_page_id}"></div>"#)));
85+
print_content.push_str(&build_header_links(
86+
&build_print_element_id(&fixed_content, &print_page_id),
87+
Some(print_page_id),
88+
));
7189

7290
// Update the context with data for this file
7391
let ctx_path = path
@@ -239,7 +257,23 @@ impl HtmlHandlebars {
239257
code_config: &Code,
240258
edition: Option<RustEdition>,
241259
) -> String {
242-
let rendered = build_header_links(&rendered);
260+
let rendered = build_header_links(&rendered, None);
261+
let rendered = self.post_process_common(rendered, &playground_config, code_config, edition);
262+
263+
rendered
264+
}
265+
266+
/// Applies some post-processing to the HTML to apply some adjustments.
267+
///
268+
/// This common function is used for both normal chapters (via
269+
/// `post_process`) and the combined print page.
270+
fn post_process_common(
271+
&self,
272+
rendered: String,
273+
playground_config: &Playground,
274+
code_config: &Code,
275+
edition: Option<RustEdition>,
276+
) -> String {
243277
let rendered = fix_code_blocks(&rendered);
244278
let rendered = add_playground_pre(&rendered, playground_config, edition);
245279
let rendered = hide_lines(&rendered, code_config);
@@ -497,7 +531,7 @@ impl Renderer for HtmlHandlebars {
497531
debug!("Render template");
498532
let rendered = handlebars.render("index", &data)?;
499533

500-
let rendered = self.post_process(
534+
let rendered = self.post_process_common(
501535
rendered,
502536
&html_config.playground,
503537
&html_config.code,
@@ -695,9 +729,35 @@ fn make_data(
695729
Ok(data)
696730
}
697731

732+
/// Go through the rendered print page HTML,
733+
/// add path id prefix to all the elements id as well as footnote links.
734+
fn build_print_element_id(html: &str, print_page_id: &str) -> String {
735+
static ALL_ID: LazyLock<Regex> =
736+
LazyLock::new(|| Regex::new(r#"(<[^>]*?id=")([^"]+?)""#).unwrap());
737+
static FOOTNOTE_ID: LazyLock<Regex> = LazyLock::new(|| {
738+
Regex::new(
739+
r##"(<sup [^>]*?class="footnote-reference"[^>]*?>[^<]*?<a [^>]*?href="#)([^"]+?)""##,
740+
)
741+
.unwrap()
742+
});
743+
744+
let temp_html = ALL_ID.replace_all(html, |caps: &Captures<'_>| {
745+
format!("{}{}-{}\"", &caps[1], print_page_id, &caps[2])
746+
});
747+
748+
FOOTNOTE_ID
749+
.replace_all(&temp_html, |caps: &Captures<'_>| {
750+
format!("{}{}-{}\"", &caps[1], print_page_id, &caps[2])
751+
})
752+
.into_owned()
753+
}
754+
698755
/// Goes through the rendered HTML, making sure all header tags have
699756
/// an anchor respectively so people can link to sections directly.
700-
fn build_header_links(html: &str) -> String {
757+
///
758+
/// `print_page_id` should be set to the print page ID prefix when adjusting the
759+
/// print page.
760+
fn build_header_links(html: &str, print_page_id: Option<&str>) -> String {
701761
static BUILD_HEADER_LINKS: LazyLock<Regex> = LazyLock::new(|| {
702762
Regex::new(r#"<h(\d)(?: id="([^"]+)")?(?: class="([^"]+)")?>(.*?)</h\d>"#).unwrap()
703763
});
@@ -726,21 +786,34 @@ fn build_header_links(html: &str) -> String {
726786
caps.get(2).map(|x| x.as_str().to_string()),
727787
caps.get(3).map(|x| x.as_str().to_string()),
728788
&mut id_counter,
789+
print_page_id,
729790
)
730791
})
731792
.into_owned()
732793
}
733794

734795
/// Insert a single link into a header, making sure each link gets its own
735796
/// unique ID by appending an auto-incremented number (if necessary).
797+
///
798+
/// For `print.html`, we will add a path id prefix.
736799
fn insert_link_into_header(
737800
level: usize,
738801
content: &str,
739802
id: Option<String>,
740803
classes: Option<String>,
741804
id_counter: &mut HashMap<String, usize>,
805+
print_page_id: Option<&str>,
742806
) -> String {
743-
let id = id.unwrap_or_else(|| utils::unique_id_from_content(content, id_counter));
807+
let id = if let Some(print_page_id) = print_page_id {
808+
let content_id = {
809+
#[allow(deprecated)]
810+
utils::id_from_content(content)
811+
};
812+
let with_prefix = format!("{} {}", print_page_id, content_id);
813+
id.unwrap_or_else(|| utils::unique_id_from_content(&with_prefix, id_counter))
814+
} else {
815+
id.unwrap_or_else(|| utils::unique_id_from_content(content, id_counter))
816+
};
744817
let classes = classes
745818
.map(|s| format!(" class=\"{s}\""))
746819
.unwrap_or_default();
@@ -1123,7 +1196,7 @@ mod tests {
11231196
];
11241197

11251198
for (src, should_be) in inputs {
1126-
let got = build_header_links(src);
1199+
let got = build_header_links(src, None);
11271200
assert_eq!(got, should_be);
11281201
}
11291202
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,8 @@ fn render_item(
134134
.with_context(|| "Could not convert HTML path to str")?;
135135
let anchor_base = utils::fs::normalize_path(filepath);
136136

137-
let options = HtmlRenderOptions::new(&chapter_path);
137+
let redirect = HashMap::new();
138+
let options = HtmlRenderOptions::new(&chapter_path, &redirect);
138139
let mut p = new_cmark_parser(&chapter.content, &options.markdown_options).peekable();
139140

140141
let mut in_heading = false;

crates/mdbook-html/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ use std::path::Path;
1111
/// Creates an [`HtmlRenderOptions`] from the given config.
1212
pub fn html_render_options_from_config<'a>(
1313
path: &'a Path,
14-
config: &HtmlConfig,
14+
config: &'a HtmlConfig,
1515
) -> HtmlRenderOptions<'a> {
16-
let mut options = HtmlRenderOptions::new(path);
16+
let mut options = HtmlRenderOptions::new(path, &config.redirect);
1717
options.markdown_options.smart_punctuation = config.smart_punctuation;
1818
options
1919
}

0 commit comments

Comments
 (0)