Skip to content

Commit c6e22e1

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. Signed-off-by: Hollow Man <[email protected]>
1 parent 2213312 commit c6e22e1

File tree

2 files changed

+116
-25
lines changed

2 files changed

+116
-25
lines changed

src/renderer/html_handlebars/hbs_renderer.rs

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,19 @@ impl HtmlHandlebars {
6363
print_content
6464
.push_str(r#"<div style="break-before: page; page-break-before: always;"></div>"#);
6565
}
66-
print_content.push_str(&fixed_content);
66+
let path_id = {
67+
let mut base = path.display().to_string();
68+
if base.ends_with(".md") {
69+
base.replace_range(base.len() - 3.., "");
70+
}
71+
&base.replace("/", "-").replace("\\", "-")
72+
};
73+
74+
// We have to build header links in advance so that we can know the ranges
75+
// for the headers in one page.
76+
// Insert a dummy div to make sure that we can locate the specific page.
77+
print_content.push_str(&(format!(r#"<div id="{}"></div>"#, &path_id)));
78+
print_content.push_str(&build_header_links(&fixed_content, Some(path_id)));
6779

6880
// Update the context with data for this file
6981
let ctx_path = path
@@ -181,19 +193,31 @@ impl HtmlHandlebars {
181193
}
182194

183195
#[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))]
184-
fn post_process(
196+
fn post_process_print(
185197
&self,
186198
rendered: String,
187199
playground_config: &Playground,
188200
edition: Option<RustEdition>,
189201
) -> String {
190-
let rendered = build_header_links(&rendered);
191202
let rendered = fix_code_blocks(&rendered);
192203
let rendered = add_playground_pre(&rendered, playground_config, edition);
193204

194205
rendered
195206
}
196207

208+
#[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))]
209+
fn post_process(
210+
&self,
211+
rendered: String,
212+
playground_config: &Playground,
213+
edition: Option<RustEdition>,
214+
) -> String {
215+
let rendered = build_header_links(&rendered, None);
216+
let rendered = self.post_process_print(rendered, &playground_config, edition);
217+
218+
rendered
219+
}
220+
197221
fn copy_static_files(
198222
&self,
199223
destination: &Path,
@@ -547,7 +571,7 @@ impl Renderer for HtmlHandlebars {
547571
let rendered = handlebars.render("index", &data)?;
548572

549573
let rendered =
550-
self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
574+
self.post_process_print(rendered, &html_config.playground, ctx.config.rust.edition);
551575

552576
utils::fs::write_file(destination, "print.html", rendered.as_bytes())?;
553577
debug!("Creating print.html ✓");
@@ -746,7 +770,7 @@ fn make_data(
746770

747771
/// Goes through the rendered HTML, making sure all header tags have
748772
/// an anchor respectively so people can link to sections directly.
749-
fn build_header_links(html: &str) -> String {
773+
fn build_header_links(html: &str, path_id: Option<&str>) -> String {
750774
let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
751775
let mut id_counter = HashMap::new();
752776

@@ -756,25 +780,40 @@ fn build_header_links(html: &str) -> String {
756780
.parse()
757781
.expect("Regex should ensure we only ever get numbers here");
758782

759-
insert_link_into_header(level, &caps[2], &mut id_counter)
783+
insert_link_into_header(level, &caps[2], &mut id_counter, path_id)
760784
})
761785
.into_owned()
762786
}
763787

764788
/// Insert a sinle link into a header, making sure each link gets its own
765789
/// unique ID by appending an auto-incremented number (if necessary).
790+
///
791+
/// For `print.html`, we will add a path id prefix.
766792
fn insert_link_into_header(
767793
level: usize,
768794
content: &str,
769795
id_counter: &mut HashMap<String, usize>,
796+
path_id: Option<&str>,
770797
) -> String {
771798
let raw_id = utils::id_from_content(content);
772799

773800
let id_count = id_counter.entry(raw_id.clone()).or_insert(0);
774801

775802
let id = match *id_count {
776-
0 => raw_id,
777-
other => format!("{}-{}", raw_id, other),
803+
0 => {
804+
if let Some(path_id) = path_id {
805+
format!("{}-{}", path_id, raw_id)
806+
} else {
807+
raw_id
808+
}
809+
}
810+
other => {
811+
if let Some(path_id) = path_id {
812+
format!("{}-{}-{}", path_id, raw_id, other)
813+
} else {
814+
format!("{}-{}", raw_id, other)
815+
}
816+
}
778817
};
779818

780819
*id_count += 1;
@@ -980,7 +1019,7 @@ mod tests {
9801019
];
9811020

9821021
for (src, should_be) in inputs {
983-
let got = build_header_links(src);
1022+
let got = build_header_links(src, None);
9841023
assert_eq!(got, should_be);
9851024
}
9861025
}

src/utils/mod.rs

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag};
1010

1111
use std::borrow::Cow;
1212
use std::fmt::Write;
13-
use std::path::Path;
13+
use std::path::{Component, Path, PathBuf};
1414

1515
pub use self::string::{
1616
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
@@ -63,19 +63,55 @@ pub fn id_from_content(content: &str) -> String {
6363
normalize_id(trimmed)
6464
}
6565

66+
/// https://stackoverflow.com/a/68233480
67+
/// Improve the path to try remove and solve .. token. Return the path id
68+
/// by replacing the directory separator with a hyphen.
69+
///
70+
/// This assumes that `a/b/../c` is `a/c` which might be different from
71+
/// what the OS would have chosen when b is a link. This is OK
72+
/// for broot verb arguments but can't be generally used elsewhere
73+
///
74+
/// This function ensures a given path ending with '/' will
75+
/// end with '-' after normalization.
76+
pub fn normalize_path_id<P: AsRef<Path>>(path: P) -> String {
77+
let ends_with_slash = path.as_ref().to_str().map_or(false, |s| s.ends_with('/'));
78+
let mut normalized = PathBuf::new();
79+
for component in path.as_ref().components() {
80+
match &component {
81+
Component::ParentDir => {
82+
if !normalized.pop() {
83+
normalized.push(component);
84+
}
85+
}
86+
_ => {
87+
normalized.push(component);
88+
}
89+
}
90+
}
91+
if ends_with_slash {
92+
normalized.push("");
93+
}
94+
normalized
95+
.to_str()
96+
.unwrap()
97+
.replace("\\", "-")
98+
.replace("/", "-")
99+
}
100+
66101
/// Fix links to the correct location.
67102
///
68103
/// This adjusts links, such as turning `.md` extensions to `.html`.
69104
///
70105
/// `path` is the path to the page being rendered relative to the root of the
71106
/// book. This is used for the `print.html` page so that links on the print
72-
/// page go to the original location. Normal page rendering sets `path` to
73-
/// None. Ideally, print page links would link to anchors on the print page,
74-
/// but that is very difficult.
107+
/// page go to the anchors that has a path id prefix. Normal page rendering
108+
/// sets `path` to None.
75109
fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
76110
lazy_static! {
77111
static ref SCHEME_LINK: Regex = Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap();
78112
static ref MD_LINK: Regex = Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap();
113+
static ref HTML_MD_LINK: Regex =
114+
Regex::new(r"(?P<link>.*)\.(html|md)(?P<anchor>#.*)?").unwrap();
79115
}
80116

81117
fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
@@ -84,9 +120,9 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
84120
if let Some(path) = path {
85121
let mut base = path.display().to_string();
86122
if base.ends_with(".md") {
87-
base.replace_range(base.len() - 3.., ".html");
123+
base.replace_range(base.len() - 3.., "");
88124
}
89-
return format!("{}{}", base, dest).into();
125+
return format!("#{}{}", normalize_path_id(base), dest.replace("#", "-")).into();
90126
} else {
91127
return dest;
92128
}
@@ -104,18 +140,34 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
104140
if !base.is_empty() {
105141
write!(fixed_link, "{}/", base).unwrap();
106142
}
107-
}
108143

109-
if let Some(caps) = MD_LINK.captures(&dest) {
110-
fixed_link.push_str(&caps["link"]);
111-
fixed_link.push_str(".html");
112-
if let Some(anchor) = caps.name("anchor") {
113-
fixed_link.push_str(anchor.as_str());
114-
}
144+
// In `print.html`, print page links would all link to anchors on the print page.
145+
if let Some(caps) = HTML_MD_LINK.captures(&dest) {
146+
fixed_link.push_str(&caps["link"]);
147+
if let Some(anchor) = caps.name("anchor") {
148+
fixed_link.push_str(anchor.as_str());
149+
}
150+
} else {
151+
fixed_link.push_str(&dest);
152+
};
153+
154+
let mut fixed_anchor_for_print = String::new();
155+
fixed_anchor_for_print.push_str("#");
156+
fixed_anchor_for_print.push_str(&normalize_path_id(&fixed_link).replace("#", "-"));
157+
return CowStr::from(fixed_anchor_for_print);
115158
} else {
116-
fixed_link.push_str(&dest);
117-
};
118-
return CowStr::from(fixed_link);
159+
// In normal page rendering, links to anchors on another page.
160+
if let Some(caps) = MD_LINK.captures(&dest) {
161+
fixed_link.push_str(&caps["link"]);
162+
fixed_link.push_str(".html");
163+
if let Some(anchor) = caps.name("anchor") {
164+
fixed_link.push_str(anchor.as_str());
165+
}
166+
} else {
167+
fixed_link.push_str(&dest);
168+
};
169+
return CowStr::from(fixed_link);
170+
}
119171
}
120172
dest
121173
}

0 commit comments

Comments
 (0)