1use std::borrow::Cow;
2use std::fmt::Display;
3
4use phf::phf_set;
5
6use crate::constants::{
7 BACKSLASH, COLON, HYPHEN, LANGLE, LBRACK, LPAREN, POUND, RANGLE, RBRACK, RPAREN, SLASH,
8};
9use crate::node_pool::NodeID;
10use crate::parse::parse_object;
11use crate::types::{Cursor, MarkupKind, MatchError, ParseOpts, Parseable, Parser, Result};
12use crate::utils::Match;
13
14const ORG_LINK_PARAMETERS: [&str; 9] = [
15 "shell", "news", "mailto", "https", "http", "ftp", "help", "file", "elisp",
16];
17
18static IMAGE_TYPES: phf::Set<&str> = phf_set! {
20 "jpeg",
21 "jpg",
22 "png",
23 "gif",
24 "svg",
25 "webp",
26};
27
28#[derive(Debug, Clone)]
29pub struct RegularLink<'a> {
30 pub path: Match<PathReg<'a>>,
31 pub description: Option<Vec<NodeID>>,
37 pub caption: Option<NodeID>,
40}
41
42impl Display for PathReg<'_> {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 PathReg::PlainLink(link) => {
46 f.write_fmt(format_args!("{}:{}", link.protocol, link.path))
47 }
48 PathReg::Id(inner) => f.write_fmt(format_args!("id:{inner}")),
49 PathReg::CustomId(inner) => f.write_fmt(format_args!("#{inner}")),
50 PathReg::Coderef(inner) => f.write_fmt(format_args!("({inner})")),
51 PathReg::Unspecified(inner) => f.write_fmt(format_args!("{inner}")),
52 PathReg::File(inner) => f.write_fmt(format_args!("file:{inner}")),
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct PlainLink<'a> {
59 pub protocol: Cow<'a, str>,
60 pub path: Cow<'a, str>,
61}
62
63impl From<&PlainLink<'_>> for String {
64 fn from(value: &PlainLink) -> Self {
65 format!("{}:{}", value.protocol, value.path)
66 }
67}
68
69#[derive(Debug, Clone)]
71pub enum PathReg<'a> {
72 PlainLink(PlainLink<'a>),
73 Id(&'a str),
74 CustomId(&'a str),
76 Coderef(&'a str),
78 File(Cow<'a, str>),
79 Unspecified(Cow<'a, str>),
80 }
86
87impl<'a> PathReg<'a> {
88 fn new(cursor: Cursor<'a>) -> Self {
89 match cursor.curr() {
90 b'i' => {
91 if let Ok(id) = PathReg::parse_id(cursor) {
92 return PathReg::Id(id);
93 } else if let Ok(link) = parse_plain_link(cursor) {
94 return PathReg::PlainLink(link.obj);
95 }
96 }
97 b'f' => {
98 if let Ok(file_path) = PathReg::parse_file(cursor) {
99 return PathReg::File(file_path.into());
100 } else if let Ok(link) = parse_plain_link(cursor) {
101 return PathReg::PlainLink(link.obj);
102 }
103 }
104 POUND => {
105 return PathReg::CustomId(cursor.clamp(cursor.index + 1, cursor.len()));
107 }
108 LPAREN => {
109 if cursor[cursor.len() - 1] == RPAREN {
111 return PathReg::Coderef(cursor.clamp(cursor.index + 1, cursor.len()));
112 }
113 }
114 chr => {
115 if let Ok(link) = parse_plain_link(cursor) {
116 return PathReg::PlainLink(link.obj);
117 }
118 }
119 }
120 return PathReg::Unspecified(cursor.clamp_forwards(cursor.len()).into());
127 }
128
129 fn parse_id(mut cursor: Cursor<'a>) -> Result<&'a str> {
130 cursor.word("id:")?;
131 let begin_id = cursor.index;
132
133 while let Ok(num) = cursor.try_curr() {
134 if !num.is_ascii_hexdigit() || num == HYPHEN {
135 return Err(MatchError::InvalidLogic);
136 }
137 cursor.next();
138 }
139
140 Ok(cursor.clamp_backwards(begin_id))
141 }
142
143 fn parse_file(mut cursor: Cursor<'a>) -> Result<&'a str> {
144 cursor.word("file:")?;
145 let begin_id = cursor.index;
146
147 while let Ok(num) = cursor.try_curr() {
148 cursor.next();
149 }
150
151 Ok(cursor.clamp_backwards(begin_id))
152 }
153}
154
155impl<'a> Parseable<'a> for RegularLink<'a> {
156 fn parse(
157 parser: &mut Parser<'a>,
158 mut cursor: Cursor<'a>,
159 parent: Option<NodeID>,
160 mut parse_opts: ParseOpts,
161 ) -> Result<NodeID> {
162 let start = cursor.index;
163 cursor.word("[[")?;
164
165 loop {
167 match cursor.try_curr()? {
168 BACKSLASH => {
169 if let BACKSLASH | LBRACK | RBRACK = cursor.peek(1)? {
171 cursor.advance(2);
172 } else {
173 return Err(MatchError::InvalidLogic);
174 }
175 }
176 RBRACK => {
177 if cursor.index == start + 2 {
179 return Err(MatchError::InvalidLogic);
180 }
181
182 if LBRACK == cursor.peek(1)? {
183 let path_reg_end = cursor.index;
184
185 cursor.advance(2);
187 parse_opts.from_object = false;
188 parse_opts.markup.insert(MarkupKind::Link);
189
190 let mut content_vec: Vec<NodeID> = Vec::new();
191 loop {
192 match parse_object(parser, cursor, parent, parse_opts) {
193 Ok(id) => {
194 cursor.index = parser.pool[id].end;
195 content_vec.push(id);
196 }
197 Err(MatchError::MarkupEnd(kind)) => {
198 if !kind.contains(MarkupKind::Link) {
199 return Err(MatchError::InvalidLogic);
201 }
202
203 let reg_curs = cursor.clamp_off(start + 2, path_reg_end);
204 let pathreg = Match {
205 start: start + 2,
206 end: path_reg_end,
207 obj: PathReg::new(reg_curs),
208 };
209
210 let new_id = parser.pool.reserve_id();
213 for id in &mut content_vec {
214 parser.pool[*id].parent = Some(new_id);
215 }
216
217 return Ok(parser.alloc_with_id(
218 Self {
219 path: pathreg,
220 description: Some(content_vec),
221 caption: None,
222 },
223 start,
224 cursor.index + 2, parent,
226 new_id,
227 ));
228 }
229 ret @ Err(_) => return ret,
230 }
231 }
232 } else if RBRACK == cursor.peek(1)? {
233 let reg_curs = cursor.clamp_off(start + 2, cursor.index);
236 let pathreg = Match {
237 start: start + 2,
238 end: cursor.index,
239 obj: PathReg::new(reg_curs),
240 };
241
242 return Ok(parser.alloc(
243 Self {
244 path: pathreg,
245 description: None,
246 caption: None,
247 },
248 start,
249 cursor.index + 2,
250 parent,
251 ));
252 } else {
253 return Err(MatchError::InvalidLogic);
254 }
255 }
256 _ => {}
257 }
258 cursor.next();
259 }
260 }
261}
262
263impl RegularLink<'_> {
264 pub fn is_image(&self, parser: &Parser) -> bool {
265 let link_source: &str = match &self.path.obj {
266 PathReg::Unspecified(inner) => inner,
267 PathReg::File(inner) => inner,
268 PathReg::PlainLink(inner) => &inner.path,
269 _ => {
270 ""
273 }
274 };
275 link_source
276 .rsplit_once('.') .map(|(_, ext)| ext)
278 .is_some_and(|ext| IMAGE_TYPES.contains(ext))
279 }
280}
281
282pub(crate) fn parse_plain_link(mut cursor: Cursor<'_>) -> Result<Match<PlainLink<'_>>> {
299 if let Ok(pre_byte) = cursor.peek_rev(1)
300 && pre_byte.is_ascii_alphanumeric()
301 {
302 return Err(MatchError::InvalidLogic);
303 }
304
305 let start = cursor.index;
306
307 for (i, &protocol) in ORG_LINK_PARAMETERS.iter().enumerate() {
308 if cursor.word(protocol).is_ok() {
311 if cursor.try_curr()? == COLON {
312 cursor.next();
313 let path_start = cursor.index;
314 while let Ok(byte) = cursor.try_curr() {
317 match byte {
318 RANGLE | LPAREN | RPAREN | LANGLE | b'\t' | b'\n' | b'\x0C' | b'\r'
319 | b' ' => {
320 break;
321 }
322 _ => {
324 cursor.next();
325 }
326 }
327 }
328
329 let last_link_byte = cursor[cursor.index - 1];
330 while !cursor.peek_rev(1)?.is_ascii_alphanumeric() && cursor.peek_rev(1)? != SLASH {
343 cursor.prev();
344 if cursor.index <= path_start {
345 return Err(MatchError::InvalidLogic);
346 }
347 }
348
349 if if let Ok(future_byte) = cursor.try_curr() {
350 !future_byte.is_ascii_alphanumeric()
351 } else {
352 true
353 } {
354 return Ok(Match {
355 start,
356 end: cursor.index,
357 obj: PlainLink {
358 protocol: protocol.into(),
359 path: cursor.clamp_backwards(path_start).into(),
360 },
361 });
362 } else {
363 return Err(MatchError::EofError);
364 }
365 } else {
366 cursor.index -= protocol.len();
367 }
368 }
369 }
370
371 Err(MatchError::InvalidLogic)
372}
373
374pub(crate) fn parse_angle_link<'a>(
375 parser: &mut Parser<'a>,
376 mut cursor: Cursor<'a>,
377 parent: Option<NodeID>,
378 parse_opts: ParseOpts,
379) -> Result<NodeID> {
380 let start = cursor.index;
381
382 cursor.next();
383
384 for (i, &protocol) in ORG_LINK_PARAMETERS.iter().enumerate() {
385 if cursor.word(protocol).is_ok() {
386 if cursor.try_curr()? == COLON {
387 cursor.next();
388 let path_start = cursor.index;
389 while let Ok(byte) = cursor.try_curr() {
390 match byte {
391 RBRACK | LANGLE | b'\n' => return Err(MatchError::InvalidLogic),
392 RANGLE => break,
393 _ => {
394 cursor.next();
395 }
396 }
397 }
398
399 return Ok(parser.alloc(
402 PlainLink {
403 protocol: protocol.into(),
404 path: cursor.clamp_backwards(path_start).into(),
405 },
406 start,
407 cursor.index + 1, parent,
409 ));
410 } else {
411 cursor.index -= protocol.len();
412 }
413 }
414 }
415
416 Err(MatchError::InvalidLogic)
417}
418
419#[cfg(test)]
420mod tests {
421 use pretty_assertions::assert_eq;
422
423 use crate::element::Affiliated;
424 use crate::expr_in_pool;
425 use crate::object::{PlainLink, RegularLink};
426 use crate::parse_org;
427 use crate::types::Expr;
428
429 #[test]
430 fn basic_plain_link() {
431 let input = "https://swag.org";
432 let parsed = parse_org(input);
433 let l = expr_in_pool!(parsed, PlainLink).unwrap();
434 assert_eq!(
435 l,
436 &PlainLink {
437 protocol: "https".into(),
438 path: "//swag.org".into()
439 }
440 )
441 }
442
443 #[test]
444 fn plain_link_subprotocol() {
445 let input = "http://swag.org";
447 let parsed = parse_org(input);
448 let l = expr_in_pool!(parsed, PlainLink).unwrap();
449 assert_eq!(
450 l,
451 &PlainLink {
452 protocol: "http".into(),
453 path: "//swag.org".into()
454 }
455 )
456 }
457
458 #[test]
459 fn plain_link_after() {
460 let input = "http://swag.com meow";
461 let parsed = parse_org(input);
462 let l = expr_in_pool!(parsed, PlainLink).unwrap();
463 assert_eq!(
464 l,
465 &PlainLink {
466 protocol: "http".into(),
467 path: "//swag.com".into()
468 }
469 )
470 }
471
472 #[test]
473 fn plain_link_ws_end() {
474 let input = " mailto:swag@cool.com ";
476 let parsed = parse_org(input);
477 let l = expr_in_pool!(parsed, PlainLink).unwrap();
478
479 assert_eq!(
480 l,
481 &PlainLink {
482 protocol: "mailto".into(),
483 path: "swag@cool.com".into()
484 }
485 )
486 }
487
488 #[test]
489 fn plain_link_word_constituent() {
490 let input = " https://one_two_three_https______..............~~~! ";
492 let parsed = parse_org(input);
493 let l = expr_in_pool!(parsed, PlainLink).unwrap();
494
495 assert_eq!(
496 l,
497 &PlainLink {
498 protocol: "https".into(),
499 path: "//one_two_three_https".into()
500 }
501 )
502 }
503
504 #[test]
505 fn plain_link_word_constituent_slash() {
506 let input = " https://one_two_three_https______/..............~~~! ";
508 let parsed = parse_org(input);
509 let l = expr_in_pool!(parsed, PlainLink).unwrap();
510
511 assert_eq!(
512 l,
513 &PlainLink {
514 protocol: "https".into(),
515 path: "//one_two_three_https______/".into()
516 }
517 )
518 }
519
520 #[test]
521 fn basic_angle_link() {
522 let input = " <https://one two !!@#!OIO DJDFK Jk> ";
524 let parsed = parse_org(input);
525 let l = expr_in_pool!(parsed, PlainLink).unwrap();
526
527 assert_eq!(
528 l,
529 &PlainLink {
530 protocol: "https".into(),
531 path: "//one two !!@#!OIO DJDFK Jk".into()
532 }
533 )
534 }
535
536 #[test]
537 fn basic_regular_link() {
538 let input = "[[hps://.org]]";
539 let pool = parse_org(input);
540 pool.print_tree();
541 }
542
543 #[test]
544 fn regular_link_malformed() {
545 let input = "
546word
547[#A]
548";
549 let pool = parse_org(input);
550 pool.print_tree();
551 }
552
553 #[test]
554 fn regular_link_description() {
555 let input = " [[https://meo][cool site]]";
556 let pool = parse_org(input);
557 pool.print_tree();
558 }
559
560 #[test]
561 fn regular_link_unclosed_recursive_markup() {
562 let input = " [[https://meo][cool *site* ~one two~ three *four ]]";
563 let pool = parse_org(input);
564 pool.print_tree();
565 }
566
567 #[test]
568 fn regular_link_unclosed_plain_markup() {
569 let input = " [[https://meo][cool *site* ~one two~ three *four ~five six ]]";
570 let pool = parse_org(input);
571 pool.print_tree();
572 }
573
574 #[test]
575 fn file_link() {
576 let input = r"
577I'll be skipping over the instrumentals unless there's reason to.
578
579[[file:bmc.jpg]]
580** songs
581";
582
583 let pool = parse_org(input);
584 pool.print_tree();
585 }
586
587 #[test]
588 fn caption_link() {
589 let input = r"
590#+caption: sing song
591[[heathers.jpg]]
592
593";
594
595 let parser = parse_org(input);
596 let image_link = expr_in_pool!(parser, RegularLink).unwrap();
597 if let Some(cap_id) = image_link.caption
598 && let Expr::Affiliated(Affiliated::Caption(aff)) = &parser.pool[cap_id].obj
599 && let Expr::Paragraph(par) = &parser.pool[*aff].obj
600 && let Expr::Plain(text) = parser.pool[par.0[0]].obj
601 {
602 assert_eq!(text, " sing song");
604 } else {
605 panic!()
606 };
607 }
608}