Skip to content

Commit 1baf2ef

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 1baf2ef

File tree

2 files changed

+114
-25
lines changed

2 files changed

+114
-25
lines changed

src/renderer/html_handlebars/hbs_renderer.rs

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,20 @@ 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
78+
.push_str(&(format!(r#"<div id="{}"></div>"#, &path_id)));
79+
print_content.push_str(&build_header_links(&fixed_content, Some(path_id)));
6780

6881
// Update the context with data for this file
6982
let ctx_path = path
@@ -181,19 +194,31 @@ impl HtmlHandlebars {
181194
}
182195

183196
#[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))]
184-
fn post_process(
197+
fn post_process_print(
185198
&self,
186199
rendered: String,
187200
playground_config: &Playground,
188201
edition: Option<RustEdition>,
189202
) -> String {
190-
let rendered = build_header_links(&rendered);
191203
let rendered = fix_code_blocks(&rendered);
192204
let rendered = add_playground_pre(&rendered, playground_config, edition);
193205

194206
rendered
195207
}
196208

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

549574
let rendered =
550-
self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
575+
self.post_process_print(rendered, &html_config.playground, ctx.config.rust.edition);
551576

552577
utils::fs::write_file(destination, "print.html", rendered.as_bytes())?;
553578
debug!("Creating print.html ✓");
@@ -746,7 +771,7 @@ fn make_data(
746771

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

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

759-
insert_link_into_header(level, &caps[2], &mut id_counter)
784+
insert_link_into_header(level, &caps[2], &mut id_counter, path_id)
760785
})
761786
.into_owned()
762787
}
763788

764789
/// Insert a sinle link into a header, making sure each link gets its own
765790
/// unique ID by appending an auto-incremented number (if necessary).
791+
///
792+
/// For `print.html`, we will add a path id prefix.
766793
fn insert_link_into_header(
767794
level: usize,
768795
content: &str,
769796
id_counter: &mut HashMap<String, usize>,
797+
path_id: Option<&str>,
770798
) -> String {
771799
let raw_id = utils::id_from_content(content);
772800

773801
let id_count = id_counter.entry(raw_id.clone()).or_insert(0);
774802

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

780820
*id_count += 1;
@@ -980,7 +1020,7 @@ mod tests {
9801020
];
9811021

9821022
for (src, should_be) in inputs {
983-
let got = build_header_links(src);
1023+
let got = build_header_links(src, None);
9841024
assert_eq!(got, should_be);
9851025
}
9861026
}

src/utils/mod.rs

Lines changed: 65 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::{Path, PathBuf, Component};
1414

1515
pub use self::string::{
1616
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
@@ -63,19 +63,52 @@ 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()
78+
.to_str()
79+
.map_or(false, |s| s.ends_with('/'));
80+
let mut normalized = PathBuf::new();
81+
for component in path.as_ref().components() {
82+
match &component {
83+
Component::ParentDir => {
84+
if !normalized.pop() {
85+
normalized.push(component);
86+
}
87+
}
88+
_ => {
89+
normalized.push(component);
90+
}
91+
}
92+
}
93+
if ends_with_slash {
94+
normalized.push("");
95+
}
96+
normalized.to_str().unwrap().replace("\\", "-").replace("/", "-")
97+
}
98+
6699
/// Fix links to the correct location.
67100
///
68101
/// This adjusts links, such as turning `.md` extensions to `.html`.
69102
///
70103
/// `path` is the path to the page being rendered relative to the root of the
71104
/// 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.
105+
/// page go to the anchors that has a path id prefix. Normal page rendering
106+
/// sets `path` to None.
75107
fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
76108
lazy_static! {
77109
static ref SCHEME_LINK: Regex = Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap();
78110
static ref MD_LINK: Regex = Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap();
111+
static ref HTML_MD_LINK: Regex = Regex::new(r"(?P<link>.*)\.(html|md)(?P<anchor>#.*)?").unwrap();
79112
}
80113

81114
fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
@@ -84,9 +117,9 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
84117
if let Some(path) = path {
85118
let mut base = path.display().to_string();
86119
if base.ends_with(".md") {
87-
base.replace_range(base.len() - 3.., ".html");
120+
base.replace_range(base.len() - 3.., "");
88121
}
89-
return format!("{}{}", base, dest).into();
122+
return format!("#{}{}", normalize_path_id(base), dest.replace("#", "-")).into();
90123
} else {
91124
return dest;
92125
}
@@ -104,18 +137,34 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
104137
if !base.is_empty() {
105138
write!(fixed_link, "{}/", base).unwrap();
106139
}
107-
}
108140

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-
}
141+
// In `print.html`, print page links would all link to anchors on the print page.
142+
if let Some(caps) = HTML_MD_LINK.captures(&dest) {
143+
fixed_link.push_str(&caps["link"]);
144+
if let Some(anchor) = caps.name("anchor") {
145+
fixed_link.push_str(anchor.as_str());
146+
}
147+
} else {
148+
fixed_link.push_str(&dest);
149+
};
150+
151+
let mut fixed_anchor_for_print = String::new();
152+
fixed_anchor_for_print.push_str("#");
153+
fixed_anchor_for_print.push_str(&normalize_path_id(&fixed_link).replace("#", "-"));
154+
return CowStr::from(fixed_anchor_for_print);
115155
} else {
116-
fixed_link.push_str(&dest);
117-
};
118-
return CowStr::from(fixed_link);
156+
// In normal page rendering, links to anchors on another page.
157+
if let Some(caps) = MD_LINK.captures(&dest) {
158+
fixed_link.push_str(&caps["link"]);
159+
fixed_link.push_str(".html");
160+
if let Some(anchor) = caps.name("anchor") {
161+
fixed_link.push_str(anchor.as_str());
162+
}
163+
} else {
164+
fixed_link.push_str(&dest);
165+
};
166+
return CowStr::from(fixed_link);
167+
}
119168
}
120169
dest
121170
}

0 commit comments

Comments
 (0)