panache_parser/parser/inlines/
wikilinks.rs1use super::sink::InlineSink;
20use crate::ParserOptions;
21use crate::syntax::SyntaxKind;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub(crate) struct WikiLinkSpan {
26 pub start: usize,
28 pub end: usize,
30 pub pipe: Option<usize>,
33 pub is_image: bool,
35}
36
37impl WikiLinkSpan {
38 pub(crate) fn body_start(&self) -> usize {
40 if self.is_image {
41 self.start + 3 } else {
43 self.start + 2 }
45 }
46
47 pub(crate) fn body_end(&self) -> usize {
49 self.end - 2
50 }
51}
52
53pub(crate) fn any_enabled(opts: &ParserOptions) -> bool {
55 opts.extensions.wikilinks_title_after_pipe || opts.extensions.wikilinks_title_before_pipe
56}
57
58pub(crate) fn try_parse_wikilink(
65 text: &str,
66 pos: usize,
67 opts: &ParserOptions,
68) -> Option<WikiLinkSpan> {
69 if !any_enabled(opts) {
70 return None;
71 }
72
73 let bytes = text.as_bytes();
74 let n = bytes.len();
75 if pos >= n {
76 return None;
77 }
78
79 let (is_image, open_end) = if bytes[pos] == b'!' {
80 if pos + 2 >= n || bytes[pos + 1] != b'[' || bytes[pos + 2] != b'[' {
81 return None;
82 }
83 (true, pos + 3)
84 } else if bytes[pos] == b'[' {
85 if pos + 1 >= n || bytes[pos + 1] != b'[' {
86 return None;
87 }
88 (false, pos + 2)
89 } else {
90 return None;
91 };
92
93 let body_start = open_end;
94 let mut i = body_start;
95 let mut pipe: Option<usize> = None;
96 while i + 1 < n {
97 let b = bytes[i];
98 if b == b'\n' || b == b'\r' {
99 return None;
100 }
101 if b == b']' && bytes[i + 1] == b']' {
102 if i == body_start {
103 return None;
105 }
106 return Some(WikiLinkSpan {
107 start: pos,
108 end: i + 2,
109 pipe,
110 is_image,
111 });
112 }
113 if b == b'|' && pipe.is_none() {
114 pipe = Some(i);
115 }
116 i += 1;
117 }
118 None
119}
120
121pub(crate) fn emit_wikilink<S: InlineSink>(
128 builder: &mut S,
129 text: &str,
130 span: WikiLinkSpan,
131 opts: &ParserOptions,
132) {
133 let outer_kind = if span.is_image {
134 SyntaxKind::IMAGE_WIKI_LINK
135 } else {
136 SyntaxKind::WIKI_LINK
137 };
138 let open_str = if span.is_image { "![[" } else { "[[" };
139
140 builder.start_node(outer_kind.into());
141 builder.token(SyntaxKind::WIKI_LINK_OPEN.into(), open_str);
142
143 let body_start = span.body_start();
144 let body_end = span.body_end();
145
146 let (url_range, title_range) = match span.pipe {
147 Some(p) => {
148 let url;
149 let title;
150 if opts.extensions.wikilinks_title_after_pipe {
151 url = body_start..p;
153 title = (p + 1)..body_end;
154 } else {
155 title = body_start..p;
157 url = (p + 1)..body_end;
158 }
159 (url, Some((p, title)))
160 }
161 None => (body_start..body_end, None),
162 };
163
164 let url_first = match span.pipe {
168 Some(_) => opts.extensions.wikilinks_title_after_pipe,
169 None => true,
170 };
171
172 let emit_url = |b: &mut S| {
173 b.start_node(SyntaxKind::WIKI_LINK_URL.into());
174 b.token(SyntaxKind::TEXT.into(), &text[url_range.clone()]);
175 b.finish_node();
176 };
177 let emit_pipe_and_title = |b: &mut S| {
178 if let Some((_p, ref tr)) = title_range {
179 b.token(SyntaxKind::WIKI_LINK_PIPE.into(), "|");
180 b.start_node(SyntaxKind::WIKI_LINK_TITLE.into());
181 b.token(SyntaxKind::TEXT.into(), &text[tr.clone()]);
182 b.finish_node();
183 }
184 };
185
186 if url_first {
187 emit_url(builder);
188 emit_pipe_and_title(builder);
189 } else {
190 if let Some((_p, ref tr)) = title_range {
192 builder.start_node(SyntaxKind::WIKI_LINK_TITLE.into());
193 builder.token(SyntaxKind::TEXT.into(), &text[tr.clone()]);
194 builder.finish_node();
195 builder.token(SyntaxKind::WIKI_LINK_PIPE.into(), "|");
196 }
197 emit_url(builder);
198 }
199
200 builder.token(SyntaxKind::WIKI_LINK_CLOSE.into(), "]]");
201 builder.finish_node();
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::options::{Extensions, ParserOptions};
208
209 fn opts_with(after: bool, before: bool) -> ParserOptions {
210 let extensions = Extensions {
211 wikilinks_title_after_pipe: after,
212 wikilinks_title_before_pipe: before,
213 ..Extensions::default()
214 };
215 ParserOptions {
216 extensions,
217 ..ParserOptions::default()
218 }
219 }
220
221 fn opts_after() -> ParserOptions {
222 opts_with(true, false)
223 }
224
225 fn opts_before() -> ParserOptions {
226 opts_with(false, true)
227 }
228
229 fn opts_both() -> ParserOptions {
230 opts_with(true, true)
231 }
232
233 fn opts_off() -> ParserOptions {
234 opts_with(false, false)
235 }
236
237 #[test]
238 fn parses_simple_wikilink() {
239 let text = "[[https://example.org]]";
240 let span = try_parse_wikilink(text, 0, &opts_after()).unwrap();
241 assert_eq!(span.start, 0);
242 assert_eq!(span.end, text.len());
243 assert_eq!(span.pipe, None);
244 assert!(!span.is_image);
245 }
246
247 #[test]
248 fn parses_with_title() {
249 let text = "[[url|hello]]";
250 let span = try_parse_wikilink(text, 0, &opts_after()).unwrap();
251 assert_eq!(span.pipe, Some(5));
252 assert_eq!(span.end, text.len());
253 }
254
255 #[test]
256 fn parses_image_wikilink() {
257 let text = "![[url]]";
258 let span = try_parse_wikilink(text, 0, &opts_after()).unwrap();
259 assert!(span.is_image);
260 assert_eq!(span.end, text.len());
261 }
262
263 #[test]
264 fn rejects_empty_body() {
265 assert!(try_parse_wikilink("[[]]", 0, &opts_after()).is_none());
267 assert!(try_parse_wikilink("![[]]", 0, &opts_after()).is_none());
268 }
269
270 #[test]
271 fn rejects_unclosed() {
272 assert!(try_parse_wikilink("[[unclosed", 0, &opts_after()).is_none());
273 assert!(try_parse_wikilink("[[no closing here", 0, &opts_after()).is_none());
274 }
275
276 #[test]
277 fn rejects_newline_inside() {
278 assert!(try_parse_wikilink("[[a\nb]]", 0, &opts_after()).is_none());
280 assert!(try_parse_wikilink("[[a\r\nb]]", 0, &opts_after()).is_none());
281 }
282
283 #[test]
284 fn rejects_when_disabled() {
285 assert!(try_parse_wikilink("[[a|b]]", 0, &opts_off()).is_none());
287 assert!(try_parse_wikilink("[[just url]]", 0, &opts_off()).is_none());
288 }
289
290 #[test]
291 fn non_greedy_close() {
292 let text = "[[a]]b]]";
294 let span = try_parse_wikilink(text, 0, &opts_after()).unwrap();
295 assert_eq!(span.end, 5); }
297
298 #[test]
299 fn first_pipe_is_separator() {
300 let text = "[[a|b|c]]";
302 let span = try_parse_wikilink(text, 0, &opts_after()).unwrap();
303 assert_eq!(span.pipe, Some(3));
304 }
305
306 #[test]
307 fn parses_with_before_pipe_extension() {
308 let text = "[[title|url]]";
309 let span = try_parse_wikilink(text, 0, &opts_before()).unwrap();
310 assert_eq!(span.pipe, Some(7));
311 }
312
313 #[test]
314 fn both_extensions_enabled_still_matches() {
315 let text = "[[a|b]]";
317 let span = try_parse_wikilink(text, 0, &opts_both()).unwrap();
318 assert_eq!(span.pipe, Some(3));
319 }
320
321 #[test]
322 fn parse_at_offset() {
323 let text = "prefix [[url|title]] suffix";
325 let span = try_parse_wikilink(text, 7, &opts_after()).unwrap();
326 assert_eq!(span.start, 7);
327 assert_eq!(span.end, 20);
328 }
329
330 #[test]
331 fn body_indexing_is_correct() {
332 let text = "[[abc|def]]";
333 let span = try_parse_wikilink(text, 0, &opts_after()).unwrap();
334 assert_eq!(span.body_start(), 2);
335 assert_eq!(span.body_end(), 9);
336 assert_eq!(&text[span.body_start()..span.body_end()], "abc|def");
337 }
338
339 #[test]
340 fn image_body_indexing_is_correct() {
341 let text = "![[abc]]";
342 let span = try_parse_wikilink(text, 0, &opts_after()).unwrap();
343 assert_eq!(span.body_start(), 3);
344 assert_eq!(span.body_end(), 6);
345 assert_eq!(&text[span.body_start()..span.body_end()], "abc");
346 }
347}