Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions crates/mdbook-html/src/html/print.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use super::Node;
use crate::html::{ChapterTree, Element, serialize};
use crate::utils::{ToUrlPath, id_from_content, normalize_path, unique_id};
use crate::utils::{ToUrlPath, id_from_content_or_fallback, normalize_path, unique_id};
use mdbook_core::static_regex;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
Expand Down Expand Up @@ -103,8 +103,7 @@ fn make_root_id_map(
// TODO: This might want to be a warning? Chapters generally
// should start with an h1.
let mut h1 = Element::new("h1");
let id = id_from_content(&chapter.name);
let id = unique_id(&id, id_counter);
let id = id_from_content_or_fallback(&chapter.name, id_counter);
h1.insert_attr("id", id.clone().into());
let mut root = tree.root_mut();
let mut h1 = root.prepend(Node::Element(h1));
Expand Down
5 changes: 2 additions & 3 deletions crates/mdbook-html/src/html/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

use super::tokenizer::parse_html;
use super::{HtmlRenderOptions, hide_lines, wrap_rust_main};
use crate::utils::{id_from_content, unique_id};
use crate::utils::id_from_content_or_fallback;
use ego_tree::{NodeId, NodeRef, Tree};
use html5ever::tendril::StrTendril;
use html5ever::tokenizer::{TagKind, Token};
Expand Down Expand Up @@ -924,8 +924,7 @@ where
let node_id = node.id();
let node_ref = self.tree.get(node_id).unwrap();
text_in_node(node_ref, &mut id);
let id = id_from_content(&id);
let id = unique_id(&id, &mut id_counter);
let id = id_from_content_or_fallback(&id, &mut id_counter);
let mut node = self.tree.get_mut(heading).unwrap();
let el = node.value().as_element_mut().unwrap();
let href = format!("#{id}");
Expand Down
21 changes: 21 additions & 0 deletions crates/mdbook-html/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,19 @@ pub(crate) fn id_from_content(content: &str) -> String {
.collect()
}

/// Returns a non-empty HTML id for heading content.
///
/// Headings that contain only punctuation (e.g. `## ::`) produce an empty slug via
/// [`id_from_content`]; this helper falls back to a generic prefix so anchor links work.
pub(crate) fn id_from_content_or_fallback(content: &str, used: &mut HashSet<String>) -> String {
let id = id_from_content(content);
if id.is_empty() {
unique_id("section", used)
} else {
unique_id(&id, used)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -129,7 +142,15 @@ mod tests {
assert_eq!(id_from_content("にほんご"), "にほんご");
assert_eq!(id_from_content("한국어"), "한국어");
assert_eq!(id_from_content(""), "");
assert_eq!(id_from_content("::"), "");
assert_eq!(id_from_content("中文標題 CJK title"), "中文標題-cjk-title");
assert_eq!(id_from_content("Über"), "über");
}

#[test]
fn empty_heading_gets_fallback_id() {
let mut used = HashSet::new();
assert_eq!(id_from_content_or_fallback("::", &mut used), "section");
assert_eq!(id_from_content_or_fallback("::", &mut used), "section-1");
}
}
Loading