Skip to content

Commit 99021e9

Browse files
authored
Merge pull request #776 from miketheman/miketheman/fix-header-ids-links
Deprecate `header_ids` in favor of `header_id_prefix`, add `header_id_prefix_in_href`
2 parents ea026ef + ec6f4e4 commit 99021e9

File tree

13 files changed

+141
-44
lines changed

13 files changed

+141
-44
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,11 @@ Options:
136136
137137
[default: 0]
138138
139-
--header-ids <PREFIX>
140-
Use the Comrak header IDs extension, with the given ID prefix
139+
--header-id-prefix <PREFIX>
140+
Prefix generated header IDs with the given ID prefix
141+
142+
--header-id-prefix-in-href
143+
Apply the header ID prefix to the href anchor as well
141144
142145
--front-matter-delimiter <DELIMITER>
143146
Detect frontmatter that starts and ends with the given string, and do not include it in

fuzz/fuzz_targets/all_options.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ struct FuzzExtensionOptions {
5757
highlight: bool,
5858
phoenix_heex: bool,
5959
insert: bool,
60+
header_id_prefix_in_href: bool,
6061
// non-bool below
61-
header_ids: bool,
62+
header_id_prefix: bool,
6263
front_matter_delimiter: bool,
6364
image_url_rewriter: bool,
6465
link_url_rewriter: bool,
@@ -95,8 +96,9 @@ impl FuzzExtensionOptions {
9596
highlight: self.highlight,
9697
phoenix_heex: self.phoenix_heex,
9798
insert: self.insert,
99+
header_id_prefix_in_href: self.header_id_prefix_in_href,
98100
// non-bool below
99-
header_ids: if self.header_ids {
101+
header_id_prefix: if self.header_id_prefix {
100102
Some("user-content-".into())
101103
} else {
102104
None
@@ -116,6 +118,7 @@ impl FuzzExtensionOptions {
116118
} else {
117119
None
118120
},
121+
header_ids: None,
119122
}
120123
}
121124
}

fuzz/fuzz_targets/quadratic.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ impl FuzzExtensionOptions {
226226
greentext: self.greentext,
227227
alerts: self.alerts,
228228
front_matter_delimiter: None,
229-
header_ids: None,
229+
header_id_prefix: None,
230230
..Default::default()
231231
}
232232
}

