use regex::Regex;
use std::sync::LazyLock;
use unicode_normalization::UnicodeNormalization;
use super::common::{
DANGEROUS_UNICODE_PATTERN, MAX_INPUT_LENGTH, UnicodeLetterMode, ZERO_WIDTH_PATTERN, is_safe_unicode_letter,
};
static EMPHASIS_ASTERISK: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\*{1,3}([^*]+?)\*{1,3}").unwrap());
static EMPHASIS_UNDERSCORE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\b_{1,2}([^_\s][^_]*?)_{1,2}\b").unwrap());
static CODE_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"``([^`]{0,500})``|`([^`]{0,500})`").unwrap());
static IMAGE_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"!\[([^\]]*)\]\([^)]*\)").unwrap());
static LINK_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\[([^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*)\](?:\([^)]*\)|\[[^\]]*\])").unwrap());
static AMPERSAND_WITH_SPACES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\s+&\s+").unwrap());
static COPYRIGHT_WITH_SPACES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\s+©\s+").unwrap());
static HTML_TAG_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)</?[a-z][^>]*>").unwrap());
pub fn heading_to_fragment(heading: &str) -> String {
if heading.is_empty() {
return String::new();
}
if heading.len() > MAX_INPUT_LENGTH {
let mut truncated_len = 0;
for (byte_index, _) in heading.char_indices() {
if byte_index >= MAX_INPUT_LENGTH {
truncated_len = byte_index;
break;
}
truncated_len = byte_index + 1; }
if truncated_len == 0 {
truncated_len = MAX_INPUT_LENGTH.min(heading.len());
}
let truncated = &heading[..truncated_len];
return heading_to_fragment_internal(truncated);
}
heading_to_fragment_internal(heading)
}
fn heading_to_fragment_internal(heading: &str) -> String {
let normalized: String = heading.nfc().collect();
let emoji_processed = if normalized.chars().any(|c| {
let code = c as u32;
(0x1F300..=0x1F9FF).contains(&code) || (0x2600..=0x26FF).contains(&code) || (0x1F1E6..=0x1F1FF).contains(&code) }) {
process_emoji_sequences(&normalized)
} else {
normalized
};
let sanitized = sanitize_unicode(&emoji_processed);
let mut text = sanitized.to_lowercase();
if text.contains('*') || text.contains('_') || text.contains('`') || text.contains('[') {
let mut code_extracts: Vec<String> = Vec::new();
text = CODE_PATTERN
.replace_all(&text, |caps: ®ex::Captures| {
let idx = code_extracts.len();
let content = caps.get(1).or_else(|| caps.get(2)).map(|m| m.as_str()).unwrap_or("");
code_extracts.push(content.to_string());
format!("\x00CODE{idx}\x00")
})
.to_string();
for _ in 0..3 {
let prev = text.clone();
text = EMPHASIS_ASTERISK.replace_all(&text, "$1").to_string();
text = EMPHASIS_UNDERSCORE.replace_all(&text, "$1").to_string();
if text == prev {
break;
}
}
text = HTML_TAG_PATTERN.replace_all(&text, "").to_string();
for (idx, content) in code_extracts.into_iter().enumerate() {
text = text.replace(&format!("\x00CODE{idx}\x00"), &content);
}
text = IMAGE_PATTERN.replace_all(&text, "$1").to_string();
text = LINK_PATTERN.replace_all(&text, "$1").to_string();
} else if text.contains('<') {
text = HTML_TAG_PATTERN.replace_all(&text, "").to_string();
}
text = text.replace(" --> ", "----"); text = text.replace(" -->", "---"); text = text.replace("--> ", "---"); text = text.replace("-->", "--");
text = text.replace(" <-> ", "---"); text = text.replace(" <->", "--"); text = text.replace("<-> ", "--"); text = text.replace("<->", "-");
text = text.replace(" ==> ", "--"); text = text.replace(" ==>", "-"); text = text.replace("==> ", "-"); text = text.replace("==>", "");
text = text.replace(" -> ", "---"); text = text.replace(" ->", "--"); text = text.replace("-> ", "--"); text = text.replace("->", "-");
text = text.replace(['–', '—'], "");
if text.starts_with("& ") {
text = text.replacen("& ", "--", 1);
}
else if text.ends_with(" &") {
text = text[..text.len() - 2].to_string() + "-";
}
else {
text = AMPERSAND_WITH_SPACES.replace_all(&text, "--").to_string();
}
text = COPYRIGHT_WITH_SPACES.replace_all(&text, "--").to_string();
text = text.replace("&", "");
text = text.replace("©", "");
let mut result = String::with_capacity(text.len());
for c in text.chars() {
let code = c as u32;
if c.is_ascii_alphabetic() || c.is_ascii_digit() || c == '_' || c == '-' {
result.push(c);
} else if c == '§' {
result.push(c);
} else if code == 0x20E3 {
result.push(c);
} else if code == 0xFE0F {
if let Some(prev) = result.chars().last()
&& is_keycap_base(prev)
{
result.push(c);
}
} else if c.is_alphabetic() && is_safe_unicode_letter(c, UnicodeLetterMode::GitHub) {
result.push(c);
} else if c.is_numeric() {
result.push(c);
} else if c.is_whitespace() {
result.push('-');
}
}
if !result.contains("§emoji§") {
return result;
}
let mut final_result = result;
for count in (2..=10).rev() {
if final_result.contains("§emoji§") {
let marker_seq = "§emoji§".repeat(count);
if final_result.contains(&marker_seq) {
let replacement = "-".repeat(count + 1);
final_result = final_result.replace(&marker_seq, &replacement);
}
}
}
if final_result.contains("§emoji§") {
let bytes = final_result.as_bytes();
let marker = "§emoji§".as_bytes();
let mut result_bytes = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if i + marker.len() <= bytes.len() && &bytes[i..i + marker.len()] == marker {
let at_start = i == 0;
let at_end = i + marker.len() >= bytes.len();
if at_start || at_end {
result_bytes.push(b'-');
} else {
result_bytes.extend_from_slice(b"--");
}
i += marker.len();
} else {
result_bytes.push(bytes[i]);
i += 1;
}
}
final_result = String::from_utf8(result_bytes).unwrap_or(final_result);
}
final_result
}
fn process_emoji_sequences(input: &str) -> String {
let mut result = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
if is_emoji_or_symbol(c) || is_regional_indicator(c) {
if result.ends_with(' ') {
result.pop();
}
let mut symbol_count = 1;
if is_regional_indicator(c) {
if let Some(&next) = chars.peek()
&& is_regional_indicator(next)
{
chars.next(); }
}
else if is_emoji_or_symbol(c) {
while let Some(&next) = chars.peek() {
if next as u32 == 0x200D {
chars.next();
if let Some(&emoji) = chars.peek() {
if is_emoji_or_symbol(emoji) || is_regional_indicator(emoji) {
chars.next();
} else {
break;
}
}
} else if next as u32 == 0xFE0F {
chars.next();
} else if is_emoji_or_symbol(next) || is_regional_indicator(next) {
chars.next();
if is_regional_indicator(next)
&& let Some(&next2) = chars.peek()
&& is_regional_indicator(next2)
{
chars.next();
}
} else {
break;
}
}
}
while let Some(&next) = chars.peek() {
if next == ' ' {
let mut temp_chars = chars.clone();
temp_chars.next(); if let Some(&after_space) = temp_chars.peek() {
if is_emoji_or_symbol(after_space) || is_regional_indicator(after_space) {
chars.next(); let symbol = chars.next().unwrap(); symbol_count += 1;
if is_regional_indicator(symbol) {
if let Some(&next) = chars.peek()
&& is_regional_indicator(next)
{
chars.next();
}
} else if is_emoji_or_symbol(symbol) {
while let Some(&next) = chars.peek() {
if next as u32 == 0x200D {
chars.next();
if let Some(&emoji) = chars.peek() {
if is_emoji_or_symbol(emoji) || is_regional_indicator(emoji) {
chars.next();
} else {
break;
}
}
} else if next as u32 == 0xFE0F {
chars.next();
} else {
break;
}
}
}
} else {
break; }
} else {
break; }
} else {
break; }
}
if let Some(&next) = chars.peek()
&& next == ' '
{
chars.next();
}
result.push_str("§EMOJI§");
for _ in 1..symbol_count {
result.push_str("§EMOJI§");
}
}
else if is_keycap_base(c) {
let mut keycap_seq = String::new();
keycap_seq.push(c);
let mut has_keycap = false;
while let Some(&next) = chars.peek() {
if next as u32 == 0xFE0F || next as u32 == 0x20E3 {
keycap_seq.push(next);
chars.next();
if next as u32 == 0x20E3 {
has_keycap = true;
break;
}
} else {
break;
}
}
if has_keycap {
result.push_str(&keycap_seq);
} else {
result.push(c);
for ch in keycap_seq.chars().skip(1) {
result.push(ch);
}
}
} else {
result.push(c);
}
}
result
}
fn sanitize_unicode(input: &str) -> String {
let no_zero_width = ZERO_WIDTH_PATTERN.replace_all(input, "");
let no_bidi_attack = DANGEROUS_UNICODE_PATTERN.replace_all(&no_zero_width, "");
let mut sanitized = String::with_capacity(no_bidi_attack.len());
for c in no_bidi_attack.chars() {
if !c.is_control() || c.is_whitespace() {
sanitized.push(c);
}
}
sanitized
}
fn is_emoji_or_symbol(c: char) -> bool {
let code = c as u32;
if (0x202A..=0x202E).contains(&code) || (0x2066..=0x2069).contains(&code) || (0x200B..=0x200D).contains(&code) || (0x200E..=0x200F).contains(&code) || code == 0x061C || code == 0x2060 || code == 0xFEFF
{
return false;
}
(0x1F600..=0x1F64F).contains(&code) || (0x1F300..=0x1F5FF).contains(&code) || (0x1F680..=0x1F6FF).contains(&code) || (0x1F700..=0x1F77F).contains(&code) || (0x1F780..=0x1F7FF).contains(&code) || (0x1F800..=0x1F8FF).contains(&code) || (0x1F900..=0x1F9FF).contains(&code) || (0x1FA00..=0x1FA6F).contains(&code) || (0x1FA70..=0x1FAFF).contains(&code) || (0x1FB00..=0x1FBFF).contains(&code) ||
(0x2600..=0x26FF).contains(&code) || (0x2700..=0x27BF).contains(&code) || (0x2B00..=0x2BFF).contains(&code) || (0x1F000..=0x1F02F).contains(&code) || (0x1F030..=0x1F09F).contains(&code) || (0x1F0A0..=0x1F0FF).contains(&code) ||
(0x2190..=0x21FF).contains(&code) || (0x2200..=0x22FF).contains(&code) || (0x2300..=0x23FF).contains(&code) || (0x2400..=0x243F).contains(&code) || (0x2440..=0x245F).contains(&code) || (0x25A0..=0x25FF).contains(&code) || (0x2000..=0x206F).contains(&code) ||
(0x20D0..=0x20FF).contains(&code) }
fn is_regional_indicator(c: char) -> bool {
let code = c as u32;
(0x1F1E6..=0x1F1FF).contains(&code) }
fn is_keycap_base(c: char) -> bool {
let code = c as u32;
(0x0030..=0x0039).contains(&code) || code == 0x002A || code == 0x0023 }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_github_basic_cases() {
assert_eq!(heading_to_fragment("Hello World"), "hello-world");
assert_eq!(heading_to_fragment("Test Case"), "test-case");
assert_eq!(heading_to_fragment(""), "");
}
#[test]
fn test_github_underscores() {
assert_eq!(heading_to_fragment("test_with_underscores"), "test_with_underscores");
assert_eq!(heading_to_fragment("Update login_type"), "update-login_type");
assert_eq!(heading_to_fragment("__dunder__"), "dunder"); assert_eq!(heading_to_fragment("_emphasized_"), "emphasized"); assert_eq!(heading_to_fragment("__double__ underscore"), "double-underscore");
}
#[test]
fn test_github_arrows_issue_39() {
assert_eq!(
heading_to_fragment("cbrown --> sbrown: --unsafe-paths"),
"cbrown----sbrown---unsafe-paths"
);
assert_eq!(heading_to_fragment("cbrown -> sbrown"), "cbrown---sbrown");
assert_eq!(
heading_to_fragment("Arrow Test <-> bidirectional"),
"arrow-test---bidirectional"
);
assert_eq!(heading_to_fragment("Double Arrow ==> Test"), "double-arrow--test");
}
#[test]
fn test_github_hyphens() {
assert_eq!(heading_to_fragment("Double--Hyphen"), "double--hyphen");
assert_eq!(heading_to_fragment("Triple---Dash"), "triple---dash");
assert_eq!(
heading_to_fragment("Test---with---multiple---hyphens"),
"test---with---multiple---hyphens"
);
}
#[test]
fn test_github_special_symbols() {
assert_eq!(heading_to_fragment("Testing & Coverage"), "testing--coverage");
assert_eq!(heading_to_fragment("Copyright © 2024"), "copyright--2024");
assert_eq!(
heading_to_fragment("API::Response > Error--Handling"),
"apiresponse--error--handling"
);
}
#[test]
fn test_github_unicode() {
assert_eq!(heading_to_fragment("Café René"), "café-rené");
assert_eq!(heading_to_fragment("naïve résumé"), "naïve-résumé");
assert_eq!(heading_to_fragment("über uns"), "über-uns");
}
#[test]
fn test_github_emojis() {
assert_eq!(heading_to_fragment("Emoji 🎉 Party"), "emoji--party");
assert_eq!(heading_to_fragment("Test 🚀 Rocket"), "test--rocket");
}
#[test]
fn test_github_markdown_removal() {
assert_eq!(heading_to_fragment("*emphasized* text"), "emphasized-text");
assert_eq!(heading_to_fragment("`code` in heading"), "code-in-heading");
assert_eq!(heading_to_fragment("[link text](url)"), "link-text");
assert_eq!(heading_to_fragment("[ref link][]"), "ref-link");
}
#[test]
fn test_github_html_jsx_tag_stripping() {
assert_eq!(heading_to_fragment("retentionPolicy<Component />"), "retentionpolicy");
assert_eq!(
heading_to_fragment("retentionPolicy<HeaderTag type=\"danger\" text=\"required\" />"),
"retentionpolicy"
);
assert_eq!(heading_to_fragment("Test <span>extra</span>"), "test-extra");
assert_eq!(
heading_to_fragment("A <b>bold</b> and <i>italic</i>"),
"a-bold-and-italic"
);
assert_eq!(heading_to_fragment("`code`<Tag />"), "code");
assert_eq!(heading_to_fragment("Generic<T>"), "generic");
assert_eq!(heading_to_fragment("Text<br />More"), "textmore");
assert_eq!(
heading_to_fragment("Test <div><span>nested</span></div>"),
"test-nested"
);
assert_eq!(
heading_to_fragment("Arrow Test <-> bidirectional"),
"arrow-test---bidirectional"
);
}
#[test]
fn test_github_leading_trailing() {
assert_eq!(heading_to_fragment("---leading"), "---leading");
assert_eq!(heading_to_fragment("trailing---"), "trailing---");
assert_eq!(heading_to_fragment("---both---"), "---both---");
}
#[test]
fn test_github_numbers() {
assert_eq!(heading_to_fragment("Step 1: Getting Started"), "step-1-getting-started");
assert_eq!(heading_to_fragment("Version 2.1.0"), "version-210");
assert_eq!(heading_to_fragment("123 Numbers"), "123-numbers");
}
#[test]
fn test_github_comprehensive_verified() {
let test_cases = [
("GitHub Anchor Generation Test", "github-anchor-generation-test"),
(
"Test Case 1: cbrown --> sbrown: --unsafe-paths",
"test-case-1-cbrown----sbrown---unsafe-paths",
),
("Test Case 2: PHP $_REQUEST", "test-case-2-php-_request"),
("Test Case 3: Update login_type", "test-case-3-update-login_type"),
(
"Test Case 4: Test with: colons > and arrows",
"test-case-4-test-with-colons--and-arrows",
),
(
"Test Case 5: Test---with---multiple---hyphens",
"test-case-5-test---with---multiple---hyphens",
),
("Test Case 6: Simple test case", "test-case-6-simple-test-case"),
(
"Test Case 7: API::Response > Error--Handling",
"test-case-7-apiresponse--error--handling",
),
];
for (input, expected) in test_cases {
let actual = heading_to_fragment(input);
assert_eq!(
actual, expected,
"GitHub verified test failed for input: '{input}'\nExpected: '{expected}'\nActual: '{actual}'"
);
}
}
#[test]
fn test_security_input_size_limits() {
let large_input = "a".repeat(20000); let result = heading_to_fragment(&large_input);
assert!(result.len() <= MAX_INPUT_LENGTH);
assert_eq!(heading_to_fragment(""), "");
}
#[test]
fn test_security_unicode_normalization() {
let normal_cafe = "café"; let decomposed_cafe = "cafe\u{0301}";
let result1 = heading_to_fragment(normal_cafe);
let result2 = heading_to_fragment(decomposed_cafe);
assert_eq!(result1, result2);
assert_eq!(result1, "café");
}
#[test]
fn test_security_bidi_injection_prevention() {
let rtl_attack = "Hello\u{202E}dlroW\u{202D}";
let result = heading_to_fragment(rtl_attack);
assert_eq!(result, "hellodlrow");
let rlo_attack = "user\u{202E}@bank.com";
let result = heading_to_fragment(rlo_attack);
assert!(!result.contains('\u{202E}'));
let isolate_attack = "test\u{2066}hidden\u{2069}text";
let result = heading_to_fragment(isolate_attack);
assert_eq!(result, "testhiddentext"); }
#[test]
fn test_security_zero_width_character_removal() {
let zero_width_attack = "hel\u{200B}lo\u{200C}wor\u{200D}ld\u{FEFF}";
let result = heading_to_fragment(zero_width_attack);
assert_eq!(result, "helloworld");
let zwj_attack = "test\u{200D}text"; let result = heading_to_fragment(zwj_attack);
assert_eq!(result, "testtext");
let bom_attack = "test\u{FEFF}text"; let result = heading_to_fragment(bom_attack);
assert_eq!(result, "testtext");
}
#[test]
fn test_security_control_character_filtering() {
let control_chars = "test\x01\x02\x03\x1F text";
let result = heading_to_fragment(control_chars);
assert_eq!(result, "test-text");
let normal_whitespace = "test\n\t text";
let result = heading_to_fragment(normal_whitespace);
assert_eq!(result, "test---text"); }
#[test]
fn test_security_comprehensive_emoji_detection() {
let flag_test = "Hello 🇺🇸 World 🇬🇧 Test";
let result = heading_to_fragment(flag_test);
assert_eq!(result, "hello--world--test");
let keycap_test = "Step 1️⃣ and 2️⃣ complete";
let result = heading_to_fragment(keycap_test);
assert_eq!(result, "step-1️⃣-and-2️⃣-complete");
let complex_emoji = "Test 👨👩👧👦 family";
let result = heading_to_fragment(complex_emoji);
assert_eq!(result, "test--family");
let mixed_symbols = "Math ∑ ∆ 🧮 symbols";
let result = heading_to_fragment(mixed_symbols);
assert_eq!(result, "math----symbols"); }
#[test]
fn test_security_redos_resistance() {
let nested_emphasis = "*".repeat(50) + "text" + &"*".repeat(50);
let result = heading_to_fragment(&nested_emphasis);
assert!(result.len() < 200);
let nested_code = "`".repeat(100) + "code" + &"`".repeat(100);
let result = heading_to_fragment(&nested_code);
assert!(result.len() < 300);
let nested_links = "[".repeat(50) + "text" + &"]".repeat(50);
let result = heading_to_fragment(&nested_links);
assert!(result.len() < 200); }
#[test]
fn test_security_dangerous_unicode_blocks() {
let pua_test = "test\u{E000}\u{F8FF}text";
let result = heading_to_fragment(pua_test);
assert_eq!(result, "testtext");
let variation_test = "test\u{FE00}\u{FE0F}text";
let result = heading_to_fragment(variation_test);
assert_eq!(result, "testtext"); }
#[test]
fn test_security_normal_behavior_preserved() {
let unicode_letters = "Café René naïve über";
let result = heading_to_fragment(unicode_letters);
assert_eq!(result, "café-rené-naïve-über");
let ascii_test = "Hello World 123";
let result = heading_to_fragment(ascii_test);
assert_eq!(result, "hello-world-123");
let github_specific = "cbrown --> sbrown: --unsafe-paths";
let result = heading_to_fragment(github_specific);
assert_eq!(result, "cbrown----sbrown---unsafe-paths");
}
#[test]
fn test_github_arrow_patterns_issue_82() {
assert_eq!(heading_to_fragment("WAL->L0 Compaction"), "wal-l0-compaction");
assert_eq!(heading_to_fragment("foo->bar->baz"), "foo-bar-baz");
assert_eq!(heading_to_fragment("a->b"), "a-b");
assert_eq!(heading_to_fragment("a ->b"), "a--b");
assert_eq!(heading_to_fragment("a-> b"), "a--b");
assert_eq!(heading_to_fragment("a -> b"), "a---b");
assert_eq!(heading_to_fragment("a-->b"), "a--b");
assert_eq!(heading_to_fragment("a -->b"), "a---b");
assert_eq!(heading_to_fragment("a--> b"), "a---b");
assert_eq!(heading_to_fragment("a --> b"), "a----b");
assert_eq!(heading_to_fragment("cbrown -> sbrown"), "cbrown---sbrown");
assert_eq!(
heading_to_fragment("cbrown --> sbrown: --unsafe-paths"),
"cbrown----sbrown---unsafe-paths"
);
}
#[test]
fn test_security_performance_edge_cases() {
let repetitive = "ab".repeat(1000);
let start = std::time::Instant::now();
let result = heading_to_fragment(&repetitive);
let duration = start.elapsed();
assert!(duration.as_millis() < 100);
assert!(!result.is_empty());
let mixed = ("a".to_string() + "ñ").repeat(500);
let start = std::time::Instant::now();
let result = heading_to_fragment(&mixed);
let duration = start.elapsed();
assert!(duration.as_millis() < 100);
assert!(!result.is_empty());
}
#[test]
fn test_code_span_preserves_underscores_in_slug() {
assert_eq!(heading_to_fragment("`__hello__`"), "__hello__");
assert_eq!(heading_to_fragment("`__init__`"), "__init__");
assert_eq!(heading_to_fragment("`_single_`"), "_single_");
}
#[test]
fn test_emphasis_underscores_removed_from_slug() {
assert_eq!(heading_to_fragment("__hello__"), "hello");
assert_eq!(heading_to_fragment("_hello_"), "hello");
}
#[test]
fn test_mixed_code_and_emphasis_in_heading() {
assert_eq!(
heading_to_fragment("`__init__` method for __MyClass__"),
"__init__-method-for-myclass"
);
}
#[test]
fn test_multiple_code_spans_in_heading() {
assert_eq!(heading_to_fragment("`__a__` and `__b__`"), "__a__-and-__b__");
assert_eq!(heading_to_fragment("`__init__` and `__del__`"), "__init__-and-__del__");
assert_eq!(heading_to_fragment("`__a__` `__b__` `__c__`"), "__a__-__b__-__c__");
}
#[test]
fn test_adjacent_code_spans_in_heading() {
assert_eq!(heading_to_fragment("`__a__``__b__`"), "__a____b__");
assert_eq!(heading_to_fragment("`_x_``_y_`"), "_x__y_");
}
#[test]
fn test_double_backtick_code_span_preserves_content() {
assert_eq!(heading_to_fragment("``__init__``"), "__init__");
assert_eq!(heading_to_fragment("``__hello__``"), "__hello__");
assert_eq!(heading_to_fragment("``_single_``"), "_single_");
}
#[test]
fn test_double_backtick_code_span_with_surrounding_text() {
assert_eq!(
heading_to_fragment("``__init__`` method for __MyClass__"),
"__init__-method-for-myclass"
);
}
#[test]
fn test_double_backtick_code_span_containing_single_backtick() {
assert_eq!(heading_to_fragment("``code`here``"), "codehere");
}
#[test]
fn test_code_span_with_parentheses() {
assert_eq!(heading_to_fragment("`__init__(self, name)`"), "__init__self-name");
assert_eq!(heading_to_fragment("`foo(bar)`"), "foobar");
assert_eq!(heading_to_fragment("`func(a, b, c)`"), "funca-b-c");
}
}