1use crate::ast::{Inline, ShortArgs};
2use crate::diag::{Code, Diagnostic};
3use crate::shortcode::ArgValue;
4use crate::span::Span;
5use std::collections::BTreeMap;
6
7pub fn parse_inline(line: &str, base: u32) -> (Vec<Inline>, Vec<Diagnostic>) {
8 let mut p = Parser {
9 src: line,
10 base,
11 pos: 0,
12 diags: Vec::new(),
13 };
14 let nodes = p.parse_until(None);
15 (nodes, p.diags)
16}
17
18struct Parser<'a> {
19 src: &'a str,
20 base: u32,
21 pos: usize,
22 diags: Vec<Diagnostic>,
23}
24
25const SPECIAL: [bool; 256] = {
32 let mut t = [false; 256];
33 t[b'\\' as usize] = true;
34 t[b'`' as usize] = true;
35 t[b'@' as usize] = true;
36 t[b'*' as usize] = true;
37 t[b'_' as usize] = true;
38 t[b'+' as usize] = true;
39 t[b'~' as usize] = true;
40 t[b']' as usize] = true;
41 t
42};
43
44impl<'a> Parser<'a> {
45 fn span(&self, start: usize, len: usize) -> Span {
46 Span::new(self.base as usize + start, len)
47 }
48
49 fn peek(&self) -> Option<u8> {
50 self.src.as_bytes().get(self.pos).copied()
51 }
52
53 fn parse_until(&mut self, terminator: Option<u8>) -> Vec<Inline> {
54 let mut out: Vec<Inline> = Vec::new();
55 let mut text_start = self.pos;
56 let bytes = self.src.as_bytes();
57
58 while self.pos < bytes.len() {
59 let c = bytes[self.pos];
60 if !SPECIAL[c as usize] {
61 self.pos += 1;
62 continue;
63 }
64 if Some(c) == terminator {
65 break;
66 }
67 match c {
68 b'\\' => {
69 self.flush_text(&mut out, text_start);
70 if let Some(esc_char) = self.src[self.pos + 1..].chars().next() {
71 let w = esc_char.len_utf8();
72 let s = self.span(self.pos, 1 + w);
73 out.push(Inline::Text {
74 value: esc_char.to_string(),
75 span: s,
76 });
77 self.pos += 1 + w;
78 } else {
79 self.pos += 1;
80 }
81 text_start = self.pos;
82 }
83 b'`' => {
84 self.flush_text(&mut out, text_start);
85 self.parse_code(&mut out);
86 text_start = self.pos;
87 }
88 b'@' => {
89 self.flush_text(&mut out, text_start);
90 if !self.try_parse_shortcode(&mut out) {
91 out.push(Inline::Text {
92 value: "@".to_string(),
93 span: self.span(self.pos, 1),
94 });
95 self.pos += 1;
96 }
97 text_start = self.pos;
98 }
99 b'*' | b'_' | b'+' | b'~' if self.is_open_marker() => {
100 self.flush_text(&mut out, text_start);
101 self.parse_emph(&mut out, c);
102 text_start = self.pos;
103 }
104 _ => {
105 self.pos += 1;
110 }
111 }
112 }
113 self.flush_text(&mut out, text_start);
114 out
115 }
116
117 fn flush_text(&self, out: &mut Vec<Inline>, start: usize) {
118 if start < self.pos {
119 let value = self.src[start..self.pos].to_string();
120 out.push(Inline::Text {
121 value,
122 span: self.span(start, self.pos - start),
123 });
124 }
125 }
126
127 fn is_open_marker(&self) -> bool {
128 is_open_marker_at(self.src.as_bytes(), self.pos)
129 }
130
131 fn is_close_marker(&self, marker: u8) -> bool {
132 let bytes = self.src.as_bytes();
133 let pos = self.pos;
134 if bytes.get(pos) != Some(&marker) {
135 return false;
136 }
137 if bytes.get(pos + 1) == Some(&marker) {
138 return false;
139 }
140 let prev = if pos == 0 { None } else { Some(bytes[pos - 1]) };
141 let next = bytes.get(pos + 1).copied();
142 let prev_ok = matches!(prev, Some(b) if b != b' ');
143 let next_ok = match next {
144 None => true,
145 Some(b' ') => true,
146 Some(b) => is_inline_sigil(b) || is_punct(b),
147 };
148 prev_ok && next_ok
149 }
150
151 fn parse_emph(&mut self, out: &mut Vec<Inline>, marker: u8) {
152 let start = self.pos;
153 self.pos += 1;
154 let inner_start = self.pos;
155 let mut content: Vec<Inline> = Vec::new();
156 let mut text_start = inner_start;
157 let mut closed = false;
158 let bytes = self.src.as_bytes();
159
160 while self.pos < bytes.len() {
161 let c = bytes[self.pos];
162 if !SPECIAL[c as usize] {
163 self.pos += 1;
164 continue;
165 }
166 if c == marker && self.is_close_marker(marker) {
167 if text_start < self.pos {
168 content.push(Inline::Text {
169 value: self.src[text_start..self.pos].to_string(),
170 span: self.span(text_start, self.pos - text_start),
171 });
172 }
173 self.pos += 1;
174 closed = true;
175 break;
176 }
177 if c == marker {
178 self.diags.push(
179 Diagnostic::new(Code::EmphasisSameMarker, self.span(self.pos, 1))
180 .label("inner emphasis re-uses the same marker")
181 .help("use a different emphasis marker for the inner span"),
182 );
183 self.pos += 1;
184 continue;
185 }
186 match c {
187 b'\\' => {
188 if text_start < self.pos {
189 content.push(Inline::Text {
190 value: self.src[text_start..self.pos].to_string(),
191 span: self.span(text_start, self.pos - text_start),
192 });
193 }
194 if let Some(esc_char) = self.src[self.pos + 1..].chars().next() {
195 let w = esc_char.len_utf8();
196 content.push(Inline::Text {
197 value: esc_char.to_string(),
198 span: self.span(self.pos, 1 + w),
199 });
200 self.pos += 1 + w;
201 } else {
202 self.pos += 1;
203 }
204 text_start = self.pos;
205 }
206 b'`' => {
207 if text_start < self.pos {
208 content.push(Inline::Text {
209 value: self.src[text_start..self.pos].to_string(),
210 span: self.span(text_start, self.pos - text_start),
211 });
212 }
213 self.parse_code(&mut content);
214 text_start = self.pos;
215 }
216 b'@' => {
217 if text_start < self.pos {
218 content.push(Inline::Text {
219 value: self.src[text_start..self.pos].to_string(),
220 span: self.span(text_start, self.pos - text_start),
221 });
222 }
223 if !self.try_parse_shortcode(&mut content) {
224 content.push(Inline::Text {
225 value: "@".to_string(),
226 span: self.span(self.pos, 1),
227 });
228 self.pos += 1;
229 }
230 text_start = self.pos;
231 }
232 b'*' | b'_' | b'+' | b'~' if c != marker && self.is_open_marker() => {
233 if text_start < self.pos {
234 content.push(Inline::Text {
235 value: self.src[text_start..self.pos].to_string(),
236 span: self.span(text_start, self.pos - text_start),
237 });
238 }
239 self.parse_emph(&mut content, c);
240 text_start = self.pos;
241 }
242 _ => {
243 self.pos += 1;
246 }
247 }
248 }
249 if !closed {
250 self.diags.push(
251 Diagnostic::new(Code::UnterminatedEmph, self.span(start, 1))
252 .label(format!("opened with `{}`", marker as char)),
253 );
254 }
255 let span = self.span(start, self.pos - start);
256 let node = match marker {
257 b'*' => Inline::Bold { content, span },
258 b'_' => Inline::Italic { content, span },
259 b'+' => Inline::Underline { content, span },
260 b'~' => Inline::Strike { content, span },
261 _ => unreachable!(),
262 };
263 out.push(node);
264 }
265
266 fn parse_code(&mut self, out: &mut Vec<Inline>) {
267 let start = self.pos;
268 let mut ticks = 0;
269 while self.peek() == Some(b'`') && ticks < 2 {
270 self.pos += 1;
271 ticks += 1;
272 }
273 if self.peek() == Some(b'`') {
274 out.push(Inline::Text {
275 value: self.src[start..self.pos].to_string(),
276 span: self.span(start, self.pos - start),
277 });
278 return;
279 }
280 let body_start = self.pos;
281 let needle = if ticks == 1 {
282 "`".to_string()
283 } else {
284 "``".to_string()
285 };
286 let rest = &self.src[body_start..];
287 if let Some(rel) = rest.find(&needle) {
288 let body = &self.src[body_start..body_start + rel];
289 self.pos = body_start + rel + needle.len();
290 out.push(Inline::InlineCode {
291 value: body.to_string(),
292 span: self.span(start, self.pos - start),
293 });
294 } else {
295 self.diags.push(Diagnostic::new(
296 Code::UnterminatedCode,
297 self.span(start, ticks),
298 ));
299 out.push(Inline::Text {
300 value: self.src[start..self.pos].to_string(),
301 span: self.span(start, self.pos - start),
302 });
303 }
304 }
305
306 fn try_parse_shortcode(&mut self, out: &mut Vec<Inline>) -> bool {
307 let saved = self.pos;
308 if self.peek() != Some(b'@') {
309 return false;
310 }
311 let mut cursor = self.pos + 1;
312 let bytes = self.src.as_bytes();
313 if bytes
314 .get(cursor)
315 .map(|b| !b.is_ascii_alphabetic())
316 .unwrap_or(true)
317 {
318 return false;
319 }
320 let name_start = cursor;
321 while let Some(&b) = bytes.get(cursor) {
322 if b.is_ascii_alphanumeric() || b == b'-' {
323 cursor += 1;
324 } else {
325 break;
326 }
327 }
328 let name = self.src[name_start..cursor].to_string();
329 let mut args = ShortArgs::default();
330 if bytes.get(cursor) == Some(&b'(') {
331 match parse_args(self.src, &mut cursor) {
332 Ok(a) => args = a,
333 Err(d) => {
334 self.diags.push(d.label("in inline shortcode"));
335 self.pos = cursor;
336 out.push(Inline::Text {
337 value: self.src[saved..self.pos].to_string(),
338 span: self.span(saved, self.pos - saved),
339 });
340 return true;
341 }
342 }
343 }
344 self.pos = cursor;
345 let mut content = None;
346 if self.peek() == Some(b'[') {
347 self.pos += 1;
348 let inner = self.parse_until(Some(b']'));
349 if self.peek() == Some(b']') {
350 self.pos += 1;
351 }
352 content = Some(inner);
353
354 if self.peek() == Some(b'(') {
361 self.pos += 1;
362 let url_start = self.pos;
363 let mut open_parens = 0usize;
364 while let Some(b) = self.peek() {
365 match b {
366 b'(' => open_parens += 1,
367 b')' => {
368 if open_parens == 0 {
369 break;
370 }
371 open_parens -= 1;
372 }
373 _ => {}
374 }
375 self.pos += 1;
376 }
377 let url = self.src[url_start..self.pos].to_string();
378 if self.peek() == Some(b')') {
379 self.pos += 1;
380 } else {
381 self.diags.push(
382 Diagnostic::new(
383 Code::BadArgSyntax,
384 self.span(url_start, self.pos - url_start),
385 )
386 .label("`(url)` is never closed")
387 .help("percent-encode unmatched parentheses in the URL as %28 / %29"),
388 );
389 }
390 args.positional.push(ArgValue::Str(url));
391 }
392 }
393 let span = self.span(saved, self.pos - saved);
394 out.push(Inline::Shortcode {
395 name,
396 args,
397 content,
398 span,
399 });
400 true
401 }
402}
403
404pub(crate) fn is_inline_sigil(b: u8) -> bool {
405 matches!(b, b'*' | b'_' | b'+' | b'~' | b'`' | b'@' | b'[' | b']')
406}
407
408pub(crate) fn is_punct(b: u8) -> bool {
409 matches!(
410 b,
411 b'.' | b',' | b';' | b':' | b'!' | b'?' | b')' | b'(' | b'"' | b'\'' | b'-' | b'/'
412 )
413}
414
415pub(crate) fn is_open_marker_at(bytes: &[u8], pos: usize) -> bool {
420 let marker = match bytes.get(pos) {
421 Some(&b @ (b'*' | b'_' | b'+' | b'~')) => b,
422 _ => return false,
423 };
424 let prev = if pos == 0 { None } else { Some(bytes[pos - 1]) };
425 let next = bytes.get(pos + 1).copied();
426 if next == Some(marker) || prev == Some(marker) {
427 return false;
428 }
429 let prev_ok = match prev {
430 None => true,
431 Some(b' ') => true,
432 Some(b) if is_inline_sigil(b) => true,
433 Some(b) if is_punct(b) => true,
434 _ => false,
435 };
436 let next_ok = matches!(next, Some(b) if b != b' ' && b != marker);
437 prev_ok && next_ok
438}
439
440pub fn parse_args(src: &str, cursor: &mut usize) -> Result<ShortArgs, Diagnostic> {
441 let bytes = src.as_bytes();
442 if bytes.get(*cursor) != Some(&b'(') {
443 return Ok(ShortArgs::default());
444 }
445 *cursor += 1;
446 let mut args = ShortArgs::default();
447 let mut keys_seen: BTreeMap<String, ()> = BTreeMap::new();
448 skip_ws(src, cursor);
449 if bytes.get(*cursor) == Some(&b')') {
450 *cursor += 1;
451 return Ok(args);
452 }
453 loop {
454 skip_ws(src, cursor);
455 let arg_start = *cursor;
456 let saved = *cursor;
457 if let Some(name) = read_ident(src, cursor) {
458 skip_ws(src, cursor);
459 if bytes.get(*cursor) == Some(&b':') {
460 *cursor += 1;
461 skip_ws(src, cursor);
462 let v = read_value(src, cursor)
463 .ok_or_else(|| Diagnostic::new(Code::BadArgSyntax, Span::new(*cursor, 1)))?;
464 if keys_seen.insert(name.clone(), ()).is_some() {
465 return Err(Diagnostic::new(
466 Code::DuplicateKwarg,
467 Span::new(arg_start, name.len()),
468 ));
469 }
470 args.keyword.insert(name, v);
471 } else {
472 *cursor = saved;
473 let v = read_value(src, cursor)
474 .ok_or_else(|| Diagnostic::new(Code::BadArgSyntax, Span::new(*cursor, 1)))?;
475 args.positional.push(v);
476 }
477 } else {
478 let v = read_value(src, cursor)
479 .ok_or_else(|| Diagnostic::new(Code::BadArgSyntax, Span::new(*cursor, 1)))?;
480 args.positional.push(v);
481 }
482 skip_ws(src, cursor);
483 match bytes.get(*cursor) {
484 Some(&b',') => {
485 *cursor += 1;
486 continue;
487 }
488 Some(&b')') => {
489 *cursor += 1;
490 break;
491 }
492 _ => return Err(Diagnostic::new(Code::BadArgSyntax, Span::new(*cursor, 1))),
493 }
494 }
495 Ok(args)
496}
497
498fn skip_ws(src: &str, cursor: &mut usize) {
499 while src.as_bytes().get(*cursor) == Some(&b' ') {
500 *cursor += 1;
501 }
502}
503
504fn read_ident(src: &str, cursor: &mut usize) -> Option<String> {
505 let bytes = src.as_bytes();
506 let start = *cursor;
507 let first = *bytes.get(start)?;
508 if !first.is_ascii_alphabetic() {
509 return None;
510 }
511 let mut end = start + 1;
512 while let Some(&b) = bytes.get(end) {
513 if b.is_ascii_alphanumeric() || b == b'-' || b == b'_' {
514 end += 1;
515 } else {
516 break;
517 }
518 }
519 *cursor = end;
520 Some(src[start..end].to_string())
521}
522
523fn read_value(src: &str, cursor: &mut usize) -> Option<ArgValue> {
524 skip_ws(src, cursor);
525 let bytes = src.as_bytes();
526 let start = *cursor;
527 match bytes.get(start)? {
528 b'"' => {
529 *cursor += 1;
530 let mut s = String::new();
531 while *cursor < bytes.len() {
532 let b = bytes[*cursor];
533 if b == b'"' {
534 *cursor += 1;
535 return Some(ArgValue::Str(s));
536 }
537 if b == b'\\' {
538 if let Some(c) = src[*cursor + 1..].chars().next() {
539 s.push(c);
540 *cursor += 1 + c.len_utf8();
541 continue;
542 }
543 s.push('\\');
545 *cursor += 1;
546 continue;
547 }
548 let c = src[*cursor..].chars().next().expect("cursor < len");
551 s.push(c);
552 *cursor += c.len_utf8();
553 }
554 None
555 }
556 b'[' => {
557 *cursor += 1;
558 let mut arr: Vec<ArgValue> = Vec::new();
559 skip_ws(src, cursor);
560 if bytes.get(*cursor) == Some(&b']') {
561 *cursor += 1;
562 return Some(ArgValue::Array(arr));
563 }
564 loop {
565 let v = read_value(src, cursor)?;
566 arr.push(v);
567 skip_ws(src, cursor);
568 match bytes.get(*cursor) {
569 Some(&b',') => {
570 *cursor += 1;
571 skip_ws(src, cursor);
572 }
573 Some(&b']') => {
574 *cursor += 1;
575 return Some(ArgValue::Array(arr));
576 }
577 _ => return None,
578 }
579 }
580 }
581 c if c.is_ascii_digit() || *c == b'-' => {
582 let mut end = start;
583 if bytes[end] == b'-' {
584 end += 1;
585 }
586 while let Some(&b) = bytes.get(end) {
587 if b.is_ascii_digit() {
588 end += 1;
589 } else {
590 break;
591 }
592 }
593 let n: i64 = src[start..end].parse().ok()?;
594 *cursor = end;
595 Some(ArgValue::Int(n))
596 }
597 c if c.is_ascii_alphabetic() => {
598 let id = read_ident(src, cursor)?;
599 Some(ArgValue::Ident(id))
600 }
601 _ => None,
602 }
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608
609 fn parse(s: &str) -> (Vec<Inline>, Vec<Diagnostic>) {
610 parse_inline(s, 0)
611 }
612
613 #[test]
614 fn plain_text() {
615 let (n, d) = parse("hello world");
616 assert!(d.is_empty());
617 assert_eq!(n.len(), 1);
618 if let Inline::Text { value, .. } = &n[0] {
619 assert_eq!(value, "hello world");
620 } else {
621 panic!();
622 }
623 }
624
625 #[test]
626 fn bold() {
627 let (n, d) = parse("a *bold* b");
628 assert!(d.is_empty(), "{:?}", d);
629 assert!(matches!(n[1], Inline::Bold { .. }));
630 }
631
632 #[test]
633 fn snake_case_is_literal() {
634 let (n, d) = parse("snake_case_name");
635 assert!(d.is_empty());
636 assert_eq!(n.len(), 1);
637 assert!(matches!(n[0], Inline::Text { .. }));
638 }
639
640 #[test]
641 fn nested_same_marker_errors() {
642 let (_, d) = parse("*outer *inner* outer*");
643 assert!(d.iter().any(|x| x.code == Code::EmphasisSameMarker));
644 }
645
646 #[test]
647 fn link_url_with_balanced_parens() {
648 let (n, d) =
649 parse("@link[rust](https://en.wikipedia.org/wiki/Rust_(programming_language)) x");
650 assert!(d.is_empty(), "{:?}", d);
651 if let Inline::Shortcode { args, .. } = &n[0] {
652 assert_eq!(
653 args.positional[0].as_str(),
654 Some("https://en.wikipedia.org/wiki/Rust_(programming_language)")
655 );
656 } else {
657 panic!("expected shortcode, got {:?}", n[0]);
658 }
659 if let Inline::Text { value, .. } = &n[1] {
660 assert_eq!(value, " x");
661 } else {
662 panic!("tail must survive as text");
663 }
664 }
665
666 #[test]
667 fn link_url_unterminated_paren_is_b0406() {
668 let (_, d) = parse("@link[x](https://e.com/a(b now");
669 assert!(d.iter().any(|x| x.code == Code::BadArgSyntax), "{:?}", d);
670 }
671
672 #[test]
673 fn inline_code() {
674 let (n, d) = parse("use `printf` here");
675 assert!(d.is_empty());
676 assert!(matches!(n[1], Inline::InlineCode { .. }));
677 }
678
679 #[test]
680 fn double_backtick_code_with_backtick() {
681 let (n, d) = parse("``a ` b``");
682 assert!(d.is_empty());
683 if let Inline::InlineCode { value, .. } = &n[0] {
684 assert_eq!(value, "a ` b");
685 } else {
686 panic!();
687 }
688 }
689
690 #[test]
691 fn shortcode_inline() {
692 let (n, d) = parse("see @link[here](https://x)");
693 assert!(d.is_empty(), "{:?}", d);
694 assert!(matches!(n.last().unwrap(), Inline::Shortcode { .. }));
695 }
696
697 #[test]
698 fn escape_emphasis() {
699 let (n, d) = parse(r"\*literal\*");
700 assert!(d.is_empty());
701 let joined: String = n
702 .iter()
703 .filter_map(|x| {
704 if let Inline::Text { value, .. } = x {
705 Some(value.clone())
706 } else {
707 None
708 }
709 })
710 .collect();
711 assert_eq!(joined, "*literal*");
712 }
713
714 #[test]
715 fn double_marker_not_emphasis() {
716 let (n, _d) = parse("**no**");
717 assert!(!matches!(n[0], Inline::Bold { .. }));
718 }
719
720 #[test]
721 fn escape_before_multibyte_char() {
722 let (n, d) = parse("a \\é b");
726 assert!(d.is_empty(), "{:?}", d);
727 let joined: String = n
728 .iter()
729 .filter_map(|x| {
730 if let Inline::Text { value, .. } = x {
731 Some(value.clone())
732 } else {
733 None
734 }
735 })
736 .collect();
737 assert_eq!(joined, "a é b");
738 }
739
740 #[test]
741 fn multibyte_text_then_emph() {
742 let (n, d) = parse("日本 *bold*");
745 assert!(d.is_empty(), "{:?}", d);
746 assert!(matches!(n.last().unwrap(), Inline::Bold { .. }));
747 }
748
749 #[test]
750 fn arg_string_preserves_multibyte() {
751 let mut cursor = 0usize;
754 let s = "(label: \"日本 🦀\")";
755 let args = parse_args(s, &mut cursor).unwrap();
756 if let ArgValue::Str(v) = args.keyword.get("label").unwrap() {
757 assert_eq!(v, "日本 🦀");
758 } else {
759 panic!();
760 }
761 }
762
763 #[test]
764 fn escape_at_end_of_input() {
765 let (_, _d) = parse("trailing\\");
767 }
768}