src/html.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -575,13 +575,18 @@ fn render_heading<T>(
575575
render_sourcepos(context, node)?;
576576
context.write_str(">")?;
577577

578-
if let Some(ref prefix) = context.options.extension.header_ids {
578+
if let Some(prefix) = context.options.extension.effective_header_id_prefix() {
579579
let text_content = collect_text(node);
580580
let id = context.anchorizer.anchorize(&text_content);
581+
let href_prefix = if context.options.extension.header_id_prefix_in_href {
582+
prefix.as_str()
583+
} else {
584+
""
585+
};
581586
write!(
582587
context,
583-
"<a href=\"#{}\" aria-hidden=\"true\" class=\"anchor\" id=\"{}{}\"></a>",
584-
id, prefix, id
588+
"<a href=\"#{}{}\" aria-hidden=\"true\" class=\"anchor\" id=\"{}{}\"></a>",
589+
href_prefix, id, prefix, id
585590
)?;
586591
}
587592
} else {

src/main.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,16 @@ struct Cli {
130130

131131
/// Use the Comrak header IDs extension, with the given ID prefix
132132
#[arg(long, value_name = "PREFIX", required = false)]
133+
header_id_prefix: Option<String>,
134+
135+
/// Deprecated: use --header-id-prefix instead
136+
#[arg(long, value_name = "PREFIX", required = false, hide = true)]
133137
header_ids: Option<String>,
134138

139+
/// Apply the header ID prefix to the href anchor as well
140+
#[arg(long)]
141+
header_id_prefix_in_href: bool,
142+
135143
/// Detect frontmatter that starts and ends with the given string, and do
136144
/// not include it in the resulting document
137145
#[arg(long, value_name = "DELIMITER", allow_hyphen_values = true)]
@@ -269,6 +277,10 @@ fn main() -> Result<(), Box<dyn Error>> {
269277
}
270278
}
271279

280+
if cli.header_ids.is_some() {
281+
eprintln!("warning: --header-ids is deprecated, use --header-id-prefix instead");
282+
}
283+
272284
let exts = &cli.extensions;
273285

274286
let extension = options::Extension::builder()
@@ -278,7 +290,8 @@ fn main() -> Result<(), Box<dyn Error>> {
278290
.autolink(exts.contains(&Extension::Autolink) || cli.gfm)
279291
.tasklist(exts.contains(&Extension::Tasklist) || cli.gfm)
280292
.superscript(exts.contains(&Extension::Superscript))
281-
.maybe_header_ids(cli.header_ids)
293+
.maybe_header_id_prefix(cli.header_id_prefix.or(cli.header_ids))
294+
.header_id_prefix_in_href(cli.header_id_prefix_in_href)
282295
.footnotes(exts.contains(&Extension::Footnotes))
283296
.inline_footnotes(exts.contains(&Extension::InlineFootnotes))
284297
.description_lists(exts.contains(&Extension::DescriptionLists))

src/parser/options.rs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,44 @@ pub struct Extension<'c> {
117117
#[cfg_attr(feature = "bon", builder(default))]
118118
pub superscript: bool,
119119

120-
/// Enables the header IDs Comrak extension.
120+
/// Enables the header IDs Comrak extension, with the given ID prefix.
121+
///
122+
/// When set, each heading gains an anchor element with an `id` attribute
123+
/// formed by prefixing the slugified heading text. This is useful for
124+
/// namespacing heading anchors to avoid collisions when rendered Markdown
125+
/// is embedded alongside other content on a page (e.g. GitHub uses the
126+
/// prefix `"user-content-"` for this purpose).
121127
///
122128
/// ```rust
123129
/// # use comrak::{markdown_to_html, Options};
124130
/// let mut options = Options::default();
125-
/// options.extension.header_ids = Some("user-content-".to_string());
131+
/// options.extension.header_id_prefix = Some("user-content-".to_string());
126132
/// assert_eq!(markdown_to_html("# README\n", &options),
127133
/// "<h1><a href=\"#readme\" aria-hidden=\"true\" class=\"anchor\" id=\"user-content-readme\"></a>README</h1>\n");
128134
/// ```
135+
pub header_id_prefix: Option<String>,
136+
137+
#[deprecated(since = "0.52.0", note = "renamed to `header_id_prefix`")]
138+
/// Deprecated: use [`header_id_prefix`](#structfield.header_id_prefix) instead.
139+
#[cfg_attr(feature = "bon", builder(skip))]
129140
pub header_ids: Option<String>,
130141

142+
/// When enabled alongside [`header_id_prefix`](#structfield.header_id_prefix), the header ID
143+
/// prefix is also applied to the `href` anchor in the generated link.
144+
///
145+
/// Has no effect if `header_id_prefix` is `None`.
146+
///
147+
/// ```rust
148+
/// # use comrak::{markdown_to_html, Options};
149+
/// let mut options = Options::default();
150+
/// options.extension.header_id_prefix = Some("user-content-".to_string());
151+
/// options.extension.header_id_prefix_in_href = true;
152+
/// assert_eq!(markdown_to_html("# README\n", &options),
153+
/// "<h1><a href=\"#user-content-readme\" aria-hidden=\"true\" class=\"anchor\" id=\"user-content-readme\"></a>README</h1>\n");
154+
/// ```
155+
#[cfg_attr(feature = "bon", builder(default))]
156+
pub header_id_prefix_in_href: bool,
157+
131158
/// Enables the footnotes extension per `cmark-gfm`.
132159
///
133160
/// For usage, see `src/tests.rs`. The extension is modelled after
@@ -578,6 +605,15 @@ pub struct Extension<'c> {
578605
}
579606

580607
impl Extension<'_> {
608+
/// Returns the effective header ID prefix, preferring [`header_id_prefix`] over the
609+
/// deprecated [`header_ids`].
610+
///
611+
/// TODO: Remove this method when `header_ids` is removed.
612+
#[allow(deprecated)]
613+
pub(crate) fn effective_header_id_prefix(&self) -> Option<&String> {
614+
self.header_id_prefix.as_ref().or(self.header_ids.as_ref())
615+
}
616+
581617
pub(crate) fn wikilinks(&self) -> Option<WikiLinksMode> {
582618
match (
583619
self.wikilinks_title_before_pipe,

src/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ mod footnotes;
2121
mod front_matter;
2222
mod fuzz;
2323
mod greentext;
24-
mod header_ids;
24+
mod header_id_prefix;
2525
mod highlight;
2626
#[path = "tests/html.rs"]
2727
mod html_;

src/tests/header_id_prefix.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use super::*;
2+
3+
#[test]
4+
fn header_id_prefix() {
5+
html_opts_i(
6+
concat!(
7+
"# Hi.\n",
8+
"## Hi 1.\n",
9+
"### Hi.\n",
10+
"#### Hello.\n",
11+
"##### Hi.\n",
12+
"###### Hello.\n",
13+
"# Isn't it grand?"
14+
),
15+
concat!(
16+
"<h1><a href=\"#hi\" aria-hidden=\"true\" class=\"anchor\" id=\"user-content-hi\"></a>Hi.</h1>\n",
17+
"<h2><a href=\"#hi-1\" aria-hidden=\"true\" class=\"anchor\" id=\"user-content-hi-1\"></a>Hi 1.</h2>\n",
18+
"<h3><a href=\"#hi-2\" aria-hidden=\"true\" class=\"anchor\" id=\"user-content-hi-2\"></a>Hi.</h3>\n",
19+
"<h4><a href=\"#hello\" aria-hidden=\"true\" class=\"anchor\" id=\"user-content-hello\"></a>Hello.</h4>\n",
20+
"<h5><a href=\"#hi-3\" aria-hidden=\"true\" class=\"anchor\" id=\"user-content-hi-3\"></a>Hi.</h5>\n",
21+
"<h6><a href=\"#hello-1\" aria-hidden=\"true\" class=\"anchor\" id=\"user-content-hello-1\"></a>Hello.</h6>\n",
22+
"<h1><a href=\"#isnt-it-grand\" aria-hidden=\"true\" class=\"anchor\" id=\"user-content-isnt-it-grand\"></a>Isn't it grand?</h1>\n"
23+
),
24+
true,
25+
|opts| opts.extension.header_id_prefix = Some("user-content-".to_owned()),
26+
);
27+
}
28+
29+
#[test]
30+
fn header_ids_prefix_in_href() {
31+
html_opts_i(
32+
concat!(
33+
"# Hi.\n",
34+
"## Hi 1.\n",
35+
"### Hi.\n",
36+
"#### Hello.\n",
37+
"##### Hi.\n",
38+
"###### Hello.\n",
39+
"# Isn't it grand?"
40+
),
41+
concat!(
42+
"<h1><a href=\"#user-content-hi\" aria-hidden=\"true\" class=\"anchor\" id=\"user-content-hi\"></a>Hi.</h1>\n",
43+
"<h2><a href=\"#user-content-hi-1\" aria-hidden=\"true\" class=\"anchor\" id=\"user-content-hi-1\"></a>Hi 1.</h2>\n",
44+
"<h3><a href=\"#user-content-hi-2\" aria-hidden=\"true\" class=\"anchor\" id=\"user-content-hi-2\"></a>Hi.</h3>\n",
45+
"<h4><a href=\"#user-content-hello\" aria-hidden=\"true\" class=\"anchor\" id=\"user-content-hello\"></a>Hello.</h4>\n",
46+
"<h5><a href=\"#user-content-hi-3\" aria-hidden=\"true\" class=\"anchor\" id=\"user-content-hi-3\"></a>Hi.</h5>\n",
47+
"<h6><a href=\"#user-content-hello-1\" aria-hidden=\"true\" class=\"anchor\" id=\"user-content-hello-1\"></a>Hello.</h6>\n",
48+
"<h1><a href=\"#user-content-isnt-it-grand\" aria-hidden=\"true\" class=\"anchor\" id=\"user-content-isnt-it-grand\"></a>Isn't it grand?</h1>\n"
49+
),
50+
true,
51+
|opts| {
52+
opts.extension.header_id_prefix = Some("user-content-".to_owned());
53+
opts.extension.header_id_prefix_in_href = true;
54+
},
55+
);
56+
}
57+
58+
#[test]
59+
fn header_id_prefix_in_href_without_prefix() {
60+
// When header_id_prefix is None, header_id_prefix_in_href has no effect
61+
html_opts_i("# Hi.\n", "<h1>Hi.</h1>\n", true, |opts| {
62+
opts.extension.header_id_prefix_in_href = true;
63+
});
64+
}

src/tests/header_ids.rs

Lines changed: 0 additions & 27 deletions
This file was deleted.

src/tests/typst.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,9 +287,9 @@ fn wikilinks_use_their_rendered_label() {
287287
}
288288

289289
#[test]
290-
fn heading_labels_follow_header_ids() {
290+
fn heading_labels_follow_header_id_prefix() {
291291
typst_opts("# Intro\n", "= Intro <sec:intro>\n", |opts| {
292-
opts.extension.header_ids = Some("sec:".to_owned());
292+
opts.extension.header_id_prefix = Some("sec:".to_owned());
293293
});
294294
}
295295

0 commit comments

Comments
 (0)