Merge pull request #2013 from ImUrX/heading-extension
Add heading extension support
This commit is contained in:
commit
3a51abfcad
|
@ -220,3 +220,16 @@ To enable it, see the [`output.html.curly-quotes`] config option.
|
|||
[tables]: https://github.github.com/gfm/#tables-extension-
|
||||
[task list extension]: https://github.github.com/gfm/#task-list-items-extension-
|
||||
[`output.html.curly-quotes`]: configuration/renderers.md#html-renderer-options
|
||||
|
||||
### Heading attributes
|
||||
|
||||
Headings can have a custom HTML ID and classes. This let's you maintain the same ID even if you change the heading's text, it also let's you add multiple classes in the heading.
|
||||
|
||||
Example:
|
||||
```md
|
||||
# Example heading { #first .class1 .class2 }
|
||||
```
|
||||
|
||||
This makes the level 1 heading with the content `Example heading`, ID `first`, and classes `class1` and `class2`. Note that the attributes should be space-separated.
|
||||
|
||||
More information can be found in the [heading attrs spec page](https://github.com/raphlinus/pulldown-cmark/blob/master/specs/heading_attrs.txt).
|
||||
|
|
|
@ -789,8 +789,10 @@ fn make_data(
|
|||
/// Goes through the rendered HTML, making sure all header tags have
|
||||
/// an anchor respectively so people can link to sections directly.
|
||||
fn build_header_links(html: &str) -> String {
|
||||
static BUILD_HEADER_LINKS: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap());
|
||||
static BUILD_HEADER_LINKS: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r#"<h(\d)(?: id="([^"]+)")?(?: class="([^"]+)")?>(.*?)</h\d>"#).unwrap()
|
||||
});
|
||||
static IGNORE_CLASS: &[&str] = &["menu-title"];
|
||||
|
||||
let mut id_counter = HashMap::new();
|
||||
|
||||
|
@ -800,7 +802,22 @@ fn build_header_links(html: &str) -> String {
|
|||
.parse()
|
||||
.expect("Regex should ensure we only ever get numbers here");
|
||||
|
||||
insert_link_into_header(level, &caps[2], &mut id_counter)
|
||||
// Ignore .menu-title because now it's getting detected by the regex.
|
||||
if let Some(classes) = caps.get(3) {
|
||||
for class in classes.as_str().split(" ") {
|
||||
if IGNORE_CLASS.contains(&class) {
|
||||
return caps[0].to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
insert_link_into_header(
|
||||
level,
|
||||
&caps[4],
|
||||
caps.get(2).map(|x| x.as_str().to_string()),
|
||||
caps.get(3).map(|x| x.as_str().to_string()),
|
||||
&mut id_counter,
|
||||
)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
@ -810,15 +827,21 @@ fn build_header_links(html: &str) -> String {
|
|||
fn insert_link_into_header(
|
||||
level: usize,
|
||||
content: &str,
|
||||
id: Option<String>,
|
||||
classes: Option<String>,
|
||||
id_counter: &mut HashMap<String, usize>,
|
||||
) -> String {
|
||||
let id = utils::unique_id_from_content(content, id_counter);
|
||||
let id = id.unwrap_or_else(|| utils::unique_id_from_content(content, id_counter));
|
||||
let classes = classes
|
||||
.map(|s| format!(" class=\"{s}\""))
|
||||
.unwrap_or_default();
|
||||
|
||||
format!(
|
||||
r##"<h{level} id="{id}"><a class="header" href="#{id}">{text}</a></h{level}>"##,
|
||||
r##"<h{level} id="{id}"{classes}><a class="header" href="#{id}">{text}</a></h{level}>"##,
|
||||
level = level,
|
||||
id = id,
|
||||
text = content
|
||||
text = content,
|
||||
classes = classes
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1015,6 +1038,21 @@ mod tests {
|
|||
"<h1>Foo</h1><h3>Foo</h3>",
|
||||
r##"<h1 id="foo"><a class="header" href="#foo">Foo</a></h1><h3 id="foo-1"><a class="header" href="#foo-1">Foo</a></h3>"##,
|
||||
),
|
||||
// id only
|
||||
(
|
||||
r##"<h1 id="foobar">Foo</h1>"##,
|
||||
r##"<h1 id="foobar"><a class="header" href="#foobar">Foo</a></h1>"##,
|
||||
),
|
||||
// class only
|
||||
(
|
||||
r##"<h1 class="class1 class2">Foo</h1>"##,
|
||||
r##"<h1 id="foo" class="class1 class2"><a class="header" href="#foo">Foo</a></h1>"##,
|
||||
),
|
||||
// both id and class
|
||||
(
|
||||
r##"<h1 id="foobar" class="class1 class2">Foo</h1>"##,
|
||||
r##"<h1 id="foobar" class="class1 class2"><a class="header" href="#foobar">Foo</a></h1>"##,
|
||||
),
|
||||
];
|
||||
|
||||
for (src, should_be) in inputs {
|
||||
|
|
|
@ -138,9 +138,11 @@ fn render_item(
|
|||
|
||||
in_heading = true;
|
||||
}
|
||||
Event::End(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => {
|
||||
Event::End(Tag::Heading(i, id, _classes)) if i as u32 <= max_section_depth => {
|
||||
in_heading = false;
|
||||
section_id = Some(utils::unique_id_from_content(&heading, &mut id_counter));
|
||||
section_id = id
|
||||
.map(|id| id.to_string())
|
||||
.or_else(|| Some(utils::unique_id_from_content(&heading, &mut id_counter)));
|
||||
breadcrumbs.push(heading.clone());
|
||||
}
|
||||
Event::Start(Tag::FootnoteDefinition(name)) => {
|
||||
|
|
|
@ -183,6 +183,7 @@ pub fn new_cmark_parser(text: &str, curly_quotes: bool) -> Parser<'_, '_> {
|
|||
opts.insert(Options::ENABLE_FOOTNOTES);
|
||||
opts.insert(Options::ENABLE_STRIKETHROUGH);
|
||||
opts.insert(Options::ENABLE_TASKLISTS);
|
||||
opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
|
||||
if curly_quotes {
|
||||
opts.insert(Options::ENABLE_SMART_PUNCTUATION);
|
||||
}
|
||||
|
|
|
@ -13,3 +13,9 @@
|
|||
##### Really Small Heading
|
||||
|
||||
###### Is it even a heading anymore - heading
|
||||
|
||||
## Custom id {#example-id}
|
||||
|
||||
## Custom class {.class1 .class2}
|
||||
|
||||
## Both id and class {#example-id2 .class1 .class2}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
- [Unicode](first/unicode.md)
|
||||
- [No Headers](first/no-headers.md)
|
||||
- [Duplicate Headers](first/duplicate-headers.md)
|
||||
- [Heading Attributes](first/heading-attributes.md)
|
||||
- [Second Chapter](second.md)
|
||||
- [Nested Chapter](second/nested.md)
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
# Heading Attributes {#attrs}
|
||||
|
||||
## Heading with classes {.class1 .class2}
|
||||
|
||||
## Heading with id and classes {#both .class1 .class2}
|
|
@ -35,6 +35,7 @@ const TOC_SECOND_LEVEL: &[&str] = &[
|
|||
"1.5. Unicode",
|
||||
"1.6. No Headers",
|
||||
"1.7. Duplicate Headers",
|
||||
"1.8. Heading Attributes",
|
||||
"2.1. Nested Chapter",
|
||||
];
|
||||
|
||||
|
@ -754,6 +755,7 @@ mod search {
|
|||
let no_headers = get_doc_ref("first/no-headers.html");
|
||||
let duplicate_headers_1 = get_doc_ref("first/duplicate-headers.html#header-text-1");
|
||||
let conclusion = get_doc_ref("conclusion.html#conclusion");
|
||||
let heading_attrs = get_doc_ref("first/heading-attributes.html#both");
|
||||
|
||||
let bodyidx = &index["index"]["index"]["body"]["root"];
|
||||
let textidx = &bodyidx["t"]["e"]["x"]["t"];
|
||||
|
@ -766,7 +768,7 @@ mod search {
|
|||
assert_eq!(docs[&some_section]["body"], "");
|
||||
assert_eq!(
|
||||
docs[&summary]["body"],
|
||||
"Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Markdown Unicode No Headers Duplicate Headers Second Chapter Nested Chapter Conclusion"
|
||||
"Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Markdown Unicode No Headers Duplicate Headers Heading Attributes Second Chapter Nested Chapter Conclusion"
|
||||
);
|
||||
assert_eq!(
|
||||
docs[&summary]["breadcrumbs"],
|
||||
|
@ -785,6 +787,10 @@ mod search {
|
|||
docs[&no_headers]["body"],
|
||||
"Capybara capybara capybara. Capybara capybara capybara. ThisLongWordIsIncludedSoWeCanCheckThatSufficientlyLongWordsAreOmittedFromTheSearchIndex."
|
||||
);
|
||||
assert_eq!(
|
||||
docs[&heading_attrs]["breadcrumbs"],
|
||||
"First Chapter » Heading Attributes » Heading with id and classes"
|
||||
);
|
||||
}
|
||||
|
||||
// Setting this to `true` may cause issues with `cargo watch`,
|
||||
|
@ -946,3 +952,19 @@ fn custom_fonts() {
|
|||
&["fonts.css", "myfont.woff"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_header_attributes() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let contents = temp.path().join("book/first/heading-attributes.html");
|
||||
|
||||
let summary_strings = &[
|
||||
r##"<h1 id="attrs"><a class="header" href="#attrs">Heading Attributes</a></h1>"##,
|
||||
r##"<h2 id="heading-with-classes" class="class1 class2"><a class="header" href="#heading-with-classes">Heading with classes</a></h2>"##,
|
||||
r##"<h2 id="both" class="class1 class2"><a class="header" href="#both">Heading with id and classes</a></h2>"##,
|
||||
];
|
||||
assert_contains_strings(&contents, summary_strings);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue