yggdrasil/api/markdown/
footnote.rs1use std::fmt::Write as _;
4
5pub(super) fn footnote_id(label: &str) -> String {
15 let mut out = String::with_capacity(label.len());
16 let mut prev_dash = true; for c in label.chars() {
19 if c.is_ascii_alphanumeric() || c == '_' {
20 out.push(c);
21 prev_dash = false;
22 } else if !c.is_ascii() {
23 out.push(c);
25 prev_dash = false;
26 } else if !prev_dash {
27 out.push('-');
29 prev_dash = true;
30 }
31 }
32
33 while out.ends_with('-') {
35 out.pop();
36 }
37
38 if out.is_empty() {
39 out.push_str("fn");
41 }
42
43 out
44}
45
46pub(super) fn reference_html(id: &str, n: usize, num: usize) -> String {
49 let mut rendered = String::new();
51 let _ = write!(
52 rendered,
53 r##"<sup class="fn-ref" id="fnref:{id}-{n}"><a href="#fn:{id}" class="fn-ref-link" role="doc-noteref" aria-label="脚注 {num}">{num}</a></sup>"##
54 );
55 rendered
56}
57
58pub(super) fn push_definition_open(html: &mut String, id: &str, num: usize) {
60 let _ = write!(
63 html,
64 r##"<aside class="footnote-definition" id="fn:{id}" role="doc-footnote" aria-labelledby="fn:{id}-label"><sup class="footnote-definition-label" id="fn:{id}-label">{num}</sup> "##
65 );
66}
67
68pub(super) fn push_backlinks(html: &mut String, id: &str, ref_count: usize) {
70 if ref_count > 0 {
74 let _ = write!(
76 html,
77 r##"<a href="#fnref:{id}-1" class="fn-backref" role="doc-backlink" aria-label="返回正文">↩</a>"##
78 );
79 for n in 2..=ref_count {
81 let _ = write!(
82 html,
83 r##" <a href="#fnref:{id}-{n}" class="fn-backref" role="doc-backlink" aria-label="返回正文 {n}">↩<sup class="fn-backref-num">{n}</sup></a>"##
84 );
85 }
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
96 fn footnote_id_preserves_alphanumeric() {
97 assert_eq!(footnote_id("abc123"), "abc123");
98 assert_eq!(footnote_id("a_b-c"), "a_b-c");
99 }
100
101 #[test]
102 fn footnote_id_preserves_non_ascii() {
103 assert_eq!(footnote_id("参考文献1"), "参考文献1");
105 }
106
107 #[test]
108 fn footnote_id_replaces_spaces_and_punctuation() {
109 assert_eq!(footnote_id("my note"), "my-note");
111 assert_eq!(footnote_id("a b!c?d"), "a-b-c-d");
112 }
113
114 #[test]
115 fn footnote_id_deterministic() {
116 for label in ["a", "my note", "参考文献", "a!b@c#"] {
118 assert_eq!(
119 footnote_id(label),
120 footnote_id(label),
121 "label {:?} 不确定",
122 label
123 );
124 }
125 }
126
127 #[test]
128 fn footnote_id_no_attribute_breaking_chars() {
129 for label in ["a\"b", "x'y", "a<b>", "c&d", "e f", "a!@#$%^&*()b"] {
131 let id = footnote_id(label);
132 assert!(
133 !id.contains('"'),
134 "id {:?} 含双引号 (label {:?})",
135 id,
136 label
137 );
138 assert!(
139 !id.contains('\''),
140 "id {:?} 含单引号 (label {:?})",
141 id,
142 label
143 );
144 assert!(!id.contains('<'), "id {:?} 含 < (label {:?})", id, label);
145 assert!(!id.contains('>'), "id {:?} 含 > (label {:?})", id, label);
146 assert!(!id.contains('&'), "id {:?} 含 & (label {:?})", id, label);
147 assert!(!id.contains(' '), "id {:?} 含空格 (label {:?})", id, label);
148 }
149 }
150
151 #[test]
152 fn footnote_id_empty_fallback() {
153 assert_eq!(footnote_id("!@#$"), "fn");
155 assert_eq!(footnote_id("!!!"), "fn");
156 }
157}