1use std::collections::HashMap;
13
14#[derive(Debug, Default, Clone)]
21pub struct LinkTable {
22 by_name: HashMap<String, (String, String)>,
23}
24
25impl LinkTable {
26 pub fn new() -> Self {
27 Self::default()
28 }
29
30 pub fn insert(&mut self, name: &str, kind: &str, slug: &str) {
33 self.by_name
34 .insert(name.to_string(), (kind.to_string(), slug.to_string()));
35 }
36
37 fn lookup(&self, query: &str) -> Option<&(String, String)> {
38 let trimmed = match query.split_once(' ') {
40 Some((_kind, name)) if is_known_kind(_kind) => name.trim(),
41 _ => query.trim(),
42 };
43 let trimmed = match trimmed.split_once("::") {
46 Some((parent, _method)) => parent,
47 None => trimmed,
48 };
49 self.by_name.get(trimmed)
50 }
51}
52
53fn is_known_kind(s: &str) -> bool {
54 matches!(
55 s,
56 "fn" | "struct" | "enum" | "interface" | "derive" | "const" | "link_extern"
57 )
58}
59
60pub fn rewrite(body: &str, table: &LinkTable, extension: &str) -> String {
65 let bytes = body.as_bytes();
66 let mut out = String::with_capacity(body.len());
67 let mut i = 0;
68 while i < bytes.len() {
74 if bytes[i] == b'[' {
75 if let Some(end_rel) = find_balanced_bracket(&bytes[i + 1..]) {
76 let end = i + 1 + end_rel;
77 let inner = &body[i + 1..end];
78 let after = bytes.get(end + 1).copied();
82 if after == Some(b'(') || after == Some(b'[') {
83 out.push_str(&body[i..=end]);
84 i = end + 1;
85 continue;
86 }
87 if let Some((_, slug)) = table.lookup(inner) {
88 out.push('[');
89 out.push_str(inner);
90 out.push_str("](");
91 out.push_str(slug);
92 out.push_str(extension);
93 out.push(')');
94 i = end + 1;
95 continue;
96 }
97 }
98 out.push('[');
99 i += 1;
100 continue;
101 }
102 let ch = body[i..].chars().next().expect("valid utf-8");
105 out.push(ch);
106 i += ch.len_utf8();
107 }
108 out
109}
110
111fn find_balanced_bracket(rest: &[u8]) -> Option<usize> {
115 let mut depth: i32 = 0;
116 for (i, b) in rest.iter().enumerate() {
117 match b {
118 b'\n' => return None,
119 b'[' => depth += 1,
120 b']' => {
121 if depth == 0 {
122 return Some(i);
123 }
124 depth -= 1;
125 }
126 _ => {}
127 }
128 }
129 None
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 fn table_with(name: &str, kind: &str, slug: &str) -> LinkTable {
137 let mut t = LinkTable::new();
138 t.insert(name, kind, slug);
139 t
140 }
141
142 #[test]
143 fn rewrites_bare_name() {
144 let t = table_with("foo", "fn", "fn.foo");
145 let out = rewrite("see [foo] for details", &t, ".html");
146 assert_eq!(out, "see [foo](fn.foo.html) for details");
147 }
148
149 #[test]
150 fn rewrites_kind_prefix() {
151 let t = table_with("foo", "fn", "fn.foo");
152 let out = rewrite("call [fn foo]", &t, ".html");
153 assert_eq!(out, "call [fn foo](fn.foo.html)");
154 }
155
156 #[test]
157 fn rewrites_method_to_parent() {
158 let t = table_with("Vec", "struct", "struct.Vec");
159 let out = rewrite("see [Vec::push]", &t, ".html");
160 assert_eq!(out, "see [Vec::push](struct.Vec.html)");
161 }
162
163 #[test]
164 fn leaves_unknown_alone() {
165 let t = LinkTable::new();
166 let out = rewrite("see [bar] for details", &t, ".html");
167 assert_eq!(out, "see [bar] for details");
168 }
169
170 #[test]
171 fn leaves_explicit_links_alone() {
172 let t = table_with("foo", "fn", "fn.foo");
175 let body = "see [foo](other.html) and [foo][ref] and [foo] last";
176 let out = rewrite(body, &t, ".html");
177 assert_eq!(
178 out,
179 "see [foo](other.html) and [foo][ref] and [foo](fn.foo.html) last"
180 );
181 }
182
183 #[test]
184 fn extension_swaps_md_and_html() {
185 let t = table_with("foo", "fn", "fn.foo");
186 assert_eq!(rewrite("[foo]", &t, ".md"), "[foo](fn.foo.md)");
187 assert_eq!(rewrite("[foo]", &t, ".html"), "[foo](fn.foo.html)");
188 }
189
190 #[test]
191 fn no_rewrite_across_newlines() {
192 let t = table_with("foo", "fn", "fn.foo");
193 let body = "weird [foo\n] nope";
196 let out = rewrite(body, &t, ".html");
197 assert_eq!(out, "weird [foo\n] nope");
198 }
199}