1use crate::options::ParserOptions;
4use crate::syntax::SyntaxKind;
5use rowan::GreenNodeBuilder;
6use unicode_width::UnicodeWidthChar;
7
8use crate::parser::utils::attributes::{
9 emit_attribute_node, try_parse_trailing_attributes_with_pos,
10};
11use crate::parser::utils::helpers::{emit_line_tokens, emit_separator_tokens, strip_newline};
12use crate::parser::utils::inline_emission;
13
14use super::container_prefix::StrippedLines;
15
16pub(crate) trait LineView {
31 fn line(&self, i: usize) -> &str;
33 fn line_count(&self) -> usize;
35}
36
37impl LineView for [&str] {
38 fn line(&self, i: usize) -> &str {
39 self[i]
40 }
41 fn line_count(&self) -> usize {
42 self.len()
43 }
44}
45
46impl<'a, 'p> LineView for StrippedLines<'a, 'p> {
47 fn line(&self, i: usize) -> &str {
48 self.strip_at(i)
49 }
50 fn line_count(&self) -> usize {
51 self.raw().len()
52 }
53}
54
55pub(crate) struct UniformStripView<'s, 'a, 'p>(&'s StrippedLines<'a, 'p>);
65
66impl<'s, 'a, 'p> LineView for UniformStripView<'s, 'a, 'p> {
67 fn line(&self, i: usize) -> &str {
68 self.0.prefix().strip(self.0.raw()[i])
69 }
70 fn line_count(&self) -> usize {
71 self.0.raw().len()
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum Alignment {
77 Left,
78 Right,
79 Center,
80 Default,
81}
82
83#[derive(Debug, Clone)]
85pub(crate) struct Column {
86 start: usize,
88 end: usize,
90 alignment: Alignment,
92}
93
94pub(crate) fn try_parse_table_separator(line: &str) -> Option<Vec<Column>> {
97 let trimmed = line.trim_start();
98 let (trimmed, newline_str) = strip_newline(trimmed);
100 let leading_spaces = line.len() - trimmed.len() - newline_str.len();
101
102 if leading_spaces > 3 {
104 return None;
105 }
106
107 if trimmed.contains('*') || trimmed.contains('_') {
109 return None;
110 }
111
112 if !trimmed.contains('-') {
114 return None;
115 }
116
117 if !trimmed.chars().all(|c| c == '-' || c == ' ') {
119 return None;
120 }
121
122 let dash_groups: Vec<_> = trimmed.split(' ').filter(|s| !s.is_empty()).collect();
124 if dash_groups.len() <= 1 {
125 return None;
126 }
127
128 let columns = extract_columns(trimmed, leading_spaces);
130
131 if columns.is_empty() {
132 return None;
133 }
134
135 Some(columns)
136}
137
138fn extract_columns(separator: &str, offset: usize) -> Vec<Column> {
140 let mut columns = Vec::new();
141 let mut in_dashes = false;
142 let mut col_start = 0;
143
144 for (i, ch) in separator.char_indices() {
145 match ch {
146 '-' if !in_dashes => {
147 col_start = i + offset;
148 in_dashes = true;
149 }
150 ' ' if in_dashes => {
151 columns.push(Column {
152 start: col_start,
153 end: i + offset,
154 alignment: Alignment::Default, });
156 in_dashes = false;
157 }
158 _ => {}
159 }
160 }
161
162 if in_dashes {
164 columns.push(Column {
165 start: col_start,
166 end: separator.len() + offset,
167 alignment: Alignment::Default,
168 });
169 }
170
171 columns
172}
173
174fn parse_single_dash_run(line: &str) -> Option<Vec<Column>> {
181 let (content, _) = strip_newline(line);
182 let trimmed = content.trim_start();
183 let leading_spaces = content.len() - trimmed.len();
184
185 if leading_spaces > 3 {
187 return None;
188 }
189
190 let dashes = trimmed.trim_end();
191 if dashes.len() < 2 || !dashes.chars().all(|c| c == '-') {
192 return None;
193 }
194
195 Some(vec![Column {
196 start: leading_spaces,
197 end: leading_spaces + dashes.len(),
198 alignment: Alignment::Default,
199 }])
200}
201
202fn find_single_column_table_end(lines: &(impl LineView + ?Sized), start: usize) -> Option<usize> {
209 let mut saw_row = false;
210 for i in start..lines.line_count() {
211 let line = lines.line(i);
212 if line.trim().is_empty() {
213 return None;
214 }
215 if try_parse_table_separator(line).is_some() || parse_single_dash_run(line).is_some() {
216 return saw_row.then_some(i + 1);
217 }
218 saw_row = true;
219 }
220 None
221}
222
223fn column_offset_to_byte_index(line: &str, offset: usize) -> usize {
234 let mut width = 0;
235 for (byte_idx, ch) in line.char_indices() {
236 if width >= offset {
237 return byte_idx;
238 }
239 width += UnicodeWidthChar::width(ch).unwrap_or(0);
240 }
241 line.len()
242}
243
244fn try_parse_caption_prefix(line: &str) -> Option<(usize, &str)> {
247 let trimmed = line.trim_start();
248 let leading_spaces = line.len() - trimmed.len();
249
250 if leading_spaces > 3 {
252 return None;
253 }
254
255 if let Some(rest) = trimmed.strip_prefix("Table:") {
257 Some((leading_spaces + 6, rest))
258 } else if let Some(rest) = trimmed.strip_prefix("table:") {
259 Some((leading_spaces + 6, rest))
260 } else if let Some(rest) = trimmed.strip_prefix(':') {
261 if rest.starts_with(|c: char| c.is_whitespace()) {
264 Some((leading_spaces + 1, rest))
265 } else {
266 None
267 }
268 } else {
269 None
270 }
271}
272
273fn is_table_caption_start(line: &str) -> bool {
275 try_parse_caption_prefix(line).is_some()
276}
277
278fn is_bare_colon_caption_start(line: &str) -> bool {
279 let trimmed = line.trim_start();
280 trimmed.starts_with(':') && !trimmed.starts_with("::") && !trimmed.starts_with(":::")
281}
282
283fn bare_colon_caption_looks_like_definition_code_block(line: &str) -> bool {
284 let Some((_, rest)) = try_parse_caption_prefix(line) else {
285 return false;
286 };
287 let trimmed = rest.trim_start();
288 trimmed.starts_with("```") || trimmed.starts_with("~~~")
289}
290
291fn line_is_fenced_div_fence(line: &str) -> bool {
292 let trimmed = line.trim_start();
293 let colon_count = trimmed.chars().take_while(|&c| c == ':').count();
294 if colon_count < 3 {
295 return false;
296 }
297 let rest = &trimmed[colon_count..];
298 rest.is_empty() || rest.starts_with(char::is_whitespace)
299}
300
301fn is_valid_caption_start_before_table(lines: &(impl LineView + ?Sized), pos: usize) -> bool {
302 if !is_table_caption_start(lines.line(pos)) {
303 return false;
304 }
305
306 if is_bare_colon_caption_start(lines.line(pos))
307 && bare_colon_caption_looks_like_definition_code_block(lines.line(pos))
308 {
309 return false;
310 }
311
312 if is_bare_colon_caption_start(lines.line(pos))
314 && pos > 0
315 && !lines.line(pos - 1).trim().is_empty()
316 && !line_is_fenced_div_fence(lines.line(pos - 1))
317 {
318 return false;
319 }
320 true
321}
322
323fn is_grid_table_start(line: &str) -> bool {
326 try_parse_grid_separator(line).is_some()
327}
328
329fn is_multiline_table_start(line: &str) -> bool {
334 try_parse_multiline_separator(line).is_some() || is_column_separator(line)
335}
336
337pub(crate) fn is_caption_followed_by_table(
340 lines: &(impl LineView + ?Sized),
341 caption_pos: usize,
342) -> bool {
343 if caption_pos >= lines.line_count() {
344 return false;
345 }
346
347 if !is_valid_caption_start_before_table(lines, caption_pos) {
349 return false;
350 }
351
352 let mut pos = caption_pos + 1;
353
354 while pos < lines.line_count()
358 && !lines.line(pos).trim().is_empty()
359 && !line_is_fenced_div_fence(lines.line(pos))
360 {
361 if try_parse_table_separator(lines.line(pos)).is_some() {
363 return true;
364 }
365 pos += 1;
366 }
367
368 if pos < lines.line_count() && lines.line(pos).trim().is_empty() {
370 pos += 1;
371 }
372
373 table_grid_starts_at(lines, pos)
375}
376
377fn table_grid_starts_at(lines: &(impl LineView + ?Sized), pos: usize) -> bool {
390 if pos >= lines.line_count() {
391 return false;
392 }
393 let line = lines.line(pos);
394
395 if is_grid_table_start(line) {
397 return true;
398 }
399
400 if is_multiline_table_start(line) {
402 return true;
403 }
404
405 if parse_single_dash_run(line).is_some()
411 && find_single_column_table_end(lines, pos + 1).is_some()
412 {
413 return true;
414 }
415
416 if try_parse_table_separator(line).is_some() {
418 return true;
419 }
420
421 if pos + 1 < lines.line_count() && !line.trim().is_empty() {
423 let next_line = lines.line(pos + 1);
424 if try_parse_table_separator(next_line).is_some()
425 || try_parse_pipe_separator(next_line).is_some()
426 {
427 return true;
428 }
429 }
430
431 false
432}
433
434fn caption_range_starting_at(
435 lines: &(impl LineView + ?Sized),
436 start: usize,
437) -> Option<(usize, usize)> {
438 if start >= lines.line_count() || !is_table_caption_start(lines.line(start)) {
439 return None;
440 }
441 let mut end = start + 1;
442 while end < lines.line_count()
443 && !lines.line(end).trim().is_empty()
444 && !line_is_fenced_div_fence(lines.line(end))
445 {
446 end += 1;
447 }
448 Some((start, end))
449}
450
451fn find_caption_before_table(
454 lines: &(impl LineView + ?Sized),
455 table_start: usize,
456) -> Option<(usize, usize)> {
457 if table_start == 0 {
458 return None;
459 }
460
461 let mut pos = table_start - 1;
464
465 if lines.line(pos).trim().is_empty() {
467 if pos == 0 {
468 return None;
469 }
470 pos -= 1;
471 }
472
473 let caption_end = pos + 1; if !is_valid_caption_start_before_table(lines, pos) {
480 let mut scan_pos = pos;
482 while scan_pos > 0 {
483 scan_pos -= 1;
484 let line = lines.line(scan_pos);
485
486 if line.trim().is_empty() || line_is_fenced_div_fence(line) {
488 return None;
489 }
490
491 if is_valid_caption_start_before_table(lines, scan_pos) {
493 if scan_pos > 0 && !lines.line(scan_pos - 1).trim().is_empty() {
494 return None;
495 }
496 if previous_nonblank_looks_like_table(lines, scan_pos) {
497 return None;
498 }
499 return Some((scan_pos, caption_end));
500 }
501 }
502 None
504 } else {
505 if pos > 0 && !lines.line(pos - 1).trim().is_empty() {
506 return None;
507 }
508 if previous_nonblank_looks_like_table(lines, pos) {
509 return None;
510 }
511 Some((pos, caption_end))
513 }
514}
515
516fn previous_nonblank_looks_like_table(lines: &(impl LineView + ?Sized), pos: usize) -> bool {
517 if pos == 0 {
518 return false;
519 }
520 let mut i = pos;
522 while i > 0 && lines.line(i - 1).trim().is_empty() {
523 i -= 1;
524 }
525 while i > 0 {
533 i -= 1;
534 if lines.line(i).trim().is_empty() || line_is_fenced_div_fence(lines.line(i)) {
535 break;
536 }
537 if line_looks_like_table_syntax(lines.line(i).trim()) {
538 return true;
539 }
540 }
541 false
542}
543
544fn line_looks_like_table_syntax(line: &str) -> bool {
545 if line.starts_with('|') && line.matches('|').count() >= 2 {
546 return true;
547 }
548 if line.starts_with('+') && line.ends_with('+') && (line.contains('-') || line.contains('=')) {
549 return true;
550 }
551 try_parse_table_separator(line).is_some()
552 || try_parse_pipe_separator(line).is_some()
553 || try_parse_grid_separator(line).is_some()
554}
555
556fn find_caption_after_table(
559 lines: &(impl LineView + ?Sized),
560 table_end: usize,
561) -> Option<(usize, usize)> {
562 if table_end >= lines.line_count() {
563 return None;
564 }
565
566 let mut pos = table_end;
567
568 if pos < lines.line_count() && lines.line(pos).trim().is_empty() {
570 pos += 1;
571 }
572
573 if pos >= lines.line_count() {
574 return None;
575 }
576
577 if is_table_caption_start(lines.line(pos)) {
579 let caption_start = pos;
580 let mut caption_end = caption_start + 1;
582 while caption_end < lines.line_count()
583 && !lines.line(caption_end).trim().is_empty()
584 && !line_is_fenced_div_fence(lines.line(caption_end))
585 {
586 caption_end += 1;
587 }
588 Some((caption_start, caption_end))
589 } else {
590 None
591 }
592}
593
594fn emit_caption_line_text(
601 builder: &mut GreenNodeBuilder<'static>,
602 text_with_newline: &str,
603 config: &ParserOptions,
604 lift_trailing_attrs: bool,
605) {
606 let (text, newline_str) = strip_newline(text_with_newline);
607
608 if lift_trailing_attrs
609 && !text.is_empty()
610 && let Some((_attrs, before_attrs, start_brace_pos)) =
611 try_parse_trailing_attributes_with_pos(text)
612 {
613 let trimmed_len = text.trim_end().len();
614 let space = &text[before_attrs.len()..start_brace_pos];
615 let raw_attrs = &text[start_brace_pos..trimmed_len];
616 let trailing_ws = &text[trimmed_len..];
617
618 if !before_attrs.is_empty() {
619 inline_emission::emit_inlines(builder, before_attrs, config, false);
620 }
621 if !space.is_empty() {
622 builder.token(SyntaxKind::WHITESPACE.into(), space);
623 }
624 emit_attribute_node(builder, raw_attrs);
625 if !trailing_ws.is_empty() {
626 builder.token(SyntaxKind::WHITESPACE.into(), trailing_ws);
627 }
628 if !newline_str.is_empty() {
629 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
630 }
631 return;
632 }
633
634 if !text.is_empty() {
635 inline_emission::emit_inlines(builder, text, config, false);
636 }
637 if !newline_str.is_empty() {
638 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
639 }
640}
641
642fn emit_caption_blank_lines(
648 builder: &mut GreenNodeBuilder<'static>,
649 window: &StrippedLines<'_, '_>,
650 from: usize,
651 to: usize,
652) {
653 for abs in from..to {
654 if window.line(abs).trim().is_empty() {
657 builder.start_node(SyntaxKind::BLANK_LINE.into());
658 let tail = window.emit_or_dispatch_tail(builder, abs);
659 builder.token(SyntaxKind::BLANK_LINE.into(), tail);
660 builder.finish_node();
661 }
662 }
663}
664
665fn emit_table_caption(
666 builder: &mut GreenNodeBuilder<'static>,
667 window: &StrippedLines<'_, '_>,
668 start: usize,
669 end: usize,
670 config: &ParserOptions,
671) {
672 builder.start_node(SyntaxKind::TABLE_CAPTION.into());
673
674 let last_idx = (end - start).saturating_sub(1);
675
676 for (i, abs) in (start..end).enumerate() {
677 let lift_attrs = i == last_idx;
678
679 let tail = window.emit_or_dispatch_tail(builder, abs);
686
687 if i == 0 {
688 let trimmed = tail.trim_start();
690 let leading_ws_len = tail.len() - trimmed.len();
691
692 if leading_ws_len > 0 {
694 builder.token(SyntaxKind::WHITESPACE.into(), &tail[..leading_ws_len]);
695 }
696
697 let prefix_and_rest = if tail.ends_with('\n') {
700 &tail[leading_ws_len..tail.len() - 1] } else {
702 &tail[leading_ws_len..]
703 };
704
705 let (prefix_len, prefix_text) = if prefix_and_rest.starts_with("Table: ") {
706 (7, "Table: ")
707 } else if prefix_and_rest.starts_with("table: ") {
708 (7, "table: ")
709 } else if prefix_and_rest.starts_with(": ") {
710 (2, ": ")
711 } else if prefix_and_rest.starts_with(':') {
712 (1, ":")
713 } else {
714 (0, "")
715 };
716
717 if prefix_len > 0 {
718 builder.token(SyntaxKind::TABLE_CAPTION_PREFIX.into(), prefix_text);
719
720 let rest_start = leading_ws_len + prefix_len;
722 if rest_start < tail.len() {
723 emit_caption_line_text(builder, &tail[rest_start..], config, lift_attrs);
724 }
725 } else {
726 emit_caption_line_text(builder, &tail[leading_ws_len..], config, lift_attrs);
728 }
729 } else {
730 emit_caption_line_text(builder, tail, config, lift_attrs);
732 }
733 }
734
735 builder.finish_node(); }
737
738fn emit_table_cell(
741 builder: &mut GreenNodeBuilder<'static>,
742 cell_text: &str,
743 config: &ParserOptions,
744) {
745 builder.start_node(SyntaxKind::TABLE_CELL.into());
746
747 if !cell_text.is_empty() {
749 inline_emission::emit_inlines(builder, cell_text, config, false);
750 }
751
752 builder.finish_node(); }
754
755fn determine_alignments(columns: &mut [Column], separator_line: &str, header_line: Option<&str>) {
757 for col in columns.iter_mut() {
758 let sep_slice = &separator_line[col.start..col.end];
759
760 if let Some(header) = header_line {
761 let header_start = column_offset_to_byte_index(header, col.start);
762 let header_end = column_offset_to_byte_index(header, col.end);
763
764 let header_text = if header_start < header_end {
766 header[header_start..header_end].trim()
767 } else if header_start < header.len() {
768 header[header_start..].trim()
769 } else {
770 ""
771 };
772
773 if header_text.is_empty() {
774 col.alignment = Alignment::Default;
775 continue;
776 }
777
778 let header_in_col = &header[header_start..header_end];
780 let text_start = header_in_col.len() - header_in_col.trim_start().len();
781 let text_end = header_in_col.trim_end().len() + text_start;
782
783 let dashes_start = 0; let dashes_end = sep_slice.len();
786
787 let flush_left = dashes_start == text_start;
788 let flush_right = dashes_end == text_end;
789
790 col.alignment = match (flush_left, flush_right) {
791 (true, true) => Alignment::Default,
792 (true, false) => Alignment::Left,
793 (false, true) => Alignment::Right,
794 (false, false) => Alignment::Center,
795 };
796 } else {
797 col.alignment = Alignment::Default;
799 }
800 }
801}
802
803pub(crate) fn try_parse_simple_table(
806 window: &StrippedLines<'_, '_>,
807 builder: &mut GreenNodeBuilder<'static>,
808 config: &ParserOptions,
809) -> Option<usize> {
810 let lines = window.raw();
811 let start_pos = window.pos();
812 log::trace!("try_parse_simple_table at line {}", start_pos + 1);
813
814 if start_pos >= lines.len() {
815 return None;
816 }
817
818 let gate_first = window.strip_at(start_pos);
825 let separator_here = try_parse_table_separator(gate_first).is_some();
826 let separator_next = !separator_here
827 && start_pos + 1 < lines.len()
828 && !gate_first.trim().is_empty()
829 && try_parse_table_separator(window.strip_at(start_pos + 1)).is_some();
830 let single_column_here =
835 !separator_here && !separator_next && parse_single_dash_run(gate_first).is_some();
836 if !separator_here && !separator_next && !single_column_here {
837 return None;
838 }
839
840 let (separator_pos, end_pos) = if single_column_here {
852 let end_pos = find_single_column_table_end(window, start_pos + 1)?;
853 (start_pos, end_pos)
854 } else {
855 let separator_pos = find_separator_line(window, start_pos)?;
856 (separator_pos, find_table_end(window, separator_pos + 1))
858 };
859 log::trace!(" found separator at line {}", separator_pos + 1);
860
861 let separator_line = window.line(separator_pos);
862 let mut columns = if single_column_here {
863 parse_single_dash_run(separator_line)?
864 } else {
865 try_parse_table_separator(separator_line)?
866 };
867
868 let has_header = separator_pos > start_pos;
870 let header_line = if has_header {
871 Some(window.line(separator_pos - 1))
872 } else {
873 None
874 };
875
876 determine_alignments(&mut columns, separator_line, header_line);
878
879 let data_rows = end_pos - separator_pos - 1;
881
882 if data_rows == 0 {
883 return None;
884 }
885
886 let caption_before = find_caption_before_table(window, start_pos);
888
889 let caption_after = if caption_before.is_some() {
891 None
892 } else {
893 find_caption_after_table(window, end_pos)
894 };
895
896 builder.start_node(SyntaxKind::SIMPLE_TABLE.into());
898
899 if let Some((cap_start, cap_end)) = caption_before {
901 emit_table_caption(builder, window, cap_start, cap_end, config);
902 emit_caption_blank_lines(builder, window, cap_end, start_pos);
904 }
905
906 if has_header {
910 emit_table_row(
911 builder,
912 window,
913 separator_pos - 1,
914 &columns,
915 SyntaxKind::TABLE_HEADER,
916 config,
917 );
918 }
919
920 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
923 let separator_tail = window.emit_or_dispatch_tail(builder, separator_pos);
924 emit_separator_tokens(builder, separator_tail);
925 builder.finish_node();
926
927 for idx in (separator_pos + 1)..end_pos {
929 emit_table_row(
930 builder,
931 window,
932 idx,
933 &columns,
934 SyntaxKind::TABLE_ROW,
935 config,
936 );
937 }
938
939 if let Some((cap_start, cap_end)) = caption_after {
941 emit_caption_blank_lines(builder, window, end_pos, cap_start);
943 emit_table_caption(builder, window, cap_start, cap_end, config);
944 }
945
946 builder.finish_node(); let table_start = if let Some((cap_start, _)) = caption_before {
950 cap_start
951 } else if has_header {
952 separator_pos - 1
953 } else {
954 separator_pos
955 };
956
957 let table_end = if let Some((_, cap_end)) = caption_after {
958 cap_end
959 } else {
960 end_pos
961 };
962
963 let lines_consumed = table_end - table_start;
964
965 Some(lines_consumed)
966}
967
968fn find_separator_line(lines: &(impl LineView + ?Sized), start_pos: usize) -> Option<usize> {
970 log::trace!(" find_separator_line from line {}", start_pos + 1);
971
972 log::trace!(" checking first line: {:?}", lines.line(start_pos));
974 if try_parse_table_separator(lines.line(start_pos)).is_some() {
975 log::trace!(" separator found at first line");
976 return Some(start_pos);
977 }
978
979 if start_pos + 1 < lines.line_count()
981 && !lines.line(start_pos).trim().is_empty()
982 && try_parse_table_separator(lines.line(start_pos + 1)).is_some()
983 {
984 return Some(start_pos + 1);
985 }
986 None
987}
988
989fn find_table_end(lines: &(impl LineView + ?Sized), start_pos: usize) -> usize {
991 for i in start_pos..lines.line_count() {
992 if lines.line(i).trim().is_empty() {
993 return i;
994 }
995 if try_parse_table_separator(lines.line(i)).is_some() {
997 if i + 1 >= lines.line_count() || lines.line(i + 1).trim().is_empty() {
999 return i + 1;
1000 }
1001 }
1002 }
1003 lines.line_count()
1004}
1005
1006fn emit_table_row(
1009 builder: &mut GreenNodeBuilder<'static>,
1010 window: &StrippedLines<'_, '_>,
1011 abs_idx: usize,
1012 columns: &[Column],
1013 row_kind: SyntaxKind,
1014 config: &ParserOptions,
1015) {
1016 builder.start_node(row_kind.into());
1017
1018 let line = window.emit_or_dispatch_tail(builder, abs_idx);
1023
1024 let (line_without_newline, newline_str) = strip_newline(line);
1025
1026 let trimmed = line_without_newline.trim_start();
1028 let leading_ws_len = line_without_newline.len() - line_without_newline.trim_start().len();
1029 if leading_ws_len > 0 {
1030 builder.token(
1031 SyntaxKind::WHITESPACE.into(),
1032 &line_without_newline[..leading_ws_len],
1033 );
1034 }
1035
1036 let mut current_pos = 0;
1038
1039 for (i, col) in columns.iter().enumerate() {
1041 let cell_start = if col.start >= leading_ws_len {
1043 column_offset_to_byte_index(trimmed, col.start - leading_ws_len)
1044 } else {
1045 0
1046 };
1047
1048 let end_offset = columns.get(i + 1).map_or(usize::MAX, |next| next.start);
1054 let cell_end = if end_offset == usize::MAX {
1055 trimmed.len()
1056 } else if end_offset >= leading_ws_len {
1057 column_offset_to_byte_index(trimmed, end_offset - leading_ws_len)
1058 } else {
1059 0
1060 };
1061
1062 let cell_text = if cell_start < cell_end && cell_start < trimmed.len() {
1067 &trimmed[cell_start..cell_end]
1068 } else {
1069 ""
1070 };
1071
1072 let cell_content = cell_text.trim();
1073 let cell_content_start = cell_text.len() - cell_text.trim_start().len();
1074
1075 let content_abs_pos = (cell_start + cell_content_start).min(trimmed.len());
1077 if current_pos < content_abs_pos {
1078 builder.token(
1079 SyntaxKind::WHITESPACE.into(),
1080 &trimmed[current_pos..content_abs_pos],
1081 );
1082 }
1083
1084 emit_table_cell(builder, cell_content, config);
1086
1087 current_pos = content_abs_pos + cell_content.len();
1089 }
1090
1091 if current_pos < trimmed.len() {
1093 builder.token(SyntaxKind::WHITESPACE.into(), &trimmed[current_pos..]);
1094 }
1095
1096 if !newline_str.is_empty() {
1098 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
1099 }
1100
1101 builder.finish_node();
1102}
1103
1104fn try_parse_pipe_separator(line: &str) -> Option<Vec<Alignment>> {
1111 let trimmed = line.trim();
1112
1113 if !trimmed.contains('|') && !trimmed.contains('+') {
1115 return None;
1116 }
1117
1118 let cells: Vec<&str> = if trimmed.contains('+') {
1120 trimmed.split(['|', '+']).collect()
1122 } else {
1123 trimmed.split('|').collect()
1124 };
1125
1126 let mut alignments = Vec::new();
1127
1128 for cell in cells {
1129 let cell = cell.trim();
1130
1131 if cell.is_empty() {
1133 continue;
1134 }
1135
1136 let starts_colon = cell.starts_with(':');
1138 let ends_colon = cell.ends_with(':');
1139
1140 let without_colons = cell.trim_start_matches(':').trim_end_matches(':');
1142
1143 if without_colons.is_empty() || !without_colons.chars().all(|c| c == '-') {
1145 return None;
1146 }
1147
1148 let alignment = match (starts_colon, ends_colon) {
1150 (true, true) => Alignment::Center,
1151 (true, false) => Alignment::Left,
1152 (false, true) => Alignment::Right,
1153 (false, false) => Alignment::Default,
1154 };
1155
1156 alignments.push(alignment);
1157 }
1158
1159 if alignments.is_empty() {
1161 None
1162 } else {
1163 Some(alignments)
1164 }
1165}
1166
1167fn parse_pipe_table_row(line: &str) -> Vec<String> {
1170 let trimmed = line.trim();
1171
1172 let mut cells = Vec::new();
1173 let mut current_cell = String::new();
1174 let mut chars = trimmed.chars().peekable();
1175 let mut char_count = 0;
1176
1177 while let Some(ch) = chars.next() {
1178 char_count += 1;
1179 match ch {
1180 '\\' => {
1181 if let Some(&'|') = chars.peek() {
1183 current_cell.push('\\');
1184 current_cell.push('|');
1185 chars.next(); } else {
1187 current_cell.push(ch);
1188 }
1189 }
1190 '|' => {
1191 if char_count == 1 {
1193 continue; }
1195
1196 cells.push(current_cell.trim().to_string());
1198 current_cell.clear();
1199 }
1200 _ => {
1201 current_cell.push(ch);
1202 }
1203 }
1204 }
1205
1206 let trimmed_cell = current_cell.trim().to_string();
1208 if !trimmed_cell.is_empty() {
1209 cells.push(trimmed_cell);
1210 }
1211
1212 cells
1213}
1214
1215fn emit_pipe_table_row(
1218 builder: &mut GreenNodeBuilder<'static>,
1219 window: &StrippedLines<'_, '_>,
1220 abs_idx: usize,
1221 row_kind: SyntaxKind,
1222 config: &ParserOptions,
1223) {
1224 builder.start_node(row_kind.into());
1225
1226 let line = if abs_idx == window.dispatch_pos() {
1234 window.dispatch_tail()
1235 } else {
1236 window.emit_prefix_at(builder, abs_idx)
1237 };
1238
1239 let (line_without_newline, newline_str) = strip_newline(line);
1240 let trimmed = line_without_newline.trim();
1241
1242 let mut cell_starts = Vec::new();
1244 let mut cell_ends = Vec::new();
1245 let mut in_escape = false;
1246
1247 let mut pipe_positions = Vec::new();
1249 for (i, ch) in trimmed.char_indices() {
1250 if in_escape {
1251 in_escape = false;
1252 continue;
1253 }
1254 if ch == '\\' {
1255 in_escape = true;
1256 continue;
1257 }
1258 if ch == '|' {
1259 pipe_positions.push(i);
1260 }
1261 }
1262
1263 if pipe_positions.is_empty() {
1265 cell_starts.push(0);
1267 cell_ends.push(trimmed.len());
1268 } else {
1269 let start_pipe = pipe_positions.first() == Some(&0);
1271 let end_pipe = pipe_positions.last() == Some(&(trimmed.len() - 1));
1273
1274 if start_pipe {
1275 for i in 1..pipe_positions.len() {
1277 cell_starts.push(pipe_positions[i - 1] + 1);
1278 cell_ends.push(pipe_positions[i]);
1279 }
1280 if !end_pipe {
1282 cell_starts.push(*pipe_positions.last().unwrap() + 1);
1283 cell_ends.push(trimmed.len());
1284 }
1285 } else {
1286 cell_starts.push(0);
1288 cell_ends.push(pipe_positions[0]);
1289
1290 for i in 1..pipe_positions.len() {
1291 cell_starts.push(pipe_positions[i - 1] + 1);
1292 cell_ends.push(pipe_positions[i]);
1293 }
1294
1295 if !end_pipe {
1297 cell_starts.push(*pipe_positions.last().unwrap() + 1);
1298 cell_ends.push(trimmed.len());
1299 }
1300 }
1301 }
1302
1303 let leading_ws_len = line_without_newline.len() - line_without_newline.trim_start().len();
1305 if leading_ws_len > 0 {
1306 builder.token(
1307 SyntaxKind::WHITESPACE.into(),
1308 &line_without_newline[..leading_ws_len],
1309 );
1310 }
1311
1312 for (idx, (start, end)) in cell_starts.iter().zip(cell_ends.iter()).enumerate() {
1314 if *start > 0 {
1316 builder.token(SyntaxKind::TEXT.into(), "|");
1317 } else if idx == 0 && trimmed.starts_with('|') {
1318 builder.token(SyntaxKind::TEXT.into(), "|");
1320 }
1321
1322 let cell_with_ws = &trimmed[*start..*end];
1324 let cell_content = cell_with_ws.trim();
1325
1326 let cell_leading_ws = &cell_with_ws[..cell_with_ws.len() - cell_with_ws.trim_start().len()];
1328 if !cell_leading_ws.is_empty() {
1329 builder.token(SyntaxKind::WHITESPACE.into(), cell_leading_ws);
1330 }
1331
1332 emit_table_cell(builder, cell_content, config);
1334
1335 let cell_trailing_ws_start = cell_leading_ws.len() + cell_content.len();
1337 if cell_trailing_ws_start < cell_with_ws.len() {
1338 builder.token(
1339 SyntaxKind::WHITESPACE.into(),
1340 &cell_with_ws[cell_trailing_ws_start..],
1341 );
1342 }
1343 }
1344
1345 if !pipe_positions.is_empty() && trimmed.ends_with('|') {
1347 builder.token(SyntaxKind::TEXT.into(), "|");
1348 }
1349
1350 let trailing_ws_start = leading_ws_len + trimmed.len();
1352 if trailing_ws_start < line_without_newline.len() {
1353 builder.token(
1354 SyntaxKind::WHITESPACE.into(),
1355 &line_without_newline[trailing_ws_start..],
1356 );
1357 }
1358
1359 if !newline_str.is_empty() {
1361 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
1362 }
1363
1364 builder.finish_node();
1365}
1366
1367pub(crate) fn try_parse_pipe_table(
1370 window: &StrippedLines<'_, '_>,
1371 builder: &mut GreenNodeBuilder<'static>,
1372 config: &ParserOptions,
1373) -> Option<usize> {
1374 let lines = window.raw();
1375 let start_pos = window.pos();
1376 if start_pos + 1 >= lines.len() {
1377 return None;
1378 }
1379
1380 if !window.strip_at(start_pos).contains('|') && !is_caption_followed_by_table(window, start_pos)
1388 {
1389 return None;
1390 }
1391
1392 let (actual_start, caption_before) = if is_caption_followed_by_table(window, start_pos) {
1404 let (cap_start, cap_end) = caption_range_starting_at(window, start_pos)?;
1405 let mut pos = cap_end;
1406 while pos < window.line_count() && window.line(pos).trim().is_empty() {
1407 pos += 1;
1408 }
1409 (pos, Some((cap_start, cap_end)))
1410 } else {
1411 (start_pos, None)
1412 };
1413
1414 if actual_start + 1 >= lines.len() {
1415 return None;
1416 }
1417
1418 if !window.line(actual_start).contains('|') {
1420 return None;
1421 }
1422
1423 let alignments = try_parse_pipe_separator(window.line(actual_start + 1))?;
1425
1426 let header_cells = parse_pipe_table_row(window.line(actual_start));
1428
1429 if header_cells.len() != alignments.len() && !header_cells.is_empty() {
1431 if header_cells.len() < alignments.len() / 2 || header_cells.len() > alignments.len() * 2 {
1433 return None;
1434 }
1435 }
1436
1437 let mut end_pos = actual_start + 2;
1439 while end_pos < window.line_count() {
1440 let line = window.line(end_pos);
1441 if line.trim().is_empty() {
1442 break;
1443 }
1444 if !line.contains('|') {
1446 break;
1447 }
1448 end_pos += 1;
1449 }
1450
1451 if end_pos <= actual_start + 2 {
1453 return None;
1454 }
1455
1456 let caption_before = caption_before.or_else(|| find_caption_before_table(window, actual_start));
1458
1459 let caption_after = if caption_before.is_some() {
1461 None
1462 } else {
1463 find_caption_after_table(window, end_pos)
1464 };
1465
1466 builder.start_node(SyntaxKind::PIPE_TABLE.into());
1468
1469 if let Some((cap_start, cap_end)) = caption_before {
1471 emit_table_caption(builder, window, cap_start, cap_end, config);
1472 emit_caption_blank_lines(builder, window, cap_end, actual_start);
1474 }
1475
1476 emit_pipe_table_row(
1481 builder,
1482 window,
1483 actual_start,
1484 SyntaxKind::TABLE_HEADER,
1485 config,
1486 );
1487
1488 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
1491 let sep_idx = actual_start + 1;
1492 let separator_tail = if sep_idx == window.dispatch_pos() {
1493 window.dispatch_tail()
1494 } else {
1495 window.emit_prefix_at(builder, sep_idx)
1496 };
1497 emit_separator_tokens(builder, separator_tail);
1498 builder.finish_node();
1499
1500 for idx in (actual_start + 2)..end_pos {
1502 emit_pipe_table_row(builder, window, idx, SyntaxKind::TABLE_ROW, config);
1503 }
1504
1505 if let Some((cap_start, cap_end)) = caption_after {
1507 emit_caption_blank_lines(builder, window, end_pos, cap_start);
1509 emit_table_caption(builder, window, cap_start, cap_end, config);
1510 }
1511
1512 builder.finish_node(); let table_start = caption_before
1516 .map(|(start, _)| start)
1517 .unwrap_or(actual_start);
1518 let table_end = if let Some((_, cap_end)) = caption_after {
1519 cap_end
1520 } else {
1521 end_pos
1522 };
1523
1524 Some(table_end - table_start)
1525}
1526
1527#[cfg(test)]
1528mod tests {
1529 use super::super::container_prefix::ContainerPrefix;
1530 use super::*;
1531
1532 #[test]
1533 fn test_separator_detection() {
1534 assert!(try_parse_table_separator("------- ------ ---------- -------").is_some());
1535 assert!(try_parse_table_separator(" --- --- ---").is_some());
1536 assert!(try_parse_table_separator("-------").is_none()); assert!(try_parse_table_separator("--- --- ---").is_some()); }
1539
1540 #[test]
1541 fn test_column_extraction() {
1542 let line = "------- ------ ---------- -------";
1543 let columns = extract_columns(line, 0);
1544 assert_eq!(columns.len(), 4);
1545 }
1546
1547 #[test]
1548 fn column_offset_maps_by_display_width() {
1549 let line = "地號xy";
1553 assert_eq!(column_offset_to_byte_index(line, 4), 6);
1554 assert_eq!(column_offset_to_byte_index("abcd", 2), 2);
1556 }
1557
1558 #[test]
1559 fn simple_table_cjk_header_keeps_footnote_ref_intact() {
1560 let input = "\
1565 地號 地主 路段 總長[^d] 水利局舖的[^s]
1566------ ------- -------- --------- --------------
15672976 Ralph 南段 64 33
1568";
1569 let tree = crate::parser::parse(input, None);
1570 let refs: Vec<_> = tree
1571 .descendants()
1572 .filter(|n| n.kind() == SyntaxKind::FOOTNOTE_REFERENCE)
1573 .map(|n| n.text().to_string())
1574 .collect();
1575 assert_eq!(refs, vec!["[^d]".to_string(), "[^s]".to_string()]);
1576 }
1577
1578 #[test]
1579 fn test_simple_table_with_header() {
1580 let input = vec![
1581 " Right Left Center Default",
1582 "------- ------ ---------- -------",
1583 " 12 12 12 12",
1584 " 123 123 123 123",
1585 "",
1586 ];
1587
1588 let mut builder = GreenNodeBuilder::new();
1589 let prefix = ContainerPrefix::default();
1590 let window = StrippedLines::new(&input, 0, &prefix);
1591 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1592
1593 assert!(result.is_some());
1594 assert_eq!(result.unwrap(), 4); }
1596
1597 #[test]
1598 fn test_headerless_table() {
1599 let input = vec![
1600 "------- ------ ---------- -------",
1601 " 12 12 12 12",
1602 " 123 123 123 123",
1603 "",
1604 ];
1605
1606 let mut builder = GreenNodeBuilder::new();
1607 let prefix = ContainerPrefix::default();
1608 let window = StrippedLines::new(&input, 0, &prefix);
1609 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1610
1611 assert!(result.is_some());
1612 assert_eq!(result.unwrap(), 3); }
1614
1615 #[test]
1616 fn single_dash_run_detection() {
1617 assert!(parse_single_dash_run("---").is_some());
1618 assert!(parse_single_dash_run("--").is_some()); assert!(parse_single_dash_run("-").is_none()); assert!(parse_single_dash_run(" ---------- ").is_some());
1621 assert!(parse_single_dash_run(" ---").is_none()); assert!(parse_single_dash_run("--- ---").is_none()); assert!(parse_single_dash_run("---x").is_none());
1624 assert!(parse_single_dash_run("- - -").is_none()); }
1626
1627 #[test]
1628 fn headerless_single_column_simple_table() {
1629 let input = vec!["---", "- a", "- b", "---", ""];
1634
1635 let mut builder = GreenNodeBuilder::new();
1636 let prefix = ContainerPrefix::default();
1637 let window = StrippedLines::new(&input, 0, &prefix);
1638 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1639
1640 assert_eq!(result, Some(4)); }
1642
1643 #[test]
1644 fn headerless_single_column_requires_closer() {
1645 let input = vec!["---", "just prose", "", "after"];
1648
1649 let mut builder = GreenNodeBuilder::new();
1650 let prefix = ContainerPrefix::default();
1651 let window = StrippedLines::new(&input, 0, &prefix);
1652 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1653
1654 assert!(result.is_none());
1655 }
1656
1657 #[test]
1658 fn headerless_single_column_requires_rows() {
1659 let input = vec!["---", "---", ""];
1661
1662 let mut builder = GreenNodeBuilder::new();
1663 let prefix = ContainerPrefix::default();
1664 let window = StrippedLines::new(&input, 0, &prefix);
1665 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1666
1667 assert!(result.is_none());
1668 }
1669
1670 #[test]
1671 fn test_caption_prefix_detection() {
1672 assert!(try_parse_caption_prefix("Table: My caption").is_some());
1673 assert!(try_parse_caption_prefix("table: My caption").is_some());
1674 assert!(try_parse_caption_prefix(": My caption").is_some());
1675 assert!(try_parse_caption_prefix(":").is_none()); assert!(try_parse_caption_prefix("Not a caption").is_none());
1677 }
1678
1679 #[test]
1680 fn table_grid_starts_at_matches_each_kind() {
1681 assert!(table_grid_starts_at(&["+---+---+"][..], 0)); assert!(table_grid_starts_at(&["----------- -------"][..], 0)); assert!(table_grid_starts_at(&["--- --- ---"][..], 0)); assert!(table_grid_starts_at(&["A | B", "| --- | --- |"][..], 0)); assert!(table_grid_starts_at(&["A B", "--- ---"][..], 0)); assert!(table_grid_starts_at(&["-------"][..], 0));
1691
1692 assert!(!table_grid_starts_at(&["just some prose"][..], 0));
1694 assert!(!table_grid_starts_at(&["# Heading"][..], 0));
1695 assert!(!table_grid_starts_at(&["```", "code", "```"][..], 0)); assert!(!table_grid_starts_at(&["only one line"][..], 1)); }
1698
1699 #[test]
1704 fn caption_lookahead_agrees_with_real_parse() {
1705 let with_table = ": Cap\n\n| A | B |\n|---|---|\n| 1 | 2 |\n";
1706 let lines: Vec<&str> = with_table.lines().collect();
1707 assert!(is_caption_followed_by_table(&lines[..], 0));
1708 assert!(format!("{:#?}", crate::parse(with_table, None)).contains("PIPE_TABLE"));
1709
1710 let no_table = ": Cap\n\nplain paragraph\n";
1711 let lines: Vec<&str> = no_table.lines().collect();
1712 assert!(!is_caption_followed_by_table(&lines[..], 0));
1713 assert!(!format!("{:#?}", crate::parse(no_table, None)).contains("TABLE"));
1714 }
1715
1716 #[test]
1723 fn ordered_marker_on_pipe_table_line_is_top_level_table() {
1724 let input = "1. | a | b |\n | - | - |\n | 1 | 2 |\n";
1725 let tree = crate::parse(input, None);
1726 assert!(
1727 tree.descendants()
1728 .any(|n| n.kind() == SyntaxKind::PIPE_TABLE),
1729 "ordered marker + pipe table on the marker line should be a top-level table"
1730 );
1731 assert!(
1732 !tree.descendants().any(|n| n.kind() == SyntaxKind::LIST),
1733 "it must not nest under a list"
1734 );
1735 let dump = format!("{tree:#?}");
1737 assert!(
1738 dump.contains("1."),
1739 "marker text preserved as a header cell"
1740 );
1741 assert!(dump.contains('b'), "overflow cell `b` preserved (lossless)");
1742 }
1743
1744 #[test]
1745 fn lone_ordered_marker_pipe_line_is_a_list() {
1746 let input = "1. | a | b |\n";
1748 let tree = crate::parse(input, None);
1749 assert!(
1750 tree.descendants().any(|n| n.kind() == SyntaxKind::LIST),
1751 "a lone ordered marker line stays a list"
1752 );
1753 assert!(
1754 !tree
1755 .descendants()
1756 .any(|n| n.kind() == SyntaxKind::PIPE_TABLE),
1757 "no table without a delimiter row"
1758 );
1759 }
1760
1761 #[test]
1762 fn bullet_marker_on_pipe_table_line_stays_a_nested_table() {
1763 let input = "- | a | b |\n | - | - |\n | 1 | 2 |\n";
1765 let tree = crate::parse(input, None);
1766 assert!(
1767 tree.descendants().any(|n| n.kind() == SyntaxKind::LIST),
1768 "bullet marker keeps the list"
1769 );
1770 assert!(
1771 tree.descendants()
1772 .any(|n| n.kind() == SyntaxKind::PIPE_TABLE),
1773 "with the table nested inside the list item"
1774 );
1775 }
1776
1777 #[test]
1778 fn bare_colon_fenced_code_is_not_table_caption() {
1779 let input = "Term\n: ```\n code\n ```\n";
1780 let tree = crate::parse(input, None);
1781
1782 assert!(
1783 tree.descendants()
1784 .any(|node| node.kind() == SyntaxKind::DEFINITION_LIST),
1785 "should parse as definition list"
1786 );
1787 assert!(
1788 tree.descendants()
1789 .any(|node| node.kind() == SyntaxKind::CODE_BLOCK),
1790 "definition should preserve fenced code block"
1791 );
1792 assert!(
1793 !tree
1794 .descendants()
1795 .any(|node| node.kind() == SyntaxKind::TABLE_CAPTION),
1796 "fenced code definition should not be parsed as table caption"
1797 );
1798 }
1799
1800 #[test]
1801 fn bare_colon_caption_after_div_opening_is_table_caption() {
1802 let input = "::: {#tbl:panel layout.nrow=\"1\"}\n : My Caption {#tbl:foo-1}\n\n | Col1 | Col2 | Col3 |\n | ---- | ---- | ---- |\n | A | B | C |\n | E | F | G |\n | A | G | G |\n\n : My Caption2 {#tbl:foo-2}\n\n | Col1 | Col2 | Col3 |\n | ---- | ---- | ---- |\n | A | B | C |\n | E | F | G |\n | A | G | G |\n\nCaption\n:::\n";
1803 let tree = crate::parse(input, None);
1804
1805 let caption_count = tree
1806 .descendants()
1807 .filter(|node| node.kind() == SyntaxKind::TABLE_CAPTION)
1808 .count();
1809 assert_eq!(
1810 caption_count, 2,
1811 "expected both captions to attach to tables"
1812 );
1813 assert!(
1814 !tree
1815 .descendants()
1816 .any(|node| node.kind() == SyntaxKind::DEFINITION_LIST),
1817 "caption lines in this fenced div table layout should not parse as definition list"
1818 );
1819 }
1820
1821 #[test]
1822 fn test_table_with_caption_after() {
1823 let input = vec![
1824 " Right Left Center Default",
1825 "------- ------ ---------- -------",
1826 " 12 12 12 12",
1827 " 123 123 123 123",
1828 "",
1829 "Table: Demonstration of simple table syntax.",
1830 "",
1831 ];
1832
1833 let mut builder = GreenNodeBuilder::new();
1834 let prefix = ContainerPrefix::default();
1835 let window = StrippedLines::new(&input, 0, &prefix);
1836 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1837
1838 assert!(result.is_some());
1839 assert_eq!(result.unwrap(), 6);
1841 }
1842
1843 #[test]
1844 fn test_table_with_caption_before() {
1845 let input = vec![
1846 "Table: Demonstration of simple table syntax.",
1847 "",
1848 " Right Left Center Default",
1849 "------- ------ ---------- -------",
1850 " 12 12 12 12",
1851 " 123 123 123 123",
1852 "",
1853 ];
1854
1855 let mut builder = GreenNodeBuilder::new();
1856 let prefix = ContainerPrefix::default();
1857 let window = StrippedLines::new(&input, 2, &prefix);
1858 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1859
1860 assert!(result.is_some());
1861 assert_eq!(result.unwrap(), 6);
1863 }
1864
1865 #[test]
1866 fn test_caption_with_colon_prefix() {
1867 let input = vec![
1868 " Right Left",
1869 "------- ------",
1870 " 12 12",
1871 "",
1872 ": Short caption",
1873 "",
1874 ];
1875
1876 let mut builder = GreenNodeBuilder::new();
1877 let prefix = ContainerPrefix::default();
1878 let window = StrippedLines::new(&input, 0, &prefix);
1879 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1880
1881 assert!(result.is_some());
1882 assert_eq!(result.unwrap(), 5); }
1884
1885 #[test]
1886 fn test_multiline_caption() {
1887 let input = vec![
1888 " Right Left",
1889 "------- ------",
1890 " 12 12",
1891 "",
1892 "Table: This is a longer caption",
1893 "that spans multiple lines.",
1894 "",
1895 ];
1896
1897 let mut builder = GreenNodeBuilder::new();
1898 let prefix = ContainerPrefix::default();
1899 let window = StrippedLines::new(&input, 0, &prefix);
1900 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1901
1902 assert!(result.is_some());
1903 assert_eq!(result.unwrap(), 6);
1905 }
1906
1907 #[test]
1908 fn test_simple_table_with_multibyte_cell_content() {
1909 let input = vec![
1910 "Name Hex code Hue C, M, Y, K (%) R, G, B (0-255) R, G, B (%)",
1911 "-------------- ------------ ------- ---------------- ----------------- ------------",
1912 " orange #E69F00 41° 0, 50, 100, 0 230, 159, 0 90, 60, 0",
1913 " sky blue #56B4E9 202° 80, 0, 0, 0 86, 180, 233 35, 70, 90",
1914 "",
1915 ];
1916
1917 let mut builder = GreenNodeBuilder::new();
1918 let prefix = ContainerPrefix::default();
1919 let window = StrippedLines::new(&input, 0, &prefix);
1920 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1921
1922 assert!(result.is_some());
1923 assert_eq!(result.unwrap(), 4);
1924 }
1925
1926 #[test]
1928 fn test_pipe_separator_detection() {
1929 assert!(try_parse_pipe_separator("|------:|:-----|---------|:------:|").is_some());
1930 assert!(try_parse_pipe_separator("|---|---|").is_some());
1931 assert!(try_parse_pipe_separator("-----|-----:").is_some()); assert!(try_parse_pipe_separator("|-----+-------|").is_some()); assert!(try_parse_pipe_separator("not a separator").is_none());
1934 }
1935
1936 #[test]
1937 fn test_pipe_alignments() {
1938 let aligns = try_parse_pipe_separator("|------:|:-----|---------|:------:|").unwrap();
1939 assert_eq!(aligns.len(), 4);
1940 assert_eq!(aligns[0], Alignment::Right);
1941 assert_eq!(aligns[1], Alignment::Left);
1942 assert_eq!(aligns[2], Alignment::Default);
1943 assert_eq!(aligns[3], Alignment::Center);
1944 }
1945
1946 #[test]
1947 fn test_parse_pipe_table_row() {
1948 let cells = parse_pipe_table_row("| Right | Left | Center |");
1949 assert_eq!(cells.len(), 3);
1950 assert_eq!(cells[0], "Right");
1951 assert_eq!(cells[1], "Left");
1952 assert_eq!(cells[2], "Center");
1953
1954 let cells2 = parse_pipe_table_row("Right | Left | Center");
1956 assert_eq!(cells2.len(), 3);
1957 }
1958
1959 #[test]
1960 fn test_basic_pipe_table() {
1961 let input = vec![
1962 "",
1963 "| Right | Left | Center |",
1964 "|------:|:-----|:------:|",
1965 "| 12 | 12 | 12 |",
1966 "| 123 | 123 | 123 |",
1967 "",
1968 ];
1969
1970 let mut builder = GreenNodeBuilder::new();
1971 let prefix = ContainerPrefix::default();
1972 let window = StrippedLines::new(&input, 1, &prefix);
1973 let result = try_parse_pipe_table(&window, &mut builder, &ParserOptions::default());
1974
1975 assert!(result.is_some());
1976 assert_eq!(result.unwrap(), 4); }
1978
1979 #[test]
1980 fn test_pipe_table_no_edge_pipes() {
1981 let input = vec![
1982 "",
1983 "fruit| price",
1984 "-----|-----:",
1985 "apple|2.05",
1986 "pear|1.37",
1987 "",
1988 ];
1989
1990 let mut builder = GreenNodeBuilder::new();
1991 let prefix = ContainerPrefix::default();
1992 let window = StrippedLines::new(&input, 1, &prefix);
1993 let result = try_parse_pipe_table(&window, &mut builder, &ParserOptions::default());
1994
1995 assert!(result.is_some());
1996 assert_eq!(result.unwrap(), 4);
1997 }
1998
1999 #[test]
2000 fn test_pipe_table_with_caption() {
2001 let input = vec![
2002 "",
2003 "| Col1 | Col2 |",
2004 "|------|------|",
2005 "| A | B |",
2006 "",
2007 "Table: My pipe table",
2008 "",
2009 ];
2010
2011 let mut builder = GreenNodeBuilder::new();
2012 let prefix = ContainerPrefix::default();
2013 let window = StrippedLines::new(&input, 1, &prefix);
2014 let result = try_parse_pipe_table(&window, &mut builder, &ParserOptions::default());
2015
2016 assert!(result.is_some());
2017 assert_eq!(result.unwrap(), 5); }
2019
2020 #[test]
2021 fn test_pipe_table_with_multiline_caption_before() {
2022 let input = vec![
2023 ": (#tab:base) base R quoting",
2024 "functions",
2025 "",
2026 "| C | D |",
2027 "|---|---|",
2028 "| 3 | 4 |",
2029 "",
2030 ];
2031
2032 let mut builder = GreenNodeBuilder::new();
2033 let prefix = ContainerPrefix::default();
2034 let window = StrippedLines::new(&input, 0, &prefix);
2035 let result = try_parse_pipe_table(&window, &mut builder, &ParserOptions::default());
2036
2037 assert!(result.is_some());
2038 assert_eq!(result.unwrap(), 6);
2040 }
2041}
2042
2043fn try_parse_grid_separator(line: &str) -> Option<Vec<GridColumn>> {
2050 let trimmed = line.trim_start();
2051 let leading_spaces = line.len() - trimmed.len();
2052
2053 if leading_spaces > 0 {
2058 return None;
2059 }
2060
2061 if !trimmed.starts_with('+') || !trimmed.trim_end().ends_with('+') {
2063 return None;
2064 }
2065
2066 let trimmed = trimmed.trim_end();
2068 let segments: Vec<&str> = trimmed.split('+').collect();
2069
2070 if segments.len() < 3 {
2072 return None;
2073 }
2074
2075 let mut columns = Vec::new();
2076
2077 for segment in segments.iter().skip(1).take(segments.len() - 2) {
2079 if segment.is_empty() {
2080 continue;
2081 }
2082
2083 let seg_trimmed = *segment;
2085
2086 let inner = seg_trimmed.trim_start_matches(':').trim_end_matches(':');
2088
2089 if inner.is_empty() {
2091 return None;
2092 }
2093
2094 let first_char = inner.chars().next().unwrap();
2095 if first_char != '-' && first_char != '=' {
2096 return None;
2097 }
2098
2099 if !inner.chars().all(|c| c == first_char) {
2100 return None;
2101 }
2102
2103 let is_header_sep = first_char == '=';
2104
2105 columns.push(GridColumn {
2106 is_header_separator: is_header_sep,
2107 width: seg_trimmed.chars().count(),
2108 });
2109 }
2110
2111 if columns.is_empty() {
2112 None
2113 } else {
2114 Some(columns)
2115 }
2116}
2117
2118#[derive(Debug, Clone)]
2120struct GridColumn {
2121 is_header_separator: bool,
2122 width: usize,
2123}
2124
2125fn slice_cell_by_display_width(line: &str, start_byte: usize, width: usize) -> (usize, usize) {
2126 let mut end_byte = start_byte;
2127 let mut display_cols = 0usize;
2128
2129 for (offset, ch) in line[start_byte..].char_indices() {
2130 if ch == '|' {
2131 let sep_byte = start_byte + offset;
2132 return (sep_byte, sep_byte + 1);
2133 }
2134 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0);
2135 if display_cols + ch_width > width {
2136 break;
2137 }
2138 display_cols += ch_width;
2139 end_byte = start_byte + offset + ch.len_utf8();
2140 if display_cols >= width {
2141 break;
2142 }
2143 }
2144
2145 let mut sep_byte = end_byte;
2149 while sep_byte < line.len() {
2150 let mut chars = line[sep_byte..].chars();
2151 let Some(ch) = chars.next() else {
2152 break;
2153 };
2154 if ch == '|' {
2155 return (sep_byte, sep_byte + 1);
2156 }
2157 sep_byte += ch.len_utf8();
2158 }
2159
2160 (end_byte, end_byte)
2161}
2162
2163fn is_grid_content_row(line: &str) -> bool {
2166 let trimmed = line.trim_start();
2167 let leading_spaces = line.len() - trimmed.len();
2168
2169 if leading_spaces > 3 {
2170 return false;
2171 }
2172
2173 let trimmed = trimmed.trim_end();
2174 trimmed.starts_with('|') && (trimmed.ends_with('|') || trimmed.ends_with('+'))
2175}
2176
2177fn extract_grid_cells_from_line(line: &str, _columns: &[GridColumn]) -> Vec<String> {
2181 let (line_content, _) = strip_newline(line);
2182 let line_trimmed = line_content.trim();
2183
2184 if !line_trimmed.starts_with('|') || !line_trimmed.ends_with('|') {
2185 return vec![String::new(); _columns.len()];
2186 }
2187
2188 let mut cells = Vec::with_capacity(_columns.len());
2189 let mut pos_byte = 1; for col in _columns {
2192 let col_idx = cells.len();
2193 if pos_byte >= line_trimmed.len() {
2194 cells.push(String::new());
2195 continue;
2196 }
2197
2198 let start_byte = pos_byte;
2199 let end_byte = if col_idx + 1 == _columns.len() {
2200 line_trimmed.len().saturating_sub(1) } else {
2202 let (end, next_start) = slice_cell_by_display_width(line_trimmed, pos_byte, col.width);
2203 pos_byte = next_start;
2204 end
2205 };
2206 cells.push(line_trimmed[start_byte..end_byte].trim().to_string());
2207 if col_idx + 1 == _columns.len() {
2208 pos_byte = line_trimmed.len();
2209 }
2210 }
2211
2212 cells
2213}
2214
2215fn emit_grid_table_row(
2219 builder: &mut GreenNodeBuilder<'static>,
2220 window: &StrippedLines<'_, '_>,
2221 indices: &[usize],
2222 columns: &[GridColumn],
2223 row_kind: SyntaxKind,
2224 config: &ParserOptions,
2225) {
2226 if indices.is_empty() {
2227 return;
2228 }
2229
2230 builder.start_node(row_kind.into());
2231
2232 let first_line = window.emit_or_dispatch_tail(builder, indices[0]);
2238 let cell_contents = extract_grid_cells_from_line(first_line, columns);
2239 let (line_without_newline, newline_str) = strip_newline(first_line);
2240 let trimmed = line_without_newline.trim();
2241 let expected_pipe_count = columns.len().saturating_add(1);
2242 let actual_pipe_count = trimmed.chars().filter(|&c| c == '|').count();
2243
2244 if actual_pipe_count != expected_pipe_count {
2248 emit_line_tokens(builder, first_line);
2249 for &idx in &indices[1..] {
2250 let tail = window.emit_or_dispatch_tail(builder, idx);
2251 emit_line_tokens(builder, tail);
2252 }
2253 builder.finish_node();
2254 return;
2255 }
2256
2257 let leading_ws_len = line_without_newline.len() - line_without_newline.trim_start().len();
2259 if leading_ws_len > 0 {
2260 builder.token(
2261 SyntaxKind::WHITESPACE.into(),
2262 &line_without_newline[..leading_ws_len],
2263 );
2264 }
2265
2266 if trimmed.starts_with('|') {
2268 builder.token(SyntaxKind::TEXT.into(), "|");
2269 }
2270
2271 let mut pos_byte = 1usize; for (idx, cell_content) in cell_contents.iter().enumerate() {
2274 let part = if idx < columns.len() && pos_byte <= trimmed.len() {
2275 let start_byte = pos_byte;
2276 let end_byte = if idx + 1 == columns.len() && !trimmed.is_empty() {
2277 trimmed.len().saturating_sub(1) } else {
2279 let (end, next_start) =
2280 slice_cell_by_display_width(trimmed, pos_byte, columns[idx].width);
2281 pos_byte = next_start;
2282 end
2283 };
2284 let slice = &trimmed[start_byte..end_byte];
2285 if idx + 1 == columns.len() {
2286 pos_byte = trimmed.len();
2287 }
2288 slice
2289 } else {
2290 ""
2291 };
2292
2293 let cell_trimmed = part.trim();
2295 let ws_start_len = part.len() - part.trim_start().len();
2296 if ws_start_len > 0 {
2297 builder.token(SyntaxKind::WHITESPACE.into(), &part[..ws_start_len]);
2298 }
2299
2300 emit_table_cell(builder, cell_content, config);
2302
2303 let ws_end_start = ws_start_len + cell_trimmed.len();
2305 if ws_end_start < part.len() {
2306 builder.token(SyntaxKind::WHITESPACE.into(), &part[ws_end_start..]);
2307 }
2308
2309 if idx < cell_contents.len() - 1 || trimmed.ends_with('|') {
2311 builder.token(SyntaxKind::TEXT.into(), "|");
2312 }
2313 }
2314
2315 let trailing_ws_start = leading_ws_len + trimmed.len();
2317 if trailing_ws_start < line_without_newline.len() {
2318 builder.token(
2319 SyntaxKind::WHITESPACE.into(),
2320 &line_without_newline[trailing_ws_start..],
2321 );
2322 }
2323
2324 if !newline_str.is_empty() {
2326 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
2327 }
2328
2329 for &idx in &indices[1..] {
2332 let tail = window.emit_or_dispatch_tail(builder, idx);
2333 emit_line_tokens(builder, tail);
2334 }
2335
2336 builder.finish_node();
2337}
2338
2339pub(crate) fn try_parse_grid_table(
2342 window: &StrippedLines<'_, '_>,
2343 builder: &mut GreenNodeBuilder<'static>,
2344 config: &ParserOptions,
2345) -> Option<usize> {
2346 let lines = window.raw();
2347 let start_pos = window.pos();
2348 if start_pos >= lines.len() {
2349 return None;
2350 }
2351
2352 let view = UniformStripView(window);
2361
2362 if try_parse_grid_separator(view.line(start_pos)).is_none()
2368 && !is_caption_followed_by_table(&view, start_pos)
2369 {
2370 return None;
2371 }
2372
2373 let (actual_start, caption_before) = if is_caption_followed_by_table(&view, start_pos) {
2376 let (cap_start, cap_end) = caption_range_starting_at(&view, start_pos)?;
2377 let mut pos = cap_end;
2378 while pos < view.line_count() && view.line(pos).trim().is_empty() {
2379 pos += 1;
2380 }
2381 (pos, Some((cap_start, cap_end)))
2382 } else {
2383 (start_pos, None)
2384 };
2385
2386 if actual_start >= lines.len() {
2387 return None;
2388 }
2389
2390 let first_line = view.line(actual_start);
2392 let _columns = try_parse_grid_separator(first_line)?;
2393
2394 let mut end_pos = actual_start + 1;
2396 let mut found_header_sep = false;
2397 let mut in_footer = false;
2398
2399 while end_pos < lines.len() {
2401 let line = view.line(end_pos);
2402
2403 if line.trim().is_empty() {
2405 break;
2406 }
2407
2408 if let Some(sep_cols) = try_parse_grid_separator(line) {
2410 if sep_cols.iter().any(|c| c.is_header_separator) {
2412 if !found_header_sep {
2413 found_header_sep = true;
2414 } else if !in_footer {
2415 in_footer = true;
2417 }
2418 }
2419 end_pos += 1;
2420 continue;
2421 }
2422
2423 if is_grid_content_row(line) {
2425 end_pos += 1;
2426 continue;
2427 }
2428
2429 break;
2431 }
2432
2433 if end_pos <= actual_start + 1 {
2436 return None;
2437 }
2438
2439 let caption_before = caption_before.or_else(|| find_caption_before_table(&view, actual_start));
2444
2445 let caption_after = if caption_before.is_some() {
2447 None
2448 } else {
2449 find_caption_after_table(&view, end_pos)
2450 };
2451
2452 builder.start_node(SyntaxKind::GRID_TABLE.into());
2454
2455 if let Some((cap_start, cap_end)) = caption_before {
2457 emit_table_caption(builder, window, cap_start, cap_end, config);
2458 emit_caption_blank_lines(builder, window, cap_end, actual_start);
2460 }
2461
2462 let mut past_header_sep = false;
2464 let mut in_footer_section = false;
2465 let mut current_row_indices: Vec<usize> = Vec::new();
2468 let mut current_row_kind = SyntaxKind::TABLE_HEADER;
2469
2470 for idx in actual_start..end_pos {
2472 let line = view.line(idx);
2473 if let Some(sep_cols) = try_parse_grid_separator(line) {
2474 if !current_row_indices.is_empty() {
2476 emit_grid_table_row(
2477 builder,
2478 window,
2479 ¤t_row_indices,
2480 &sep_cols,
2481 current_row_kind,
2482 config,
2483 );
2484 current_row_indices.clear();
2485 }
2486
2487 let is_header_sep = sep_cols.iter().any(|c| c.is_header_separator);
2488
2489 if is_header_sep {
2492 if !past_header_sep {
2493 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
2495 let tail = window.emit_or_dispatch_tail(builder, idx);
2496 emit_separator_tokens(builder, tail);
2497 builder.finish_node();
2498 past_header_sep = true;
2499 } else {
2500 if !in_footer_section {
2502 in_footer_section = true;
2503 }
2504 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
2505 let tail = window.emit_or_dispatch_tail(builder, idx);
2506 emit_separator_tokens(builder, tail);
2507 builder.finish_node();
2508 }
2509 } else {
2510 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
2512 let tail = window.emit_or_dispatch_tail(builder, idx);
2513 emit_separator_tokens(builder, tail);
2514 builder.finish_node();
2515 }
2516 } else if is_grid_content_row(line) {
2517 current_row_kind = if !past_header_sep && found_header_sep {
2519 SyntaxKind::TABLE_HEADER
2520 } else if in_footer_section {
2521 SyntaxKind::TABLE_FOOTER
2522 } else {
2523 SyntaxKind::TABLE_ROW
2524 };
2525
2526 current_row_indices.push(idx);
2527 }
2528 }
2529
2530 if !current_row_indices.is_empty() {
2532 if let Some(sep_cols) = try_parse_grid_separator(view.line(actual_start)) {
2534 emit_grid_table_row(
2535 builder,
2536 window,
2537 ¤t_row_indices,
2538 &sep_cols,
2539 current_row_kind,
2540 config,
2541 );
2542 }
2543 }
2544
2545 if let Some((cap_start, cap_end)) = caption_after {
2547 emit_caption_blank_lines(builder, window, end_pos, cap_start);
2548 emit_table_caption(builder, window, cap_start, cap_end, config);
2549 }
2550
2551 builder.finish_node(); let table_start = caption_before
2555 .map(|(start, _)| start)
2556 .unwrap_or(actual_start);
2557 let table_end = if let Some((_, cap_end)) = caption_after {
2558 cap_end
2559 } else {
2560 end_pos
2561 };
2562
2563 Some(table_end - table_start)
2564}
2565
2566#[cfg(test)]
2567mod grid_table_tests {
2568 use super::super::container_prefix::ContainerPrefix;
2569 use super::*;
2570
2571 #[test]
2572 fn test_grid_separator_detection() {
2573 assert!(try_parse_grid_separator("+---+---+").is_some());
2574 assert!(try_parse_grid_separator("+===+===+").is_some());
2575 assert!(try_parse_grid_separator("+---------------+---------------+").is_some());
2576 assert!(try_parse_grid_separator("+:---:+").is_some()); assert!(try_parse_grid_separator("not a separator").is_none());
2578 assert!(try_parse_grid_separator("|---|---|").is_none()); assert!(try_parse_grid_separator(" +---+---+").is_none());
2585 assert!(try_parse_grid_separator(" +---+---+").is_none());
2586 assert!(try_parse_grid_separator(" +===+===+").is_none());
2587 }
2588
2589 #[test]
2590 fn test_grid_header_separator() {
2591 let cols = try_parse_grid_separator("+===+===+").unwrap();
2592 assert!(cols.iter().all(|c| c.is_header_separator));
2593
2594 let cols2 = try_parse_grid_separator("+---+---+").unwrap();
2595 assert!(cols2.iter().all(|c| !c.is_header_separator));
2596 }
2597
2598 #[test]
2599 fn test_grid_content_row_detection() {
2600 assert!(is_grid_content_row("| content | content |"));
2601 assert!(is_grid_content_row("| | |"));
2602 assert!(is_grid_content_row("| content +------+"));
2603 assert!(!is_grid_content_row("+---+---+")); assert!(!is_grid_content_row("no pipes here"));
2605 }
2606
2607 #[test]
2608 fn test_basic_grid_table() {
2609 let input = vec![
2610 "+-------+-------+",
2611 "| Col1 | Col2 |",
2612 "+=======+=======+",
2613 "| A | B |",
2614 "+-------+-------+",
2615 "",
2616 ];
2617
2618 let mut builder = GreenNodeBuilder::new();
2619 let prefix = ContainerPrefix::default();
2620 let window = StrippedLines::new(&input, 0, &prefix);
2621 let result = try_parse_grid_table(&window, &mut builder, &ParserOptions::default());
2622
2623 assert!(result.is_some());
2624 assert_eq!(result.unwrap(), 5);
2625 }
2626
2627 #[test]
2628 fn test_grid_table_multirow() {
2629 let input = vec![
2630 "+---------------+---------------+",
2631 "| Fruit | Advantages |",
2632 "+===============+===============+",
2633 "| Bananas | - wrapper |",
2634 "| | - color |",
2635 "+---------------+---------------+",
2636 "| Oranges | - scurvy |",
2637 "| | - tasty |",
2638 "+---------------+---------------+",
2639 "",
2640 ];
2641
2642 let mut builder = GreenNodeBuilder::new();
2643 let prefix = ContainerPrefix::default();
2644 let window = StrippedLines::new(&input, 0, &prefix);
2645 let result = try_parse_grid_table(&window, &mut builder, &ParserOptions::default());
2646
2647 assert!(result.is_some());
2648 assert_eq!(result.unwrap(), 9);
2649 }
2650
2651 #[test]
2652 fn test_grid_table_with_footer() {
2653 let input = vec![
2654 "+-------+-------+",
2655 "| Fruit | Price |",
2656 "+=======+=======+",
2657 "| Apple | $1.00 |",
2658 "+-------+-------+",
2659 "| Pear | $1.50 |",
2660 "+=======+=======+",
2661 "| Total | $2.50 |",
2662 "+=======+=======+",
2663 "",
2664 ];
2665
2666 let mut builder = GreenNodeBuilder::new();
2667 let prefix = ContainerPrefix::default();
2668 let window = StrippedLines::new(&input, 0, &prefix);
2669 let result = try_parse_grid_table(&window, &mut builder, &ParserOptions::default());
2670
2671 assert!(result.is_some());
2672 assert_eq!(result.unwrap(), 9);
2673 }
2674
2675 #[test]
2676 fn test_grid_table_headerless() {
2677 let input = vec![
2678 "+-------+-------+",
2679 "| A | B |",
2680 "+-------+-------+",
2681 "| C | D |",
2682 "+-------+-------+",
2683 "",
2684 ];
2685
2686 let mut builder = GreenNodeBuilder::new();
2687 let prefix = ContainerPrefix::default();
2688 let window = StrippedLines::new(&input, 0, &prefix);
2689 let result = try_parse_grid_table(&window, &mut builder, &ParserOptions::default());
2690
2691 assert!(result.is_some());
2692 assert_eq!(result.unwrap(), 5);
2693 }
2694
2695 #[test]
2696 fn test_grid_table_with_caption_before() {
2697 let input = vec![
2698 ": Sample table",
2699 "",
2700 "+-------+-------+",
2701 "| A | B |",
2702 "+=======+=======+",
2703 "| C | D |",
2704 "+-------+-------+",
2705 "",
2706 ];
2707
2708 let mut builder = GreenNodeBuilder::new();
2709 let prefix = ContainerPrefix::default();
2710 let window = StrippedLines::new(&input, 2, &prefix);
2711 let result = try_parse_grid_table(&window, &mut builder, &ParserOptions::default());
2712
2713 assert!(result.is_some());
2714 assert_eq!(result.unwrap(), 7);
2716 }
2717
2718 #[test]
2719 fn test_grid_table_with_caption_after() {
2720 let input = vec![
2721 "+-------+-------+",
2722 "| A | B |",
2723 "+=======+=======+",
2724 "| C | D |",
2725 "+-------+-------+",
2726 "",
2727 "Table: My grid table",
2728 "",
2729 ];
2730
2731 let mut builder = GreenNodeBuilder::new();
2732 let prefix = ContainerPrefix::default();
2733 let window = StrippedLines::new(&input, 0, &prefix);
2734 let result = try_parse_grid_table(&window, &mut builder, &ParserOptions::default());
2735
2736 assert!(result.is_some());
2737 assert_eq!(result.unwrap(), 7);
2739 }
2740}
2741
2742fn try_parse_multiline_separator(line: &str) -> Option<Vec<Column>> {
2750 let trimmed = line.trim_start();
2751 let leading_spaces = line.len() - trimmed.len();
2752
2753 if leading_spaces > 3 {
2755 return None;
2756 }
2757
2758 let trimmed = trimmed.trim_end();
2759
2760 if trimmed.is_empty() || !trimmed.chars().all(|c| c == '-') {
2762 return None;
2763 }
2764
2765 if trimmed.len() < 3 {
2767 return None;
2768 }
2769
2770 Some(vec![Column {
2772 start: leading_spaces,
2773 end: leading_spaces + trimmed.len(),
2774 alignment: Alignment::Default,
2775 }])
2776}
2777
2778fn is_column_separator(line: &str) -> bool {
2781 try_parse_table_separator(line).is_some() && !line.contains('*') && !line.contains('_')
2782}
2783
2784fn is_headerless_single_row_without_blank(
2785 lines: &(impl LineView + ?Sized),
2786 row_start: usize,
2787 row_end: usize,
2788 columns: &[Column],
2789) -> bool {
2790 if row_start >= row_end {
2791 return false;
2792 }
2793
2794 if row_end - row_start == 1 {
2795 return false;
2796 }
2797
2798 let Some(last_col) = columns.last() else {
2799 return false;
2800 };
2801
2802 for i in (row_start + 1)..row_end {
2803 let (content, _) = strip_newline(lines.line(i));
2804 let prefix_end = last_col.start.min(content.len());
2805 if !content[..prefix_end].trim().is_empty() {
2806 return false;
2807 }
2808 }
2809
2810 true
2811}
2812
2813pub(crate) fn try_parse_multiline_table(
2816 window: &StrippedLines<'_, '_>,
2817 builder: &mut GreenNodeBuilder<'static>,
2818 config: &ParserOptions,
2819) -> Option<usize> {
2820 let lines = window.raw();
2821 let start_pos = window.pos();
2822 if start_pos >= lines.len() {
2823 return None;
2824 }
2825
2826 let first_line = window.strip_at(start_pos);
2832
2833 let is_full_width_start = try_parse_multiline_separator(first_line).is_some();
2837 let is_column_sep_start = !is_full_width_start && is_column_separator(first_line);
2838 if !is_full_width_start && !is_column_sep_start {
2839 return None;
2840 }
2841
2842 let headerless_columns = if is_column_sep_start {
2851 try_parse_table_separator(window.line(start_pos))
2852 } else {
2853 None
2854 };
2855
2856 let opening_has_wide_column = headerless_columns
2863 .as_deref()
2864 .is_some_and(|cols| cols.iter().any(|col| col.end - col.start >= 2));
2865
2866 let mut pos = start_pos + 1;
2868 let mut found_column_sep = is_column_sep_start; let mut column_sep_pos = if is_column_sep_start { start_pos } else { 0 };
2870 let mut has_header = false;
2871 let mut found_blank_line = false;
2872 let mut found_closing_sep = false;
2873 let mut content_line_count = 0usize;
2874
2875 while pos < lines.len() {
2877 let line = window.line(pos);
2878
2879 if is_full_width_start && is_column_separator(line) && !found_column_sep {
2881 found_column_sep = true;
2882 column_sep_pos = pos;
2883 has_header = pos > start_pos + 1; pos += 1;
2885 continue;
2886 }
2887
2888 if line.trim().is_empty() {
2890 found_blank_line = true;
2891 pos += 1;
2892 if pos < lines.len() {
2894 let next = window.line(pos);
2895 let is_valid_closer = if is_full_width_start {
2896 try_parse_multiline_separator(next).is_some()
2897 } else {
2898 is_column_separator(next)
2906 || (opening_has_wide_column
2907 && try_parse_multiline_separator(next).is_some())
2908 };
2909 if is_valid_closer {
2910 found_closing_sep = true;
2911 pos += 1; break;
2913 }
2914 }
2915 continue;
2916 }
2917
2918 if is_full_width_start && try_parse_multiline_separator(line).is_some() {
2920 found_closing_sep = true;
2921 pos += 1;
2922 break;
2923 }
2924
2925 if is_column_sep_start && is_column_separator(line) && content_line_count > 0 {
2927 found_closing_sep = true;
2928 pos += 1;
2929 break;
2930 }
2931
2932 content_line_count += 1;
2934 pos += 1;
2935 }
2936
2937 if !found_column_sep {
2939 return None;
2940 }
2941
2942 if !found_blank_line && is_column_sep_start {
2951 let columns = headerless_columns.as_deref()?;
2952 if !is_headerless_single_row_without_blank(window, start_pos + 1, pos - 1, columns) {
2953 return None;
2954 }
2955 }
2956
2957 if !found_closing_sep {
2959 return None;
2960 }
2961
2962 if pos <= start_pos + 2 {
2964 return None;
2965 }
2966
2967 let end_pos = pos;
2968
2969 let columns = try_parse_table_separator(window.line(column_sep_pos))
2971 .expect("Column separator must be valid");
2972
2973 let caption_before = find_caption_before_table(window, start_pos);
2975
2976 let caption_after = if caption_before.is_some() {
2978 None
2979 } else {
2980 find_caption_after_table(window, end_pos)
2981 };
2982
2983 builder.start_node(SyntaxKind::MULTILINE_TABLE.into());
2985
2986 if let Some((cap_start, cap_end)) = caption_before {
2988 emit_table_caption(builder, window, cap_start, cap_end, config);
2989 emit_caption_blank_lines(builder, window, cap_end, start_pos);
2991 }
2992
2993 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
2997 let tail = window.emit_or_dispatch_tail(builder, start_pos);
2998 emit_separator_tokens(builder, tail);
2999 builder.finish_node();
3000
3001 let mut in_header = has_header;
3005 let mut current_row_indices: Vec<usize> = Vec::new();
3006
3007 for i in (start_pos + 1)..end_pos {
3008 let line = window.line(i);
3009 if i == column_sep_pos {
3011 if !current_row_indices.is_empty() {
3013 emit_multiline_table_row(
3014 builder,
3015 window,
3016 ¤t_row_indices,
3017 &columns,
3018 SyntaxKind::TABLE_HEADER,
3019 config,
3020 );
3021 current_row_indices.clear();
3022 }
3023
3024 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
3025 let tail = window.emit_or_dispatch_tail(builder, i);
3026 emit_separator_tokens(builder, tail);
3027 builder.finish_node();
3028 in_header = false;
3029 continue;
3030 }
3031
3032 if try_parse_multiline_separator(line).is_some() || is_column_separator(line) {
3034 if !current_row_indices.is_empty() {
3036 let kind = if in_header {
3037 SyntaxKind::TABLE_HEADER
3038 } else {
3039 SyntaxKind::TABLE_ROW
3040 };
3041 emit_multiline_table_row(
3042 builder,
3043 window,
3044 ¤t_row_indices,
3045 &columns,
3046 kind,
3047 config,
3048 );
3049 current_row_indices.clear();
3050 }
3051
3052 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
3053 let tail = window.emit_or_dispatch_tail(builder, i);
3054 emit_separator_tokens(builder, tail);
3055 builder.finish_node();
3056 continue;
3057 }
3058
3059 if line.trim().is_empty() {
3061 if !current_row_indices.is_empty() {
3063 let kind = if in_header {
3064 SyntaxKind::TABLE_HEADER
3065 } else {
3066 SyntaxKind::TABLE_ROW
3067 };
3068 emit_multiline_table_row(
3069 builder,
3070 window,
3071 ¤t_row_indices,
3072 &columns,
3073 kind,
3074 config,
3075 );
3076 current_row_indices.clear();
3077 }
3078
3079 builder.start_node(SyntaxKind::BLANK_LINE.into());
3082 let tail = window.emit_or_dispatch_tail(builder, i);
3083 builder.token(SyntaxKind::BLANK_LINE.into(), tail);
3084 builder.finish_node();
3085 continue;
3086 }
3087
3088 current_row_indices.push(i);
3090 }
3091
3092 if !current_row_indices.is_empty() {
3094 let kind = if in_header {
3095 SyntaxKind::TABLE_HEADER
3096 } else {
3097 SyntaxKind::TABLE_ROW
3098 };
3099 emit_multiline_table_row(
3100 builder,
3101 window,
3102 ¤t_row_indices,
3103 &columns,
3104 kind,
3105 config,
3106 );
3107 }
3108
3109 if let Some((cap_start, cap_end)) = caption_after {
3111 emit_caption_blank_lines(builder, window, end_pos, cap_start);
3112 emit_table_caption(builder, window, cap_start, cap_end, config);
3113 }
3114
3115 builder.finish_node(); let table_start = caption_before.map(|(start, _)| start).unwrap_or(start_pos);
3119 let table_end = if let Some((_, cap_end)) = caption_after {
3120 cap_end
3121 } else {
3122 end_pos
3123 };
3124
3125 Some(table_end - table_start)
3126}
3127
3128fn extract_first_line_cell_contents(line: &str, columns: &[Column]) -> Vec<String> {
3131 let (line_content, _) = strip_newline(line);
3132 let mut cells = Vec::new();
3133
3134 for column in columns.iter() {
3135 let column_start = column_offset_to_byte_index(line_content, column.start);
3136 let column_end = column_offset_to_byte_index(line_content, column.end);
3137
3138 let cell_text = if column_start < column_end {
3140 &line_content[column_start..column_end]
3141 } else if column_start < line_content.len() {
3142 &line_content[column_start..]
3143 } else {
3144 ""
3145 };
3146
3147 cells.push(cell_text.to_string());
3148 }
3149
3150 cells
3151}
3152
3153fn emit_multiline_table_row(
3160 builder: &mut GreenNodeBuilder<'static>,
3161 window: &StrippedLines<'_, '_>,
3162 indices: &[usize],
3163 columns: &[Column],
3164 kind: SyntaxKind,
3165 config: &ParserOptions,
3166) {
3167 if indices.is_empty() {
3168 return;
3169 }
3170
3171 builder.start_node(kind.into());
3172
3173 let first_line = window.emit_or_dispatch_tail(builder, indices[0]);
3177 let cell_contents = extract_first_line_cell_contents(first_line, columns);
3178 let (trimmed, newline_str) = strip_newline(first_line);
3179 let mut current_pos = 0;
3180
3181 for (col_idx, column) in columns.iter().enumerate() {
3182 let cell_text = &cell_contents[col_idx];
3183 let cell_start = column_offset_to_byte_index(trimmed, column.start);
3184 let cell_end = column_offset_to_byte_index(trimmed, column.end);
3185
3186 if current_pos < cell_start {
3188 builder.token(
3189 SyntaxKind::WHITESPACE.into(),
3190 &trimmed[current_pos..cell_start],
3191 );
3192 }
3193
3194 emit_table_cell(builder, cell_text, config);
3196
3197 current_pos = cell_end;
3198 }
3199
3200 if current_pos < trimmed.len() {
3202 builder.token(SyntaxKind::WHITESPACE.into(), &trimmed[current_pos..]);
3203 }
3204
3205 if !newline_str.is_empty() {
3207 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
3208 }
3209
3210 for &idx in &indices[1..] {
3213 let tail = window.emit_or_dispatch_tail(builder, idx);
3214 emit_line_tokens(builder, tail);
3215 }
3216
3217 builder.finish_node();
3218}
3219
3220#[cfg(test)]
3221mod multiline_table_tests {
3222 use super::super::container_prefix::ContainerPrefix;
3223 use super::*;
3224 use crate::syntax::SyntaxNode;
3225
3226 #[test]
3227 fn test_multiline_separator_detection() {
3228 assert!(
3229 try_parse_multiline_separator(
3230 "-------------------------------------------------------------"
3231 )
3232 .is_some()
3233 );
3234 assert!(try_parse_multiline_separator("---").is_some());
3235 assert!(try_parse_multiline_separator(" -----").is_some()); assert!(try_parse_multiline_separator("--").is_none()); assert!(try_parse_multiline_separator("--- ---").is_none()); assert!(try_parse_multiline_separator("+---+").is_none()); }
3240
3241 #[test]
3242 fn test_basic_multiline_table() {
3243 let input = vec![
3244 "-------------------------------------------------------------",
3245 " Centered Default Right Left",
3246 " Header Aligned Aligned Aligned",
3247 "----------- ------- --------------- -------------------------",
3248 " First row 12.0 Example of a row that",
3249 " spans multiple lines.",
3250 "",
3251 " Second row 5.0 Here's another one.",
3252 "-------------------------------------------------------------",
3253 "",
3254 ];
3255
3256 let mut builder = GreenNodeBuilder::new();
3257 let prefix = ContainerPrefix::default();
3258 let window = StrippedLines::new(&input, 0, &prefix);
3259 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3260
3261 assert!(result.is_some());
3262 assert_eq!(result.unwrap(), 9);
3263 }
3264
3265 #[test]
3266 fn test_multiline_table_headerless() {
3267 let input = vec![
3268 "----------- ------- --------------- -------------------------",
3269 " First row 12.0 Example of a row that",
3270 " spans multiple lines.",
3271 "",
3272 " Second row 5.0 Here's another one.",
3273 "----------- ------- --------------- -------------------------",
3274 "",
3275 ];
3276
3277 let mut builder = GreenNodeBuilder::new();
3278 let prefix = ContainerPrefix::default();
3279 let window = StrippedLines::new(&input, 0, &prefix);
3280 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3281
3282 assert!(result.is_some());
3283 assert_eq!(result.unwrap(), 6);
3284 }
3285
3286 #[test]
3287 fn test_multiline_table_headerless_single_line_is_not_multiline() {
3288 let input = vec![
3289 "------- ------ ---------- -------",
3290 " 12 12 12 12",
3291 "------- ------ ---------- -------",
3292 "",
3293 "Not part of table.",
3294 "",
3295 ];
3296
3297 let mut builder = GreenNodeBuilder::new();
3298 let prefix = ContainerPrefix::default();
3299 let window = StrippedLines::new(&input, 0, &prefix);
3300 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3301
3302 assert!(result.is_none());
3303 }
3304
3305 #[test]
3306 fn test_multiline_table_headerless_single_row_continuation_without_blank_line() {
3307 let input = vec![
3308 "---------- --------- ----------- ---------------------------",
3309 " First row 12.0 Example of a row that spans",
3310 " multiple lines.",
3311 "---------- --------- ----------- ---------------------------",
3312 "",
3313 ];
3314
3315 let mut builder = GreenNodeBuilder::new();
3316 let prefix = ContainerPrefix::default();
3317 let window = StrippedLines::new(&input, 0, &prefix);
3318 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3319
3320 assert!(result.is_some());
3321 assert_eq!(result.unwrap(), 4);
3322 }
3323
3324 #[test]
3325 fn test_multiline_table_with_caption() {
3326 let input = vec![
3327 "-------------------------------------------------------------",
3328 " Col1 Col2",
3329 "----------- -------",
3330 " A B",
3331 "",
3332 "-------------------------------------------------------------",
3333 "",
3334 "Table: Here's the caption.",
3335 "",
3336 ];
3337
3338 let mut builder = GreenNodeBuilder::new();
3339 let prefix = ContainerPrefix::default();
3340 let window = StrippedLines::new(&input, 0, &prefix);
3341 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3342
3343 assert!(result.is_some());
3344 assert_eq!(result.unwrap(), 8);
3346 }
3347
3348 #[test]
3349 fn test_multiline_table_single_row() {
3350 let input = vec![
3351 "---------------------------------------------",
3352 " Header1 Header2",
3353 "----------- -----------",
3354 " Data More data",
3355 "",
3356 "---------------------------------------------",
3357 "",
3358 ];
3359
3360 let mut builder = GreenNodeBuilder::new();
3361 let prefix = ContainerPrefix::default();
3362 let window = StrippedLines::new(&input, 0, &prefix);
3363 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3364
3365 assert!(result.is_some());
3366 assert_eq!(result.unwrap(), 6);
3367 }
3368
3369 #[test]
3370 fn test_headerless_multiline_table_does_not_close_on_full_width_rule() {
3371 let input = vec![
3372 "- - - - -",
3373 "Third section with underscores.",
3374 "",
3375 "_____",
3376 "",
3377 "> Quote before rule",
3378 ">",
3379 "> ***",
3380 ">",
3381 "> Quote after rule",
3382 "",
3383 "Final paragraph.",
3384 "",
3385 "Here's a horizontal rule:",
3386 "",
3387 "---",
3388 "Text directly after the horizontal rule.",
3389 "",
3390 ];
3391
3392 let mut builder = GreenNodeBuilder::new();
3393 let prefix = ContainerPrefix::default();
3394 let window = StrippedLines::new(&input, 0, &prefix);
3395 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3396
3397 assert!(result.is_none());
3398 }
3399
3400 #[test]
3401 fn test_not_multiline_table() {
3402 let input = vec![
3404 " Right Left Center Default",
3405 "------- ------ ---------- -------",
3406 " 12 12 12 12",
3407 "",
3408 ];
3409
3410 let mut builder = GreenNodeBuilder::new();
3411 let prefix = ContainerPrefix::default();
3412 let window = StrippedLines::new(&input, 0, &prefix);
3413 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3414
3415 assert!(result.is_none());
3417 }
3418
3419 #[test]
3421 fn test_emit_table_cell_plain_text() {
3422 let mut builder = GreenNodeBuilder::new();
3423 emit_table_cell(&mut builder, "Cell", &ParserOptions::default());
3424 let green = builder.finish();
3425 let node = SyntaxNode::new_root(green);
3426
3427 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3428 assert_eq!(node.text(), "Cell");
3429
3430 let children: Vec<_> = node.children_with_tokens().collect();
3432 assert_eq!(children.len(), 1);
3433 assert_eq!(children[0].kind(), SyntaxKind::TEXT);
3434 }
3435
3436 #[test]
3437 fn test_emit_table_cell_with_emphasis() {
3438 let mut builder = GreenNodeBuilder::new();
3439 emit_table_cell(&mut builder, "*italic*", &ParserOptions::default());
3440 let green = builder.finish();
3441 let node = SyntaxNode::new_root(green);
3442
3443 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3444 assert_eq!(node.text(), "*italic*");
3445
3446 let children: Vec<_> = node.children().collect();
3448 assert_eq!(children.len(), 1);
3449 assert_eq!(children[0].kind(), SyntaxKind::EMPHASIS);
3450 }
3451
3452 #[test]
3453 fn test_emit_table_cell_with_code() {
3454 let mut builder = GreenNodeBuilder::new();
3455 emit_table_cell(&mut builder, "`code`", &ParserOptions::default());
3456 let green = builder.finish();
3457 let node = SyntaxNode::new_root(green);
3458
3459 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3460 assert_eq!(node.text(), "`code`");
3461
3462 let children: Vec<_> = node.children().collect();
3464 assert_eq!(children.len(), 1);
3465 assert_eq!(children[0].kind(), SyntaxKind::INLINE_CODE);
3466 }
3467
3468 #[test]
3469 fn test_emit_table_cell_with_link() {
3470 let mut builder = GreenNodeBuilder::new();
3471 emit_table_cell(&mut builder, "[text](url)", &ParserOptions::default());
3472 let green = builder.finish();
3473 let node = SyntaxNode::new_root(green);
3474
3475 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3476 assert_eq!(node.text(), "[text](url)");
3477
3478 let children: Vec<_> = node.children().collect();
3480 assert_eq!(children.len(), 1);
3481 assert_eq!(children[0].kind(), SyntaxKind::LINK);
3482 }
3483
3484 #[test]
3485 fn test_emit_table_cell_with_strong() {
3486 let mut builder = GreenNodeBuilder::new();
3487 emit_table_cell(&mut builder, "**bold**", &ParserOptions::default());
3488 let green = builder.finish();
3489 let node = SyntaxNode::new_root(green);
3490
3491 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3492 assert_eq!(node.text(), "**bold**");
3493
3494 let children: Vec<_> = node.children().collect();
3496 assert_eq!(children.len(), 1);
3497 assert_eq!(children[0].kind(), SyntaxKind::STRONG);
3498 }
3499
3500 #[test]
3501 fn test_emit_table_cell_mixed_inline() {
3502 let mut builder = GreenNodeBuilder::new();
3503 emit_table_cell(
3504 &mut builder,
3505 "Text **bold** and `code`",
3506 &ParserOptions::default(),
3507 );
3508 let green = builder.finish();
3509 let node = SyntaxNode::new_root(green);
3510
3511 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3512 assert_eq!(node.text(), "Text **bold** and `code`");
3513
3514 let children: Vec<_> = node.children_with_tokens().collect();
3516 assert!(children.len() >= 4);
3517
3518 assert_eq!(children[0].kind(), SyntaxKind::TEXT);
3520 assert_eq!(children[1].kind(), SyntaxKind::STRONG);
3521 }
3522
3523 #[test]
3524 fn test_emit_table_cell_empty() {
3525 let mut builder = GreenNodeBuilder::new();
3526 emit_table_cell(&mut builder, "", &ParserOptions::default());
3527 let green = builder.finish();
3528 let node = SyntaxNode::new_root(green);
3529
3530 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3531 assert_eq!(node.text(), "");
3532
3533 let children: Vec<_> = node.children_with_tokens().collect();
3535 assert_eq!(children.len(), 0);
3536 }
3537
3538 #[test]
3539 fn test_emit_table_cell_escaped_pipe() {
3540 let mut builder = GreenNodeBuilder::new();
3541 emit_table_cell(&mut builder, r"A \| B", &ParserOptions::default());
3542 let green = builder.finish();
3543 let node = SyntaxNode::new_root(green);
3544
3545 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3546 assert_eq!(node.text(), r"A \| B");
3548 }
3549}