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 column_offset_to_byte_index(line: &str, offset: usize) -> usize {
185 let mut width = 0;
186 for (byte_idx, ch) in line.char_indices() {
187 if width >= offset {
188 return byte_idx;
189 }
190 width += UnicodeWidthChar::width(ch).unwrap_or(0);
191 }
192 line.len()
193}
194
195fn try_parse_caption_prefix(line: &str) -> Option<(usize, &str)> {
198 let trimmed = line.trim_start();
199 let leading_spaces = line.len() - trimmed.len();
200
201 if leading_spaces > 3 {
203 return None;
204 }
205
206 if let Some(rest) = trimmed.strip_prefix("Table:") {
208 Some((leading_spaces + 6, rest))
209 } else if let Some(rest) = trimmed.strip_prefix("table:") {
210 Some((leading_spaces + 6, rest))
211 } else if let Some(rest) = trimmed.strip_prefix(':') {
212 if rest.starts_with(|c: char| c.is_whitespace()) {
215 Some((leading_spaces + 1, rest))
216 } else {
217 None
218 }
219 } else {
220 None
221 }
222}
223
224fn is_table_caption_start(line: &str) -> bool {
226 try_parse_caption_prefix(line).is_some()
227}
228
229fn is_bare_colon_caption_start(line: &str) -> bool {
230 let trimmed = line.trim_start();
231 trimmed.starts_with(':') && !trimmed.starts_with("::") && !trimmed.starts_with(":::")
232}
233
234fn bare_colon_caption_looks_like_definition_code_block(line: &str) -> bool {
235 let Some((_, rest)) = try_parse_caption_prefix(line) else {
236 return false;
237 };
238 let trimmed = rest.trim_start();
239 trimmed.starts_with("```") || trimmed.starts_with("~~~")
240}
241
242fn line_is_fenced_div_fence(line: &str) -> bool {
243 let trimmed = line.trim_start();
244 let colon_count = trimmed.chars().take_while(|&c| c == ':').count();
245 if colon_count < 3 {
246 return false;
247 }
248 let rest = &trimmed[colon_count..];
249 rest.is_empty() || rest.starts_with(char::is_whitespace)
250}
251
252fn is_valid_caption_start_before_table(lines: &(impl LineView + ?Sized), pos: usize) -> bool {
253 if !is_table_caption_start(lines.line(pos)) {
254 return false;
255 }
256
257 if is_bare_colon_caption_start(lines.line(pos))
258 && bare_colon_caption_looks_like_definition_code_block(lines.line(pos))
259 {
260 return false;
261 }
262
263 if is_bare_colon_caption_start(lines.line(pos))
265 && pos > 0
266 && !lines.line(pos - 1).trim().is_empty()
267 && !line_is_fenced_div_fence(lines.line(pos - 1))
268 {
269 return false;
270 }
271 true
272}
273
274fn is_grid_table_start(line: &str) -> bool {
277 try_parse_grid_separator(line).is_some()
278}
279
280fn is_multiline_table_start(line: &str) -> bool {
285 try_parse_multiline_separator(line).is_some() || is_column_separator(line)
286}
287
288pub(crate) fn is_caption_followed_by_table(
291 lines: &(impl LineView + ?Sized),
292 caption_pos: usize,
293) -> bool {
294 if caption_pos >= lines.line_count() {
295 return false;
296 }
297
298 if !is_valid_caption_start_before_table(lines, caption_pos) {
300 return false;
301 }
302
303 let mut pos = caption_pos + 1;
304
305 while pos < lines.line_count()
309 && !lines.line(pos).trim().is_empty()
310 && !line_is_fenced_div_fence(lines.line(pos))
311 {
312 if try_parse_table_separator(lines.line(pos)).is_some() {
314 return true;
315 }
316 pos += 1;
317 }
318
319 if pos < lines.line_count() && lines.line(pos).trim().is_empty() {
321 pos += 1;
322 }
323
324 table_grid_starts_at(lines, pos)
326}
327
328fn table_grid_starts_at(lines: &(impl LineView + ?Sized), pos: usize) -> bool {
341 if pos >= lines.line_count() {
342 return false;
343 }
344 let line = lines.line(pos);
345
346 if is_grid_table_start(line) {
348 return true;
349 }
350
351 if is_multiline_table_start(line) {
353 return true;
354 }
355
356 if try_parse_table_separator(line).is_some() {
358 return true;
359 }
360
361 if pos + 1 < lines.line_count() && !line.trim().is_empty() {
363 let next_line = lines.line(pos + 1);
364 if try_parse_table_separator(next_line).is_some()
365 || try_parse_pipe_separator(next_line).is_some()
366 {
367 return true;
368 }
369 }
370
371 false
372}
373
374fn caption_range_starting_at(
375 lines: &(impl LineView + ?Sized),
376 start: usize,
377) -> Option<(usize, usize)> {
378 if start >= lines.line_count() || !is_table_caption_start(lines.line(start)) {
379 return None;
380 }
381 let mut end = start + 1;
382 while end < lines.line_count()
383 && !lines.line(end).trim().is_empty()
384 && !line_is_fenced_div_fence(lines.line(end))
385 {
386 end += 1;
387 }
388 Some((start, end))
389}
390
391fn find_caption_before_table(
394 lines: &(impl LineView + ?Sized),
395 table_start: usize,
396) -> Option<(usize, usize)> {
397 if table_start == 0 {
398 return None;
399 }
400
401 let mut pos = table_start - 1;
404
405 if lines.line(pos).trim().is_empty() {
407 if pos == 0 {
408 return None;
409 }
410 pos -= 1;
411 }
412
413 let caption_end = pos + 1; if !is_valid_caption_start_before_table(lines, pos) {
420 let mut scan_pos = pos;
422 while scan_pos > 0 {
423 scan_pos -= 1;
424 let line = lines.line(scan_pos);
425
426 if line.trim().is_empty() || line_is_fenced_div_fence(line) {
428 return None;
429 }
430
431 if is_valid_caption_start_before_table(lines, scan_pos) {
433 if scan_pos > 0 && !lines.line(scan_pos - 1).trim().is_empty() {
434 return None;
435 }
436 if previous_nonblank_looks_like_table(lines, scan_pos) {
437 return None;
438 }
439 return Some((scan_pos, caption_end));
440 }
441 }
442 None
444 } else {
445 if pos > 0 && !lines.line(pos - 1).trim().is_empty() {
446 return None;
447 }
448 if previous_nonblank_looks_like_table(lines, pos) {
449 return None;
450 }
451 Some((pos, caption_end))
453 }
454}
455
456fn previous_nonblank_looks_like_table(lines: &(impl LineView + ?Sized), pos: usize) -> bool {
457 if pos == 0 {
458 return false;
459 }
460 let mut i = pos;
462 while i > 0 && lines.line(i - 1).trim().is_empty() {
463 i -= 1;
464 }
465 while i > 0 {
473 i -= 1;
474 if lines.line(i).trim().is_empty() || line_is_fenced_div_fence(lines.line(i)) {
475 break;
476 }
477 if line_looks_like_table_syntax(lines.line(i).trim()) {
478 return true;
479 }
480 }
481 false
482}
483
484fn line_looks_like_table_syntax(line: &str) -> bool {
485 if line.starts_with('|') && line.matches('|').count() >= 2 {
486 return true;
487 }
488 if line.starts_with('+') && line.ends_with('+') && (line.contains('-') || line.contains('=')) {
489 return true;
490 }
491 try_parse_table_separator(line).is_some()
492 || try_parse_pipe_separator(line).is_some()
493 || try_parse_grid_separator(line).is_some()
494}
495
496fn find_caption_after_table(
499 lines: &(impl LineView + ?Sized),
500 table_end: usize,
501) -> Option<(usize, usize)> {
502 if table_end >= lines.line_count() {
503 return None;
504 }
505
506 let mut pos = table_end;
507
508 if pos < lines.line_count() && lines.line(pos).trim().is_empty() {
510 pos += 1;
511 }
512
513 if pos >= lines.line_count() {
514 return None;
515 }
516
517 if is_table_caption_start(lines.line(pos)) {
519 let caption_start = pos;
520 let mut caption_end = caption_start + 1;
522 while caption_end < lines.line_count()
523 && !lines.line(caption_end).trim().is_empty()
524 && !line_is_fenced_div_fence(lines.line(caption_end))
525 {
526 caption_end += 1;
527 }
528 Some((caption_start, caption_end))
529 } else {
530 None
531 }
532}
533
534fn emit_caption_line_text(
541 builder: &mut GreenNodeBuilder<'static>,
542 text_with_newline: &str,
543 config: &ParserOptions,
544 lift_trailing_attrs: bool,
545) {
546 let (text, newline_str) = strip_newline(text_with_newline);
547
548 if lift_trailing_attrs
549 && !text.is_empty()
550 && let Some((_attrs, before_attrs, start_brace_pos)) =
551 try_parse_trailing_attributes_with_pos(text)
552 {
553 let trimmed_len = text.trim_end().len();
554 let space = &text[before_attrs.len()..start_brace_pos];
555 let raw_attrs = &text[start_brace_pos..trimmed_len];
556 let trailing_ws = &text[trimmed_len..];
557
558 if !before_attrs.is_empty() {
559 inline_emission::emit_inlines(builder, before_attrs, config, false);
560 }
561 if !space.is_empty() {
562 builder.token(SyntaxKind::WHITESPACE.into(), space);
563 }
564 emit_attribute_node(builder, raw_attrs);
565 if !trailing_ws.is_empty() {
566 builder.token(SyntaxKind::WHITESPACE.into(), trailing_ws);
567 }
568 if !newline_str.is_empty() {
569 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
570 }
571 return;
572 }
573
574 if !text.is_empty() {
575 inline_emission::emit_inlines(builder, text, config, false);
576 }
577 if !newline_str.is_empty() {
578 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
579 }
580}
581
582fn emit_caption_blank_lines(
588 builder: &mut GreenNodeBuilder<'static>,
589 window: &StrippedLines<'_, '_>,
590 from: usize,
591 to: usize,
592) {
593 for abs in from..to {
594 if window.line(abs).trim().is_empty() {
597 builder.start_node(SyntaxKind::BLANK_LINE.into());
598 let tail = window.emit_or_dispatch_tail(builder, abs);
599 builder.token(SyntaxKind::BLANK_LINE.into(), tail);
600 builder.finish_node();
601 }
602 }
603}
604
605fn emit_table_caption(
606 builder: &mut GreenNodeBuilder<'static>,
607 window: &StrippedLines<'_, '_>,
608 start: usize,
609 end: usize,
610 config: &ParserOptions,
611) {
612 builder.start_node(SyntaxKind::TABLE_CAPTION.into());
613
614 let last_idx = (end - start).saturating_sub(1);
615
616 for (i, abs) in (start..end).enumerate() {
617 let lift_attrs = i == last_idx;
618
619 let tail = window.emit_or_dispatch_tail(builder, abs);
626
627 if i == 0 {
628 let trimmed = tail.trim_start();
630 let leading_ws_len = tail.len() - trimmed.len();
631
632 if leading_ws_len > 0 {
634 builder.token(SyntaxKind::WHITESPACE.into(), &tail[..leading_ws_len]);
635 }
636
637 let prefix_and_rest = if tail.ends_with('\n') {
640 &tail[leading_ws_len..tail.len() - 1] } else {
642 &tail[leading_ws_len..]
643 };
644
645 let (prefix_len, prefix_text) = if prefix_and_rest.starts_with("Table: ") {
646 (7, "Table: ")
647 } else if prefix_and_rest.starts_with("table: ") {
648 (7, "table: ")
649 } else if prefix_and_rest.starts_with(": ") {
650 (2, ": ")
651 } else if prefix_and_rest.starts_with(':') {
652 (1, ":")
653 } else {
654 (0, "")
655 };
656
657 if prefix_len > 0 {
658 builder.token(SyntaxKind::TABLE_CAPTION_PREFIX.into(), prefix_text);
659
660 let rest_start = leading_ws_len + prefix_len;
662 if rest_start < tail.len() {
663 emit_caption_line_text(builder, &tail[rest_start..], config, lift_attrs);
664 }
665 } else {
666 emit_caption_line_text(builder, &tail[leading_ws_len..], config, lift_attrs);
668 }
669 } else {
670 emit_caption_line_text(builder, tail, config, lift_attrs);
672 }
673 }
674
675 builder.finish_node(); }
677
678fn emit_table_cell(
681 builder: &mut GreenNodeBuilder<'static>,
682 cell_text: &str,
683 config: &ParserOptions,
684) {
685 builder.start_node(SyntaxKind::TABLE_CELL.into());
686
687 if !cell_text.is_empty() {
689 inline_emission::emit_inlines(builder, cell_text, config, false);
690 }
691
692 builder.finish_node(); }
694
695fn determine_alignments(columns: &mut [Column], separator_line: &str, header_line: Option<&str>) {
697 for col in columns.iter_mut() {
698 let sep_slice = &separator_line[col.start..col.end];
699
700 if let Some(header) = header_line {
701 let header_start = column_offset_to_byte_index(header, col.start);
702 let header_end = column_offset_to_byte_index(header, col.end);
703
704 let header_text = if header_start < header_end {
706 header[header_start..header_end].trim()
707 } else if header_start < header.len() {
708 header[header_start..].trim()
709 } else {
710 ""
711 };
712
713 if header_text.is_empty() {
714 col.alignment = Alignment::Default;
715 continue;
716 }
717
718 let header_in_col = &header[header_start..header_end];
720 let text_start = header_in_col.len() - header_in_col.trim_start().len();
721 let text_end = header_in_col.trim_end().len() + text_start;
722
723 let dashes_start = 0; let dashes_end = sep_slice.len();
726
727 let flush_left = dashes_start == text_start;
728 let flush_right = dashes_end == text_end;
729
730 col.alignment = match (flush_left, flush_right) {
731 (true, true) => Alignment::Default,
732 (true, false) => Alignment::Left,
733 (false, true) => Alignment::Right,
734 (false, false) => Alignment::Center,
735 };
736 } else {
737 col.alignment = Alignment::Default;
739 }
740 }
741}
742
743pub(crate) fn try_parse_simple_table(
746 window: &StrippedLines<'_, '_>,
747 builder: &mut GreenNodeBuilder<'static>,
748 config: &ParserOptions,
749) -> Option<usize> {
750 let lines = window.raw();
751 let start_pos = window.pos();
752 log::trace!("try_parse_simple_table at line {}", start_pos + 1);
753
754 if start_pos >= lines.len() {
755 return None;
756 }
757
758 let gate_first = window.strip_at(start_pos);
765 let separator_here = try_parse_table_separator(gate_first).is_some();
766 let separator_next = !separator_here
767 && start_pos + 1 < lines.len()
768 && !gate_first.trim().is_empty()
769 && try_parse_table_separator(window.strip_at(start_pos + 1)).is_some();
770 if !separator_here && !separator_next {
771 return None;
772 }
773
774 let separator_pos = find_separator_line(window, start_pos)?;
784 log::trace!(" found separator at line {}", separator_pos + 1);
785
786 let separator_line = window.line(separator_pos);
787 let mut columns = try_parse_table_separator(separator_line)?;
788
789 let has_header = separator_pos > start_pos;
791 let header_line = if has_header {
792 Some(window.line(separator_pos - 1))
793 } else {
794 None
795 };
796
797 determine_alignments(&mut columns, separator_line, header_line);
799
800 let end_pos = find_table_end(window, separator_pos + 1);
802
803 let data_rows = end_pos - separator_pos - 1;
805
806 if data_rows == 0 {
807 return None;
808 }
809
810 let caption_before = find_caption_before_table(window, start_pos);
812
813 let caption_after = if caption_before.is_some() {
815 None
816 } else {
817 find_caption_after_table(window, end_pos)
818 };
819
820 builder.start_node(SyntaxKind::SIMPLE_TABLE.into());
822
823 if let Some((cap_start, cap_end)) = caption_before {
825 emit_table_caption(builder, window, cap_start, cap_end, config);
826 emit_caption_blank_lines(builder, window, cap_end, start_pos);
828 }
829
830 if has_header {
834 emit_table_row(
835 builder,
836 window,
837 separator_pos - 1,
838 &columns,
839 SyntaxKind::TABLE_HEADER,
840 config,
841 );
842 }
843
844 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
847 let separator_tail = window.emit_or_dispatch_tail(builder, separator_pos);
848 emit_separator_tokens(builder, separator_tail);
849 builder.finish_node();
850
851 for idx in (separator_pos + 1)..end_pos {
853 emit_table_row(
854 builder,
855 window,
856 idx,
857 &columns,
858 SyntaxKind::TABLE_ROW,
859 config,
860 );
861 }
862
863 if let Some((cap_start, cap_end)) = caption_after {
865 emit_caption_blank_lines(builder, window, end_pos, cap_start);
867 emit_table_caption(builder, window, cap_start, cap_end, config);
868 }
869
870 builder.finish_node(); let table_start = if let Some((cap_start, _)) = caption_before {
874 cap_start
875 } else if has_header {
876 separator_pos - 1
877 } else {
878 separator_pos
879 };
880
881 let table_end = if let Some((_, cap_end)) = caption_after {
882 cap_end
883 } else {
884 end_pos
885 };
886
887 let lines_consumed = table_end - table_start;
888
889 Some(lines_consumed)
890}
891
892fn find_separator_line(lines: &(impl LineView + ?Sized), start_pos: usize) -> Option<usize> {
894 log::trace!(" find_separator_line from line {}", start_pos + 1);
895
896 log::trace!(" checking first line: {:?}", lines.line(start_pos));
898 if try_parse_table_separator(lines.line(start_pos)).is_some() {
899 log::trace!(" separator found at first line");
900 return Some(start_pos);
901 }
902
903 if start_pos + 1 < lines.line_count()
905 && !lines.line(start_pos).trim().is_empty()
906 && try_parse_table_separator(lines.line(start_pos + 1)).is_some()
907 {
908 return Some(start_pos + 1);
909 }
910 None
911}
912
913fn find_table_end(lines: &(impl LineView + ?Sized), start_pos: usize) -> usize {
915 for i in start_pos..lines.line_count() {
916 if lines.line(i).trim().is_empty() {
917 return i;
918 }
919 if try_parse_table_separator(lines.line(i)).is_some() {
921 if i + 1 >= lines.line_count() || lines.line(i + 1).trim().is_empty() {
923 return i + 1;
924 }
925 }
926 }
927 lines.line_count()
928}
929
930fn emit_table_row(
933 builder: &mut GreenNodeBuilder<'static>,
934 window: &StrippedLines<'_, '_>,
935 abs_idx: usize,
936 columns: &[Column],
937 row_kind: SyntaxKind,
938 config: &ParserOptions,
939) {
940 builder.start_node(row_kind.into());
941
942 let line = window.emit_or_dispatch_tail(builder, abs_idx);
947
948 let (line_without_newline, newline_str) = strip_newline(line);
949
950 let trimmed = line_without_newline.trim_start();
952 let leading_ws_len = line_without_newline.len() - line_without_newline.trim_start().len();
953 if leading_ws_len > 0 {
954 builder.token(
955 SyntaxKind::WHITESPACE.into(),
956 &line_without_newline[..leading_ws_len],
957 );
958 }
959
960 let mut current_pos = 0;
962
963 for (i, col) in columns.iter().enumerate() {
965 let cell_start = if col.start >= leading_ws_len {
967 column_offset_to_byte_index(trimmed, col.start - leading_ws_len)
968 } else {
969 0
970 };
971
972 let end_offset = columns.get(i + 1).map_or(usize::MAX, |next| next.start);
978 let cell_end = if end_offset == usize::MAX {
979 trimmed.len()
980 } else if end_offset >= leading_ws_len {
981 column_offset_to_byte_index(trimmed, end_offset - leading_ws_len)
982 } else {
983 0
984 };
985
986 let cell_text = if cell_start < cell_end && cell_start < trimmed.len() {
991 &trimmed[cell_start..cell_end]
992 } else {
993 ""
994 };
995
996 let cell_content = cell_text.trim();
997 let cell_content_start = cell_text.len() - cell_text.trim_start().len();
998
999 let content_abs_pos = (cell_start + cell_content_start).min(trimmed.len());
1001 if current_pos < content_abs_pos {
1002 builder.token(
1003 SyntaxKind::WHITESPACE.into(),
1004 &trimmed[current_pos..content_abs_pos],
1005 );
1006 }
1007
1008 emit_table_cell(builder, cell_content, config);
1010
1011 current_pos = content_abs_pos + cell_content.len();
1013 }
1014
1015 if current_pos < trimmed.len() {
1017 builder.token(SyntaxKind::WHITESPACE.into(), &trimmed[current_pos..]);
1018 }
1019
1020 if !newline_str.is_empty() {
1022 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
1023 }
1024
1025 builder.finish_node();
1026}
1027
1028fn try_parse_pipe_separator(line: &str) -> Option<Vec<Alignment>> {
1035 let trimmed = line.trim();
1036
1037 if !trimmed.contains('|') && !trimmed.contains('+') {
1039 return None;
1040 }
1041
1042 let cells: Vec<&str> = if trimmed.contains('+') {
1044 trimmed.split(['|', '+']).collect()
1046 } else {
1047 trimmed.split('|').collect()
1048 };
1049
1050 let mut alignments = Vec::new();
1051
1052 for cell in cells {
1053 let cell = cell.trim();
1054
1055 if cell.is_empty() {
1057 continue;
1058 }
1059
1060 let starts_colon = cell.starts_with(':');
1062 let ends_colon = cell.ends_with(':');
1063
1064 let without_colons = cell.trim_start_matches(':').trim_end_matches(':');
1066
1067 if without_colons.is_empty() || !without_colons.chars().all(|c| c == '-') {
1069 return None;
1070 }
1071
1072 let alignment = match (starts_colon, ends_colon) {
1074 (true, true) => Alignment::Center,
1075 (true, false) => Alignment::Left,
1076 (false, true) => Alignment::Right,
1077 (false, false) => Alignment::Default,
1078 };
1079
1080 alignments.push(alignment);
1081 }
1082
1083 if alignments.is_empty() {
1085 None
1086 } else {
1087 Some(alignments)
1088 }
1089}
1090
1091fn parse_pipe_table_row(line: &str) -> Vec<String> {
1094 let trimmed = line.trim();
1095
1096 let mut cells = Vec::new();
1097 let mut current_cell = String::new();
1098 let mut chars = trimmed.chars().peekable();
1099 let mut char_count = 0;
1100
1101 while let Some(ch) = chars.next() {
1102 char_count += 1;
1103 match ch {
1104 '\\' => {
1105 if let Some(&'|') = chars.peek() {
1107 current_cell.push('\\');
1108 current_cell.push('|');
1109 chars.next(); } else {
1111 current_cell.push(ch);
1112 }
1113 }
1114 '|' => {
1115 if char_count == 1 {
1117 continue; }
1119
1120 cells.push(current_cell.trim().to_string());
1122 current_cell.clear();
1123 }
1124 _ => {
1125 current_cell.push(ch);
1126 }
1127 }
1128 }
1129
1130 let trimmed_cell = current_cell.trim().to_string();
1132 if !trimmed_cell.is_empty() {
1133 cells.push(trimmed_cell);
1134 }
1135
1136 cells
1137}
1138
1139fn emit_pipe_table_row(
1142 builder: &mut GreenNodeBuilder<'static>,
1143 window: &StrippedLines<'_, '_>,
1144 abs_idx: usize,
1145 row_kind: SyntaxKind,
1146 config: &ParserOptions,
1147) {
1148 builder.start_node(row_kind.into());
1149
1150 let line = if abs_idx == window.dispatch_pos() {
1158 window.dispatch_tail()
1159 } else {
1160 window.emit_prefix_at(builder, abs_idx)
1161 };
1162
1163 let (line_without_newline, newline_str) = strip_newline(line);
1164 let trimmed = line_without_newline.trim();
1165
1166 let mut cell_starts = Vec::new();
1168 let mut cell_ends = Vec::new();
1169 let mut in_escape = false;
1170
1171 let mut pipe_positions = Vec::new();
1173 for (i, ch) in trimmed.char_indices() {
1174 if in_escape {
1175 in_escape = false;
1176 continue;
1177 }
1178 if ch == '\\' {
1179 in_escape = true;
1180 continue;
1181 }
1182 if ch == '|' {
1183 pipe_positions.push(i);
1184 }
1185 }
1186
1187 if pipe_positions.is_empty() {
1189 cell_starts.push(0);
1191 cell_ends.push(trimmed.len());
1192 } else {
1193 let start_pipe = pipe_positions.first() == Some(&0);
1195 let end_pipe = pipe_positions.last() == Some(&(trimmed.len() - 1));
1197
1198 if start_pipe {
1199 for i in 1..pipe_positions.len() {
1201 cell_starts.push(pipe_positions[i - 1] + 1);
1202 cell_ends.push(pipe_positions[i]);
1203 }
1204 if !end_pipe {
1206 cell_starts.push(*pipe_positions.last().unwrap() + 1);
1207 cell_ends.push(trimmed.len());
1208 }
1209 } else {
1210 cell_starts.push(0);
1212 cell_ends.push(pipe_positions[0]);
1213
1214 for i in 1..pipe_positions.len() {
1215 cell_starts.push(pipe_positions[i - 1] + 1);
1216 cell_ends.push(pipe_positions[i]);
1217 }
1218
1219 if !end_pipe {
1221 cell_starts.push(*pipe_positions.last().unwrap() + 1);
1222 cell_ends.push(trimmed.len());
1223 }
1224 }
1225 }
1226
1227 let leading_ws_len = line_without_newline.len() - line_without_newline.trim_start().len();
1229 if leading_ws_len > 0 {
1230 builder.token(
1231 SyntaxKind::WHITESPACE.into(),
1232 &line_without_newline[..leading_ws_len],
1233 );
1234 }
1235
1236 for (idx, (start, end)) in cell_starts.iter().zip(cell_ends.iter()).enumerate() {
1238 if *start > 0 {
1240 builder.token(SyntaxKind::TEXT.into(), "|");
1241 } else if idx == 0 && trimmed.starts_with('|') {
1242 builder.token(SyntaxKind::TEXT.into(), "|");
1244 }
1245
1246 let cell_with_ws = &trimmed[*start..*end];
1248 let cell_content = cell_with_ws.trim();
1249
1250 let cell_leading_ws = &cell_with_ws[..cell_with_ws.len() - cell_with_ws.trim_start().len()];
1252 if !cell_leading_ws.is_empty() {
1253 builder.token(SyntaxKind::WHITESPACE.into(), cell_leading_ws);
1254 }
1255
1256 emit_table_cell(builder, cell_content, config);
1258
1259 let cell_trailing_ws_start = cell_leading_ws.len() + cell_content.len();
1261 if cell_trailing_ws_start < cell_with_ws.len() {
1262 builder.token(
1263 SyntaxKind::WHITESPACE.into(),
1264 &cell_with_ws[cell_trailing_ws_start..],
1265 );
1266 }
1267 }
1268
1269 if !pipe_positions.is_empty() && trimmed.ends_with('|') {
1271 builder.token(SyntaxKind::TEXT.into(), "|");
1272 }
1273
1274 let trailing_ws_start = leading_ws_len + trimmed.len();
1276 if trailing_ws_start < line_without_newline.len() {
1277 builder.token(
1278 SyntaxKind::WHITESPACE.into(),
1279 &line_without_newline[trailing_ws_start..],
1280 );
1281 }
1282
1283 if !newline_str.is_empty() {
1285 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
1286 }
1287
1288 builder.finish_node();
1289}
1290
1291pub(crate) fn try_parse_pipe_table(
1294 window: &StrippedLines<'_, '_>,
1295 builder: &mut GreenNodeBuilder<'static>,
1296 config: &ParserOptions,
1297) -> Option<usize> {
1298 let lines = window.raw();
1299 let start_pos = window.pos();
1300 if start_pos + 1 >= lines.len() {
1301 return None;
1302 }
1303
1304 if !window.strip_at(start_pos).contains('|') && !is_caption_followed_by_table(window, start_pos)
1312 {
1313 return None;
1314 }
1315
1316 let (actual_start, caption_before) = if is_caption_followed_by_table(window, start_pos) {
1328 let (cap_start, cap_end) = caption_range_starting_at(window, start_pos)?;
1329 let mut pos = cap_end;
1330 while pos < window.line_count() && window.line(pos).trim().is_empty() {
1331 pos += 1;
1332 }
1333 (pos, Some((cap_start, cap_end)))
1334 } else {
1335 (start_pos, None)
1336 };
1337
1338 if actual_start + 1 >= lines.len() {
1339 return None;
1340 }
1341
1342 if !window.line(actual_start).contains('|') {
1344 return None;
1345 }
1346
1347 let alignments = try_parse_pipe_separator(window.line(actual_start + 1))?;
1349
1350 let header_cells = parse_pipe_table_row(window.line(actual_start));
1352
1353 if header_cells.len() != alignments.len() && !header_cells.is_empty() {
1355 if header_cells.len() < alignments.len() / 2 || header_cells.len() > alignments.len() * 2 {
1357 return None;
1358 }
1359 }
1360
1361 let mut end_pos = actual_start + 2;
1363 while end_pos < window.line_count() {
1364 let line = window.line(end_pos);
1365 if line.trim().is_empty() {
1366 break;
1367 }
1368 if !line.contains('|') {
1370 break;
1371 }
1372 end_pos += 1;
1373 }
1374
1375 if end_pos <= actual_start + 2 {
1377 return None;
1378 }
1379
1380 let caption_before = caption_before.or_else(|| find_caption_before_table(window, actual_start));
1382
1383 let caption_after = if caption_before.is_some() {
1385 None
1386 } else {
1387 find_caption_after_table(window, end_pos)
1388 };
1389
1390 builder.start_node(SyntaxKind::PIPE_TABLE.into());
1392
1393 if let Some((cap_start, cap_end)) = caption_before {
1395 emit_table_caption(builder, window, cap_start, cap_end, config);
1396 emit_caption_blank_lines(builder, window, cap_end, actual_start);
1398 }
1399
1400 emit_pipe_table_row(
1405 builder,
1406 window,
1407 actual_start,
1408 SyntaxKind::TABLE_HEADER,
1409 config,
1410 );
1411
1412 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
1415 let sep_idx = actual_start + 1;
1416 let separator_tail = if sep_idx == window.dispatch_pos() {
1417 window.dispatch_tail()
1418 } else {
1419 window.emit_prefix_at(builder, sep_idx)
1420 };
1421 emit_separator_tokens(builder, separator_tail);
1422 builder.finish_node();
1423
1424 for idx in (actual_start + 2)..end_pos {
1426 emit_pipe_table_row(builder, window, idx, SyntaxKind::TABLE_ROW, config);
1427 }
1428
1429 if let Some((cap_start, cap_end)) = caption_after {
1431 emit_caption_blank_lines(builder, window, end_pos, cap_start);
1433 emit_table_caption(builder, window, cap_start, cap_end, config);
1434 }
1435
1436 builder.finish_node(); let table_start = caption_before
1440 .map(|(start, _)| start)
1441 .unwrap_or(actual_start);
1442 let table_end = if let Some((_, cap_end)) = caption_after {
1443 cap_end
1444 } else {
1445 end_pos
1446 };
1447
1448 Some(table_end - table_start)
1449}
1450
1451#[cfg(test)]
1452mod tests {
1453 use super::super::container_prefix::ContainerPrefix;
1454 use super::*;
1455
1456 #[test]
1457 fn test_separator_detection() {
1458 assert!(try_parse_table_separator("------- ------ ---------- -------").is_some());
1459 assert!(try_parse_table_separator(" --- --- ---").is_some());
1460 assert!(try_parse_table_separator("-------").is_none()); assert!(try_parse_table_separator("--- --- ---").is_some()); }
1463
1464 #[test]
1465 fn test_column_extraction() {
1466 let line = "------- ------ ---------- -------";
1467 let columns = extract_columns(line, 0);
1468 assert_eq!(columns.len(), 4);
1469 }
1470
1471 #[test]
1472 fn column_offset_maps_by_display_width() {
1473 let line = "地號xy";
1477 assert_eq!(column_offset_to_byte_index(line, 4), 6);
1478 assert_eq!(column_offset_to_byte_index("abcd", 2), 2);
1480 }
1481
1482 #[test]
1483 fn simple_table_cjk_header_keeps_footnote_ref_intact() {
1484 let input = "\
1489 地號 地主 路段 總長[^d] 水利局舖的[^s]
1490------ ------- -------- --------- --------------
14912976 Ralph 南段 64 33
1492";
1493 let tree = crate::parser::parse(input, None);
1494 let refs: Vec<_> = tree
1495 .descendants()
1496 .filter(|n| n.kind() == SyntaxKind::FOOTNOTE_REFERENCE)
1497 .map(|n| n.text().to_string())
1498 .collect();
1499 assert_eq!(refs, vec!["[^d]".to_string(), "[^s]".to_string()]);
1500 }
1501
1502 #[test]
1503 fn test_simple_table_with_header() {
1504 let input = vec![
1505 " Right Left Center Default",
1506 "------- ------ ---------- -------",
1507 " 12 12 12 12",
1508 " 123 123 123 123",
1509 "",
1510 ];
1511
1512 let mut builder = GreenNodeBuilder::new();
1513 let prefix = ContainerPrefix::default();
1514 let window = StrippedLines::new(&input, 0, &prefix);
1515 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1516
1517 assert!(result.is_some());
1518 assert_eq!(result.unwrap(), 4); }
1520
1521 #[test]
1522 fn test_headerless_table() {
1523 let input = vec![
1524 "------- ------ ---------- -------",
1525 " 12 12 12 12",
1526 " 123 123 123 123",
1527 "",
1528 ];
1529
1530 let mut builder = GreenNodeBuilder::new();
1531 let prefix = ContainerPrefix::default();
1532 let window = StrippedLines::new(&input, 0, &prefix);
1533 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1534
1535 assert!(result.is_some());
1536 assert_eq!(result.unwrap(), 3); }
1538
1539 #[test]
1540 fn test_caption_prefix_detection() {
1541 assert!(try_parse_caption_prefix("Table: My caption").is_some());
1542 assert!(try_parse_caption_prefix("table: My caption").is_some());
1543 assert!(try_parse_caption_prefix(": My caption").is_some());
1544 assert!(try_parse_caption_prefix(":").is_none()); assert!(try_parse_caption_prefix("Not a caption").is_none());
1546 }
1547
1548 #[test]
1549 fn table_grid_starts_at_matches_each_kind() {
1550 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));
1560
1561 assert!(!table_grid_starts_at(&["just some prose"][..], 0));
1563 assert!(!table_grid_starts_at(&["# Heading"][..], 0));
1564 assert!(!table_grid_starts_at(&["```", "code", "```"][..], 0)); assert!(!table_grid_starts_at(&["only one line"][..], 1)); }
1567
1568 #[test]
1573 fn caption_lookahead_agrees_with_real_parse() {
1574 let with_table = ": Cap\n\n| A | B |\n|---|---|\n| 1 | 2 |\n";
1575 let lines: Vec<&str> = with_table.lines().collect();
1576 assert!(is_caption_followed_by_table(&lines[..], 0));
1577 assert!(format!("{:#?}", crate::parse(with_table, None)).contains("PIPE_TABLE"));
1578
1579 let no_table = ": Cap\n\nplain paragraph\n";
1580 let lines: Vec<&str> = no_table.lines().collect();
1581 assert!(!is_caption_followed_by_table(&lines[..], 0));
1582 assert!(!format!("{:#?}", crate::parse(no_table, None)).contains("TABLE"));
1583 }
1584
1585 #[test]
1592 fn ordered_marker_on_pipe_table_line_is_top_level_table() {
1593 let input = "1. | a | b |\n | - | - |\n | 1 | 2 |\n";
1594 let tree = crate::parse(input, None);
1595 assert!(
1596 tree.descendants()
1597 .any(|n| n.kind() == SyntaxKind::PIPE_TABLE),
1598 "ordered marker + pipe table on the marker line should be a top-level table"
1599 );
1600 assert!(
1601 !tree.descendants().any(|n| n.kind() == SyntaxKind::LIST),
1602 "it must not nest under a list"
1603 );
1604 let dump = format!("{tree:#?}");
1606 assert!(
1607 dump.contains("1."),
1608 "marker text preserved as a header cell"
1609 );
1610 assert!(dump.contains('b'), "overflow cell `b` preserved (lossless)");
1611 }
1612
1613 #[test]
1614 fn lone_ordered_marker_pipe_line_is_a_list() {
1615 let input = "1. | a | b |\n";
1617 let tree = crate::parse(input, None);
1618 assert!(
1619 tree.descendants().any(|n| n.kind() == SyntaxKind::LIST),
1620 "a lone ordered marker line stays a list"
1621 );
1622 assert!(
1623 !tree
1624 .descendants()
1625 .any(|n| n.kind() == SyntaxKind::PIPE_TABLE),
1626 "no table without a delimiter row"
1627 );
1628 }
1629
1630 #[test]
1631 fn bullet_marker_on_pipe_table_line_stays_a_nested_table() {
1632 let input = "- | a | b |\n | - | - |\n | 1 | 2 |\n";
1634 let tree = crate::parse(input, None);
1635 assert!(
1636 tree.descendants().any(|n| n.kind() == SyntaxKind::LIST),
1637 "bullet marker keeps the list"
1638 );
1639 assert!(
1640 tree.descendants()
1641 .any(|n| n.kind() == SyntaxKind::PIPE_TABLE),
1642 "with the table nested inside the list item"
1643 );
1644 }
1645
1646 #[test]
1647 fn bare_colon_fenced_code_is_not_table_caption() {
1648 let input = "Term\n: ```\n code\n ```\n";
1649 let tree = crate::parse(input, None);
1650
1651 assert!(
1652 tree.descendants()
1653 .any(|node| node.kind() == SyntaxKind::DEFINITION_LIST),
1654 "should parse as definition list"
1655 );
1656 assert!(
1657 tree.descendants()
1658 .any(|node| node.kind() == SyntaxKind::CODE_BLOCK),
1659 "definition should preserve fenced code block"
1660 );
1661 assert!(
1662 !tree
1663 .descendants()
1664 .any(|node| node.kind() == SyntaxKind::TABLE_CAPTION),
1665 "fenced code definition should not be parsed as table caption"
1666 );
1667 }
1668
1669 #[test]
1670 fn bare_colon_caption_after_div_opening_is_table_caption() {
1671 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";
1672 let tree = crate::parse(input, None);
1673
1674 let caption_count = tree
1675 .descendants()
1676 .filter(|node| node.kind() == SyntaxKind::TABLE_CAPTION)
1677 .count();
1678 assert_eq!(
1679 caption_count, 2,
1680 "expected both captions to attach to tables"
1681 );
1682 assert!(
1683 !tree
1684 .descendants()
1685 .any(|node| node.kind() == SyntaxKind::DEFINITION_LIST),
1686 "caption lines in this fenced div table layout should not parse as definition list"
1687 );
1688 }
1689
1690 #[test]
1691 fn test_table_with_caption_after() {
1692 let input = vec![
1693 " Right Left Center Default",
1694 "------- ------ ---------- -------",
1695 " 12 12 12 12",
1696 " 123 123 123 123",
1697 "",
1698 "Table: Demonstration of simple table syntax.",
1699 "",
1700 ];
1701
1702 let mut builder = GreenNodeBuilder::new();
1703 let prefix = ContainerPrefix::default();
1704 let window = StrippedLines::new(&input, 0, &prefix);
1705 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1706
1707 assert!(result.is_some());
1708 assert_eq!(result.unwrap(), 6);
1710 }
1711
1712 #[test]
1713 fn test_table_with_caption_before() {
1714 let input = vec![
1715 "Table: Demonstration of simple table syntax.",
1716 "",
1717 " Right Left Center Default",
1718 "------- ------ ---------- -------",
1719 " 12 12 12 12",
1720 " 123 123 123 123",
1721 "",
1722 ];
1723
1724 let mut builder = GreenNodeBuilder::new();
1725 let prefix = ContainerPrefix::default();
1726 let window = StrippedLines::new(&input, 2, &prefix);
1727 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1728
1729 assert!(result.is_some());
1730 assert_eq!(result.unwrap(), 6);
1732 }
1733
1734 #[test]
1735 fn test_caption_with_colon_prefix() {
1736 let input = vec![
1737 " Right Left",
1738 "------- ------",
1739 " 12 12",
1740 "",
1741 ": Short caption",
1742 "",
1743 ];
1744
1745 let mut builder = GreenNodeBuilder::new();
1746 let prefix = ContainerPrefix::default();
1747 let window = StrippedLines::new(&input, 0, &prefix);
1748 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1749
1750 assert!(result.is_some());
1751 assert_eq!(result.unwrap(), 5); }
1753
1754 #[test]
1755 fn test_multiline_caption() {
1756 let input = vec![
1757 " Right Left",
1758 "------- ------",
1759 " 12 12",
1760 "",
1761 "Table: This is a longer caption",
1762 "that spans multiple lines.",
1763 "",
1764 ];
1765
1766 let mut builder = GreenNodeBuilder::new();
1767 let prefix = ContainerPrefix::default();
1768 let window = StrippedLines::new(&input, 0, &prefix);
1769 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1770
1771 assert!(result.is_some());
1772 assert_eq!(result.unwrap(), 6);
1774 }
1775
1776 #[test]
1777 fn test_simple_table_with_multibyte_cell_content() {
1778 let input = vec![
1779 "Name Hex code Hue C, M, Y, K (%) R, G, B (0-255) R, G, B (%)",
1780 "-------------- ------------ ------- ---------------- ----------------- ------------",
1781 " orange #E69F00 41° 0, 50, 100, 0 230, 159, 0 90, 60, 0",
1782 " sky blue #56B4E9 202° 80, 0, 0, 0 86, 180, 233 35, 70, 90",
1783 "",
1784 ];
1785
1786 let mut builder = GreenNodeBuilder::new();
1787 let prefix = ContainerPrefix::default();
1788 let window = StrippedLines::new(&input, 0, &prefix);
1789 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1790
1791 assert!(result.is_some());
1792 assert_eq!(result.unwrap(), 4);
1793 }
1794
1795 #[test]
1797 fn test_pipe_separator_detection() {
1798 assert!(try_parse_pipe_separator("|------:|:-----|---------|:------:|").is_some());
1799 assert!(try_parse_pipe_separator("|---|---|").is_some());
1800 assert!(try_parse_pipe_separator("-----|-----:").is_some()); assert!(try_parse_pipe_separator("|-----+-------|").is_some()); assert!(try_parse_pipe_separator("not a separator").is_none());
1803 }
1804
1805 #[test]
1806 fn test_pipe_alignments() {
1807 let aligns = try_parse_pipe_separator("|------:|:-----|---------|:------:|").unwrap();
1808 assert_eq!(aligns.len(), 4);
1809 assert_eq!(aligns[0], Alignment::Right);
1810 assert_eq!(aligns[1], Alignment::Left);
1811 assert_eq!(aligns[2], Alignment::Default);
1812 assert_eq!(aligns[3], Alignment::Center);
1813 }
1814
1815 #[test]
1816 fn test_parse_pipe_table_row() {
1817 let cells = parse_pipe_table_row("| Right | Left | Center |");
1818 assert_eq!(cells.len(), 3);
1819 assert_eq!(cells[0], "Right");
1820 assert_eq!(cells[1], "Left");
1821 assert_eq!(cells[2], "Center");
1822
1823 let cells2 = parse_pipe_table_row("Right | Left | Center");
1825 assert_eq!(cells2.len(), 3);
1826 }
1827
1828 #[test]
1829 fn test_basic_pipe_table() {
1830 let input = vec![
1831 "",
1832 "| Right | Left | Center |",
1833 "|------:|:-----|:------:|",
1834 "| 12 | 12 | 12 |",
1835 "| 123 | 123 | 123 |",
1836 "",
1837 ];
1838
1839 let mut builder = GreenNodeBuilder::new();
1840 let prefix = ContainerPrefix::default();
1841 let window = StrippedLines::new(&input, 1, &prefix);
1842 let result = try_parse_pipe_table(&window, &mut builder, &ParserOptions::default());
1843
1844 assert!(result.is_some());
1845 assert_eq!(result.unwrap(), 4); }
1847
1848 #[test]
1849 fn test_pipe_table_no_edge_pipes() {
1850 let input = vec![
1851 "",
1852 "fruit| price",
1853 "-----|-----:",
1854 "apple|2.05",
1855 "pear|1.37",
1856 "",
1857 ];
1858
1859 let mut builder = GreenNodeBuilder::new();
1860 let prefix = ContainerPrefix::default();
1861 let window = StrippedLines::new(&input, 1, &prefix);
1862 let result = try_parse_pipe_table(&window, &mut builder, &ParserOptions::default());
1863
1864 assert!(result.is_some());
1865 assert_eq!(result.unwrap(), 4);
1866 }
1867
1868 #[test]
1869 fn test_pipe_table_with_caption() {
1870 let input = vec![
1871 "",
1872 "| Col1 | Col2 |",
1873 "|------|------|",
1874 "| A | B |",
1875 "",
1876 "Table: My pipe table",
1877 "",
1878 ];
1879
1880 let mut builder = GreenNodeBuilder::new();
1881 let prefix = ContainerPrefix::default();
1882 let window = StrippedLines::new(&input, 1, &prefix);
1883 let result = try_parse_pipe_table(&window, &mut builder, &ParserOptions::default());
1884
1885 assert!(result.is_some());
1886 assert_eq!(result.unwrap(), 5); }
1888
1889 #[test]
1890 fn test_pipe_table_with_multiline_caption_before() {
1891 let input = vec![
1892 ": (#tab:base) base R quoting",
1893 "functions",
1894 "",
1895 "| C | D |",
1896 "|---|---|",
1897 "| 3 | 4 |",
1898 "",
1899 ];
1900
1901 let mut builder = GreenNodeBuilder::new();
1902 let prefix = ContainerPrefix::default();
1903 let window = StrippedLines::new(&input, 0, &prefix);
1904 let result = try_parse_pipe_table(&window, &mut builder, &ParserOptions::default());
1905
1906 assert!(result.is_some());
1907 assert_eq!(result.unwrap(), 6);
1909 }
1910}
1911
1912fn try_parse_grid_separator(line: &str) -> Option<Vec<GridColumn>> {
1919 let trimmed = line.trim_start();
1920 let leading_spaces = line.len() - trimmed.len();
1921
1922 if leading_spaces > 0 {
1927 return None;
1928 }
1929
1930 if !trimmed.starts_with('+') || !trimmed.trim_end().ends_with('+') {
1932 return None;
1933 }
1934
1935 let trimmed = trimmed.trim_end();
1937 let segments: Vec<&str> = trimmed.split('+').collect();
1938
1939 if segments.len() < 3 {
1941 return None;
1942 }
1943
1944 let mut columns = Vec::new();
1945
1946 for segment in segments.iter().skip(1).take(segments.len() - 2) {
1948 if segment.is_empty() {
1949 continue;
1950 }
1951
1952 let seg_trimmed = *segment;
1954
1955 let inner = seg_trimmed.trim_start_matches(':').trim_end_matches(':');
1957
1958 if inner.is_empty() {
1960 return None;
1961 }
1962
1963 let first_char = inner.chars().next().unwrap();
1964 if first_char != '-' && first_char != '=' {
1965 return None;
1966 }
1967
1968 if !inner.chars().all(|c| c == first_char) {
1969 return None;
1970 }
1971
1972 let is_header_sep = first_char == '=';
1973
1974 columns.push(GridColumn {
1975 is_header_separator: is_header_sep,
1976 width: seg_trimmed.chars().count(),
1977 });
1978 }
1979
1980 if columns.is_empty() {
1981 None
1982 } else {
1983 Some(columns)
1984 }
1985}
1986
1987#[derive(Debug, Clone)]
1989struct GridColumn {
1990 is_header_separator: bool,
1991 width: usize,
1992}
1993
1994fn slice_cell_by_display_width(line: &str, start_byte: usize, width: usize) -> (usize, usize) {
1995 let mut end_byte = start_byte;
1996 let mut display_cols = 0usize;
1997
1998 for (offset, ch) in line[start_byte..].char_indices() {
1999 if ch == '|' {
2000 let sep_byte = start_byte + offset;
2001 return (sep_byte, sep_byte + 1);
2002 }
2003 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0);
2004 if display_cols + ch_width > width {
2005 break;
2006 }
2007 display_cols += ch_width;
2008 end_byte = start_byte + offset + ch.len_utf8();
2009 if display_cols >= width {
2010 break;
2011 }
2012 }
2013
2014 let mut sep_byte = end_byte;
2018 while sep_byte < line.len() {
2019 let mut chars = line[sep_byte..].chars();
2020 let Some(ch) = chars.next() else {
2021 break;
2022 };
2023 if ch == '|' {
2024 return (sep_byte, sep_byte + 1);
2025 }
2026 sep_byte += ch.len_utf8();
2027 }
2028
2029 (end_byte, end_byte)
2030}
2031
2032fn is_grid_content_row(line: &str) -> bool {
2035 let trimmed = line.trim_start();
2036 let leading_spaces = line.len() - trimmed.len();
2037
2038 if leading_spaces > 3 {
2039 return false;
2040 }
2041
2042 let trimmed = trimmed.trim_end();
2043 trimmed.starts_with('|') && (trimmed.ends_with('|') || trimmed.ends_with('+'))
2044}
2045
2046fn extract_grid_cells_from_line(line: &str, _columns: &[GridColumn]) -> Vec<String> {
2050 let (line_content, _) = strip_newline(line);
2051 let line_trimmed = line_content.trim();
2052
2053 if !line_trimmed.starts_with('|') || !line_trimmed.ends_with('|') {
2054 return vec![String::new(); _columns.len()];
2055 }
2056
2057 let mut cells = Vec::with_capacity(_columns.len());
2058 let mut pos_byte = 1; for col in _columns {
2061 let col_idx = cells.len();
2062 if pos_byte >= line_trimmed.len() {
2063 cells.push(String::new());
2064 continue;
2065 }
2066
2067 let start_byte = pos_byte;
2068 let end_byte = if col_idx + 1 == _columns.len() {
2069 line_trimmed.len().saturating_sub(1) } else {
2071 let (end, next_start) = slice_cell_by_display_width(line_trimmed, pos_byte, col.width);
2072 pos_byte = next_start;
2073 end
2074 };
2075 cells.push(line_trimmed[start_byte..end_byte].trim().to_string());
2076 if col_idx + 1 == _columns.len() {
2077 pos_byte = line_trimmed.len();
2078 }
2079 }
2080
2081 cells
2082}
2083
2084fn emit_grid_table_row(
2088 builder: &mut GreenNodeBuilder<'static>,
2089 window: &StrippedLines<'_, '_>,
2090 indices: &[usize],
2091 columns: &[GridColumn],
2092 row_kind: SyntaxKind,
2093 config: &ParserOptions,
2094) {
2095 if indices.is_empty() {
2096 return;
2097 }
2098
2099 builder.start_node(row_kind.into());
2100
2101 let first_line = window.emit_or_dispatch_tail(builder, indices[0]);
2107 let cell_contents = extract_grid_cells_from_line(first_line, columns);
2108 let (line_without_newline, newline_str) = strip_newline(first_line);
2109 let trimmed = line_without_newline.trim();
2110 let expected_pipe_count = columns.len().saturating_add(1);
2111 let actual_pipe_count = trimmed.chars().filter(|&c| c == '|').count();
2112
2113 if actual_pipe_count != expected_pipe_count {
2117 emit_line_tokens(builder, first_line);
2118 for &idx in &indices[1..] {
2119 let tail = window.emit_or_dispatch_tail(builder, idx);
2120 emit_line_tokens(builder, tail);
2121 }
2122 builder.finish_node();
2123 return;
2124 }
2125
2126 let leading_ws_len = line_without_newline.len() - line_without_newline.trim_start().len();
2128 if leading_ws_len > 0 {
2129 builder.token(
2130 SyntaxKind::WHITESPACE.into(),
2131 &line_without_newline[..leading_ws_len],
2132 );
2133 }
2134
2135 if trimmed.starts_with('|') {
2137 builder.token(SyntaxKind::TEXT.into(), "|");
2138 }
2139
2140 let mut pos_byte = 1usize; for (idx, cell_content) in cell_contents.iter().enumerate() {
2143 let part = if idx < columns.len() && pos_byte <= trimmed.len() {
2144 let start_byte = pos_byte;
2145 let end_byte = if idx + 1 == columns.len() && !trimmed.is_empty() {
2146 trimmed.len().saturating_sub(1) } else {
2148 let (end, next_start) =
2149 slice_cell_by_display_width(trimmed, pos_byte, columns[idx].width);
2150 pos_byte = next_start;
2151 end
2152 };
2153 let slice = &trimmed[start_byte..end_byte];
2154 if idx + 1 == columns.len() {
2155 pos_byte = trimmed.len();
2156 }
2157 slice
2158 } else {
2159 ""
2160 };
2161
2162 let cell_trimmed = part.trim();
2164 let ws_start_len = part.len() - part.trim_start().len();
2165 if ws_start_len > 0 {
2166 builder.token(SyntaxKind::WHITESPACE.into(), &part[..ws_start_len]);
2167 }
2168
2169 emit_table_cell(builder, cell_content, config);
2171
2172 let ws_end_start = ws_start_len + cell_trimmed.len();
2174 if ws_end_start < part.len() {
2175 builder.token(SyntaxKind::WHITESPACE.into(), &part[ws_end_start..]);
2176 }
2177
2178 if idx < cell_contents.len() - 1 || trimmed.ends_with('|') {
2180 builder.token(SyntaxKind::TEXT.into(), "|");
2181 }
2182 }
2183
2184 let trailing_ws_start = leading_ws_len + trimmed.len();
2186 if trailing_ws_start < line_without_newline.len() {
2187 builder.token(
2188 SyntaxKind::WHITESPACE.into(),
2189 &line_without_newline[trailing_ws_start..],
2190 );
2191 }
2192
2193 if !newline_str.is_empty() {
2195 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
2196 }
2197
2198 for &idx in &indices[1..] {
2201 let tail = window.emit_or_dispatch_tail(builder, idx);
2202 emit_line_tokens(builder, tail);
2203 }
2204
2205 builder.finish_node();
2206}
2207
2208pub(crate) fn try_parse_grid_table(
2211 window: &StrippedLines<'_, '_>,
2212 builder: &mut GreenNodeBuilder<'static>,
2213 config: &ParserOptions,
2214) -> Option<usize> {
2215 let lines = window.raw();
2216 let start_pos = window.pos();
2217 if start_pos >= lines.len() {
2218 return None;
2219 }
2220
2221 let view = UniformStripView(window);
2230
2231 if try_parse_grid_separator(view.line(start_pos)).is_none()
2237 && !is_caption_followed_by_table(&view, start_pos)
2238 {
2239 return None;
2240 }
2241
2242 let (actual_start, caption_before) = if is_caption_followed_by_table(&view, start_pos) {
2245 let (cap_start, cap_end) = caption_range_starting_at(&view, start_pos)?;
2246 let mut pos = cap_end;
2247 while pos < view.line_count() && view.line(pos).trim().is_empty() {
2248 pos += 1;
2249 }
2250 (pos, Some((cap_start, cap_end)))
2251 } else {
2252 (start_pos, None)
2253 };
2254
2255 if actual_start >= lines.len() {
2256 return None;
2257 }
2258
2259 let first_line = view.line(actual_start);
2261 let _columns = try_parse_grid_separator(first_line)?;
2262
2263 let mut end_pos = actual_start + 1;
2265 let mut found_header_sep = false;
2266 let mut in_footer = false;
2267
2268 while end_pos < lines.len() {
2270 let line = view.line(end_pos);
2271
2272 if line.trim().is_empty() {
2274 break;
2275 }
2276
2277 if let Some(sep_cols) = try_parse_grid_separator(line) {
2279 if sep_cols.iter().any(|c| c.is_header_separator) {
2281 if !found_header_sep {
2282 found_header_sep = true;
2283 } else if !in_footer {
2284 in_footer = true;
2286 }
2287 }
2288 end_pos += 1;
2289 continue;
2290 }
2291
2292 if is_grid_content_row(line) {
2294 end_pos += 1;
2295 continue;
2296 }
2297
2298 break;
2300 }
2301
2302 if end_pos <= actual_start + 1 {
2305 return None;
2306 }
2307
2308 let caption_before = caption_before.or_else(|| find_caption_before_table(&view, actual_start));
2313
2314 let caption_after = if caption_before.is_some() {
2316 None
2317 } else {
2318 find_caption_after_table(&view, end_pos)
2319 };
2320
2321 builder.start_node(SyntaxKind::GRID_TABLE.into());
2323
2324 if let Some((cap_start, cap_end)) = caption_before {
2326 emit_table_caption(builder, window, cap_start, cap_end, config);
2327 emit_caption_blank_lines(builder, window, cap_end, actual_start);
2329 }
2330
2331 let mut past_header_sep = false;
2333 let mut in_footer_section = false;
2334 let mut current_row_indices: Vec<usize> = Vec::new();
2337 let mut current_row_kind = SyntaxKind::TABLE_HEADER;
2338
2339 for idx in actual_start..end_pos {
2341 let line = view.line(idx);
2342 if let Some(sep_cols) = try_parse_grid_separator(line) {
2343 if !current_row_indices.is_empty() {
2345 emit_grid_table_row(
2346 builder,
2347 window,
2348 ¤t_row_indices,
2349 &sep_cols,
2350 current_row_kind,
2351 config,
2352 );
2353 current_row_indices.clear();
2354 }
2355
2356 let is_header_sep = sep_cols.iter().any(|c| c.is_header_separator);
2357
2358 if is_header_sep {
2361 if !past_header_sep {
2362 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
2364 let tail = window.emit_or_dispatch_tail(builder, idx);
2365 emit_separator_tokens(builder, tail);
2366 builder.finish_node();
2367 past_header_sep = true;
2368 } else {
2369 if !in_footer_section {
2371 in_footer_section = true;
2372 }
2373 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
2374 let tail = window.emit_or_dispatch_tail(builder, idx);
2375 emit_separator_tokens(builder, tail);
2376 builder.finish_node();
2377 }
2378 } else {
2379 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
2381 let tail = window.emit_or_dispatch_tail(builder, idx);
2382 emit_separator_tokens(builder, tail);
2383 builder.finish_node();
2384 }
2385 } else if is_grid_content_row(line) {
2386 current_row_kind = if !past_header_sep && found_header_sep {
2388 SyntaxKind::TABLE_HEADER
2389 } else if in_footer_section {
2390 SyntaxKind::TABLE_FOOTER
2391 } else {
2392 SyntaxKind::TABLE_ROW
2393 };
2394
2395 current_row_indices.push(idx);
2396 }
2397 }
2398
2399 if !current_row_indices.is_empty() {
2401 if let Some(sep_cols) = try_parse_grid_separator(view.line(actual_start)) {
2403 emit_grid_table_row(
2404 builder,
2405 window,
2406 ¤t_row_indices,
2407 &sep_cols,
2408 current_row_kind,
2409 config,
2410 );
2411 }
2412 }
2413
2414 if let Some((cap_start, cap_end)) = caption_after {
2416 emit_caption_blank_lines(builder, window, end_pos, cap_start);
2417 emit_table_caption(builder, window, cap_start, cap_end, config);
2418 }
2419
2420 builder.finish_node(); let table_start = caption_before
2424 .map(|(start, _)| start)
2425 .unwrap_or(actual_start);
2426 let table_end = if let Some((_, cap_end)) = caption_after {
2427 cap_end
2428 } else {
2429 end_pos
2430 };
2431
2432 Some(table_end - table_start)
2433}
2434
2435#[cfg(test)]
2436mod grid_table_tests {
2437 use super::super::container_prefix::ContainerPrefix;
2438 use super::*;
2439
2440 #[test]
2441 fn test_grid_separator_detection() {
2442 assert!(try_parse_grid_separator("+---+---+").is_some());
2443 assert!(try_parse_grid_separator("+===+===+").is_some());
2444 assert!(try_parse_grid_separator("+---------------+---------------+").is_some());
2445 assert!(try_parse_grid_separator("+:---:+").is_some()); assert!(try_parse_grid_separator("not a separator").is_none());
2447 assert!(try_parse_grid_separator("|---|---|").is_none()); assert!(try_parse_grid_separator(" +---+---+").is_none());
2454 assert!(try_parse_grid_separator(" +---+---+").is_none());
2455 assert!(try_parse_grid_separator(" +===+===+").is_none());
2456 }
2457
2458 #[test]
2459 fn test_grid_header_separator() {
2460 let cols = try_parse_grid_separator("+===+===+").unwrap();
2461 assert!(cols.iter().all(|c| c.is_header_separator));
2462
2463 let cols2 = try_parse_grid_separator("+---+---+").unwrap();
2464 assert!(cols2.iter().all(|c| !c.is_header_separator));
2465 }
2466
2467 #[test]
2468 fn test_grid_content_row_detection() {
2469 assert!(is_grid_content_row("| content | content |"));
2470 assert!(is_grid_content_row("| | |"));
2471 assert!(is_grid_content_row("| content +------+"));
2472 assert!(!is_grid_content_row("+---+---+")); assert!(!is_grid_content_row("no pipes here"));
2474 }
2475
2476 #[test]
2477 fn test_basic_grid_table() {
2478 let input = vec![
2479 "+-------+-------+",
2480 "| Col1 | Col2 |",
2481 "+=======+=======+",
2482 "| A | B |",
2483 "+-------+-------+",
2484 "",
2485 ];
2486
2487 let mut builder = GreenNodeBuilder::new();
2488 let prefix = ContainerPrefix::default();
2489 let window = StrippedLines::new(&input, 0, &prefix);
2490 let result = try_parse_grid_table(&window, &mut builder, &ParserOptions::default());
2491
2492 assert!(result.is_some());
2493 assert_eq!(result.unwrap(), 5);
2494 }
2495
2496 #[test]
2497 fn test_grid_table_multirow() {
2498 let input = vec![
2499 "+---------------+---------------+",
2500 "| Fruit | Advantages |",
2501 "+===============+===============+",
2502 "| Bananas | - wrapper |",
2503 "| | - color |",
2504 "+---------------+---------------+",
2505 "| Oranges | - scurvy |",
2506 "| | - tasty |",
2507 "+---------------+---------------+",
2508 "",
2509 ];
2510
2511 let mut builder = GreenNodeBuilder::new();
2512 let prefix = ContainerPrefix::default();
2513 let window = StrippedLines::new(&input, 0, &prefix);
2514 let result = try_parse_grid_table(&window, &mut builder, &ParserOptions::default());
2515
2516 assert!(result.is_some());
2517 assert_eq!(result.unwrap(), 9);
2518 }
2519
2520 #[test]
2521 fn test_grid_table_with_footer() {
2522 let input = vec![
2523 "+-------+-------+",
2524 "| Fruit | Price |",
2525 "+=======+=======+",
2526 "| Apple | $1.00 |",
2527 "+-------+-------+",
2528 "| Pear | $1.50 |",
2529 "+=======+=======+",
2530 "| Total | $2.50 |",
2531 "+=======+=======+",
2532 "",
2533 ];
2534
2535 let mut builder = GreenNodeBuilder::new();
2536 let prefix = ContainerPrefix::default();
2537 let window = StrippedLines::new(&input, 0, &prefix);
2538 let result = try_parse_grid_table(&window, &mut builder, &ParserOptions::default());
2539
2540 assert!(result.is_some());
2541 assert_eq!(result.unwrap(), 9);
2542 }
2543
2544 #[test]
2545 fn test_grid_table_headerless() {
2546 let input = vec![
2547 "+-------+-------+",
2548 "| A | B |",
2549 "+-------+-------+",
2550 "| C | D |",
2551 "+-------+-------+",
2552 "",
2553 ];
2554
2555 let mut builder = GreenNodeBuilder::new();
2556 let prefix = ContainerPrefix::default();
2557 let window = StrippedLines::new(&input, 0, &prefix);
2558 let result = try_parse_grid_table(&window, &mut builder, &ParserOptions::default());
2559
2560 assert!(result.is_some());
2561 assert_eq!(result.unwrap(), 5);
2562 }
2563
2564 #[test]
2565 fn test_grid_table_with_caption_before() {
2566 let input = vec![
2567 ": Sample table",
2568 "",
2569 "+-------+-------+",
2570 "| A | B |",
2571 "+=======+=======+",
2572 "| C | D |",
2573 "+-------+-------+",
2574 "",
2575 ];
2576
2577 let mut builder = GreenNodeBuilder::new();
2578 let prefix = ContainerPrefix::default();
2579 let window = StrippedLines::new(&input, 2, &prefix);
2580 let result = try_parse_grid_table(&window, &mut builder, &ParserOptions::default());
2581
2582 assert!(result.is_some());
2583 assert_eq!(result.unwrap(), 7);
2585 }
2586
2587 #[test]
2588 fn test_grid_table_with_caption_after() {
2589 let input = vec![
2590 "+-------+-------+",
2591 "| A | B |",
2592 "+=======+=======+",
2593 "| C | D |",
2594 "+-------+-------+",
2595 "",
2596 "Table: My grid table",
2597 "",
2598 ];
2599
2600 let mut builder = GreenNodeBuilder::new();
2601 let prefix = ContainerPrefix::default();
2602 let window = StrippedLines::new(&input, 0, &prefix);
2603 let result = try_parse_grid_table(&window, &mut builder, &ParserOptions::default());
2604
2605 assert!(result.is_some());
2606 assert_eq!(result.unwrap(), 7);
2608 }
2609}
2610
2611fn try_parse_multiline_separator(line: &str) -> Option<Vec<Column>> {
2619 let trimmed = line.trim_start();
2620 let leading_spaces = line.len() - trimmed.len();
2621
2622 if leading_spaces > 3 {
2624 return None;
2625 }
2626
2627 let trimmed = trimmed.trim_end();
2628
2629 if trimmed.is_empty() || !trimmed.chars().all(|c| c == '-') {
2631 return None;
2632 }
2633
2634 if trimmed.len() < 3 {
2636 return None;
2637 }
2638
2639 Some(vec![Column {
2641 start: leading_spaces,
2642 end: leading_spaces + trimmed.len(),
2643 alignment: Alignment::Default,
2644 }])
2645}
2646
2647fn is_column_separator(line: &str) -> bool {
2650 try_parse_table_separator(line).is_some() && !line.contains('*') && !line.contains('_')
2651}
2652
2653fn is_headerless_single_row_without_blank(
2654 lines: &(impl LineView + ?Sized),
2655 row_start: usize,
2656 row_end: usize,
2657 columns: &[Column],
2658) -> bool {
2659 if row_start >= row_end {
2660 return false;
2661 }
2662
2663 if row_end - row_start == 1 {
2664 return false;
2665 }
2666
2667 let Some(last_col) = columns.last() else {
2668 return false;
2669 };
2670
2671 for i in (row_start + 1)..row_end {
2672 let (content, _) = strip_newline(lines.line(i));
2673 let prefix_end = last_col.start.min(content.len());
2674 if !content[..prefix_end].trim().is_empty() {
2675 return false;
2676 }
2677 }
2678
2679 true
2680}
2681
2682pub(crate) fn try_parse_multiline_table(
2685 window: &StrippedLines<'_, '_>,
2686 builder: &mut GreenNodeBuilder<'static>,
2687 config: &ParserOptions,
2688) -> Option<usize> {
2689 let lines = window.raw();
2690 let start_pos = window.pos();
2691 if start_pos >= lines.len() {
2692 return None;
2693 }
2694
2695 let first_line = window.strip_at(start_pos);
2701
2702 let is_full_width_start = try_parse_multiline_separator(first_line).is_some();
2706 let is_column_sep_start = !is_full_width_start && is_column_separator(first_line);
2707 if !is_full_width_start && !is_column_sep_start {
2708 return None;
2709 }
2710
2711 let headerless_columns = if is_column_sep_start {
2720 try_parse_table_separator(window.line(start_pos))
2721 } else {
2722 None
2723 };
2724
2725 let opening_has_wide_column = headerless_columns
2732 .as_deref()
2733 .is_some_and(|cols| cols.iter().any(|col| col.end - col.start >= 2));
2734
2735 let mut pos = start_pos + 1;
2737 let mut found_column_sep = is_column_sep_start; let mut column_sep_pos = if is_column_sep_start { start_pos } else { 0 };
2739 let mut has_header = false;
2740 let mut found_blank_line = false;
2741 let mut found_closing_sep = false;
2742 let mut content_line_count = 0usize;
2743
2744 while pos < lines.len() {
2746 let line = window.line(pos);
2747
2748 if is_full_width_start && is_column_separator(line) && !found_column_sep {
2750 found_column_sep = true;
2751 column_sep_pos = pos;
2752 has_header = pos > start_pos + 1; pos += 1;
2754 continue;
2755 }
2756
2757 if line.trim().is_empty() {
2759 found_blank_line = true;
2760 pos += 1;
2761 if pos < lines.len() {
2763 let next = window.line(pos);
2764 let is_valid_closer = if is_full_width_start {
2765 try_parse_multiline_separator(next).is_some()
2766 } else {
2767 is_column_separator(next)
2775 || (opening_has_wide_column
2776 && try_parse_multiline_separator(next).is_some())
2777 };
2778 if is_valid_closer {
2779 found_closing_sep = true;
2780 pos += 1; break;
2782 }
2783 }
2784 continue;
2785 }
2786
2787 if is_full_width_start && try_parse_multiline_separator(line).is_some() {
2789 found_closing_sep = true;
2790 pos += 1;
2791 break;
2792 }
2793
2794 if is_column_sep_start && is_column_separator(line) && content_line_count > 0 {
2796 found_closing_sep = true;
2797 pos += 1;
2798 break;
2799 }
2800
2801 content_line_count += 1;
2803 pos += 1;
2804 }
2805
2806 if !found_column_sep {
2808 return None;
2809 }
2810
2811 if !found_blank_line && is_column_sep_start {
2820 let columns = headerless_columns.as_deref()?;
2821 if !is_headerless_single_row_without_blank(window, start_pos + 1, pos - 1, columns) {
2822 return None;
2823 }
2824 }
2825
2826 if !found_closing_sep {
2828 return None;
2829 }
2830
2831 if pos <= start_pos + 2 {
2833 return None;
2834 }
2835
2836 let end_pos = pos;
2837
2838 let columns = try_parse_table_separator(window.line(column_sep_pos))
2840 .expect("Column separator must be valid");
2841
2842 let caption_before = find_caption_before_table(window, start_pos);
2844
2845 let caption_after = if caption_before.is_some() {
2847 None
2848 } else {
2849 find_caption_after_table(window, end_pos)
2850 };
2851
2852 builder.start_node(SyntaxKind::MULTILINE_TABLE.into());
2854
2855 if let Some((cap_start, cap_end)) = caption_before {
2857 emit_table_caption(builder, window, cap_start, cap_end, config);
2858 emit_caption_blank_lines(builder, window, cap_end, start_pos);
2860 }
2861
2862 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
2866 let tail = window.emit_or_dispatch_tail(builder, start_pos);
2867 emit_separator_tokens(builder, tail);
2868 builder.finish_node();
2869
2870 let mut in_header = has_header;
2874 let mut current_row_indices: Vec<usize> = Vec::new();
2875
2876 for i in (start_pos + 1)..end_pos {
2877 let line = window.line(i);
2878 if i == column_sep_pos {
2880 if !current_row_indices.is_empty() {
2882 emit_multiline_table_row(
2883 builder,
2884 window,
2885 ¤t_row_indices,
2886 &columns,
2887 SyntaxKind::TABLE_HEADER,
2888 config,
2889 );
2890 current_row_indices.clear();
2891 }
2892
2893 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
2894 let tail = window.emit_or_dispatch_tail(builder, i);
2895 emit_separator_tokens(builder, tail);
2896 builder.finish_node();
2897 in_header = false;
2898 continue;
2899 }
2900
2901 if try_parse_multiline_separator(line).is_some() || is_column_separator(line) {
2903 if !current_row_indices.is_empty() {
2905 let kind = if in_header {
2906 SyntaxKind::TABLE_HEADER
2907 } else {
2908 SyntaxKind::TABLE_ROW
2909 };
2910 emit_multiline_table_row(
2911 builder,
2912 window,
2913 ¤t_row_indices,
2914 &columns,
2915 kind,
2916 config,
2917 );
2918 current_row_indices.clear();
2919 }
2920
2921 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
2922 let tail = window.emit_or_dispatch_tail(builder, i);
2923 emit_separator_tokens(builder, tail);
2924 builder.finish_node();
2925 continue;
2926 }
2927
2928 if line.trim().is_empty() {
2930 if !current_row_indices.is_empty() {
2932 let kind = if in_header {
2933 SyntaxKind::TABLE_HEADER
2934 } else {
2935 SyntaxKind::TABLE_ROW
2936 };
2937 emit_multiline_table_row(
2938 builder,
2939 window,
2940 ¤t_row_indices,
2941 &columns,
2942 kind,
2943 config,
2944 );
2945 current_row_indices.clear();
2946 }
2947
2948 builder.start_node(SyntaxKind::BLANK_LINE.into());
2951 let tail = window.emit_or_dispatch_tail(builder, i);
2952 builder.token(SyntaxKind::BLANK_LINE.into(), tail);
2953 builder.finish_node();
2954 continue;
2955 }
2956
2957 current_row_indices.push(i);
2959 }
2960
2961 if !current_row_indices.is_empty() {
2963 let kind = if in_header {
2964 SyntaxKind::TABLE_HEADER
2965 } else {
2966 SyntaxKind::TABLE_ROW
2967 };
2968 emit_multiline_table_row(
2969 builder,
2970 window,
2971 ¤t_row_indices,
2972 &columns,
2973 kind,
2974 config,
2975 );
2976 }
2977
2978 if let Some((cap_start, cap_end)) = caption_after {
2980 emit_caption_blank_lines(builder, window, end_pos, cap_start);
2981 emit_table_caption(builder, window, cap_start, cap_end, config);
2982 }
2983
2984 builder.finish_node(); let table_start = caption_before.map(|(start, _)| start).unwrap_or(start_pos);
2988 let table_end = if let Some((_, cap_end)) = caption_after {
2989 cap_end
2990 } else {
2991 end_pos
2992 };
2993
2994 Some(table_end - table_start)
2995}
2996
2997fn extract_first_line_cell_contents(line: &str, columns: &[Column]) -> Vec<String> {
3000 let (line_content, _) = strip_newline(line);
3001 let mut cells = Vec::new();
3002
3003 for column in columns.iter() {
3004 let column_start = column_offset_to_byte_index(line_content, column.start);
3005 let column_end = column_offset_to_byte_index(line_content, column.end);
3006
3007 let cell_text = if column_start < column_end {
3009 &line_content[column_start..column_end]
3010 } else if column_start < line_content.len() {
3011 &line_content[column_start..]
3012 } else {
3013 ""
3014 };
3015
3016 cells.push(cell_text.to_string());
3017 }
3018
3019 cells
3020}
3021
3022fn emit_multiline_table_row(
3029 builder: &mut GreenNodeBuilder<'static>,
3030 window: &StrippedLines<'_, '_>,
3031 indices: &[usize],
3032 columns: &[Column],
3033 kind: SyntaxKind,
3034 config: &ParserOptions,
3035) {
3036 if indices.is_empty() {
3037 return;
3038 }
3039
3040 builder.start_node(kind.into());
3041
3042 let first_line = window.emit_or_dispatch_tail(builder, indices[0]);
3046 let cell_contents = extract_first_line_cell_contents(first_line, columns);
3047 let (trimmed, newline_str) = strip_newline(first_line);
3048 let mut current_pos = 0;
3049
3050 for (col_idx, column) in columns.iter().enumerate() {
3051 let cell_text = &cell_contents[col_idx];
3052 let cell_start = column_offset_to_byte_index(trimmed, column.start);
3053 let cell_end = column_offset_to_byte_index(trimmed, column.end);
3054
3055 if current_pos < cell_start {
3057 builder.token(
3058 SyntaxKind::WHITESPACE.into(),
3059 &trimmed[current_pos..cell_start],
3060 );
3061 }
3062
3063 emit_table_cell(builder, cell_text, config);
3065
3066 current_pos = cell_end;
3067 }
3068
3069 if current_pos < trimmed.len() {
3071 builder.token(SyntaxKind::WHITESPACE.into(), &trimmed[current_pos..]);
3072 }
3073
3074 if !newline_str.is_empty() {
3076 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
3077 }
3078
3079 for &idx in &indices[1..] {
3082 let tail = window.emit_or_dispatch_tail(builder, idx);
3083 emit_line_tokens(builder, tail);
3084 }
3085
3086 builder.finish_node();
3087}
3088
3089#[cfg(test)]
3090mod multiline_table_tests {
3091 use super::super::container_prefix::ContainerPrefix;
3092 use super::*;
3093 use crate::syntax::SyntaxNode;
3094
3095 #[test]
3096 fn test_multiline_separator_detection() {
3097 assert!(
3098 try_parse_multiline_separator(
3099 "-------------------------------------------------------------"
3100 )
3101 .is_some()
3102 );
3103 assert!(try_parse_multiline_separator("---").is_some());
3104 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()); }
3109
3110 #[test]
3111 fn test_basic_multiline_table() {
3112 let input = vec![
3113 "-------------------------------------------------------------",
3114 " Centered Default Right Left",
3115 " Header Aligned Aligned Aligned",
3116 "----------- ------- --------------- -------------------------",
3117 " First row 12.0 Example of a row that",
3118 " spans multiple lines.",
3119 "",
3120 " Second row 5.0 Here's another one.",
3121 "-------------------------------------------------------------",
3122 "",
3123 ];
3124
3125 let mut builder = GreenNodeBuilder::new();
3126 let prefix = ContainerPrefix::default();
3127 let window = StrippedLines::new(&input, 0, &prefix);
3128 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3129
3130 assert!(result.is_some());
3131 assert_eq!(result.unwrap(), 9);
3132 }
3133
3134 #[test]
3135 fn test_multiline_table_headerless() {
3136 let input = vec![
3137 "----------- ------- --------------- -------------------------",
3138 " First row 12.0 Example of a row that",
3139 " spans multiple lines.",
3140 "",
3141 " Second row 5.0 Here's another one.",
3142 "----------- ------- --------------- -------------------------",
3143 "",
3144 ];
3145
3146 let mut builder = GreenNodeBuilder::new();
3147 let prefix = ContainerPrefix::default();
3148 let window = StrippedLines::new(&input, 0, &prefix);
3149 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3150
3151 assert!(result.is_some());
3152 assert_eq!(result.unwrap(), 6);
3153 }
3154
3155 #[test]
3156 fn test_multiline_table_headerless_single_line_is_not_multiline() {
3157 let input = vec![
3158 "------- ------ ---------- -------",
3159 " 12 12 12 12",
3160 "------- ------ ---------- -------",
3161 "",
3162 "Not part of table.",
3163 "",
3164 ];
3165
3166 let mut builder = GreenNodeBuilder::new();
3167 let prefix = ContainerPrefix::default();
3168 let window = StrippedLines::new(&input, 0, &prefix);
3169 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3170
3171 assert!(result.is_none());
3172 }
3173
3174 #[test]
3175 fn test_multiline_table_headerless_single_row_continuation_without_blank_line() {
3176 let input = vec![
3177 "---------- --------- ----------- ---------------------------",
3178 " First row 12.0 Example of a row that spans",
3179 " multiple lines.",
3180 "---------- --------- ----------- ---------------------------",
3181 "",
3182 ];
3183
3184 let mut builder = GreenNodeBuilder::new();
3185 let prefix = ContainerPrefix::default();
3186 let window = StrippedLines::new(&input, 0, &prefix);
3187 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3188
3189 assert!(result.is_some());
3190 assert_eq!(result.unwrap(), 4);
3191 }
3192
3193 #[test]
3194 fn test_multiline_table_with_caption() {
3195 let input = vec![
3196 "-------------------------------------------------------------",
3197 " Col1 Col2",
3198 "----------- -------",
3199 " A B",
3200 "",
3201 "-------------------------------------------------------------",
3202 "",
3203 "Table: Here's the caption.",
3204 "",
3205 ];
3206
3207 let mut builder = GreenNodeBuilder::new();
3208 let prefix = ContainerPrefix::default();
3209 let window = StrippedLines::new(&input, 0, &prefix);
3210 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3211
3212 assert!(result.is_some());
3213 assert_eq!(result.unwrap(), 8);
3215 }
3216
3217 #[test]
3218 fn test_multiline_table_single_row() {
3219 let input = vec![
3220 "---------------------------------------------",
3221 " Header1 Header2",
3222 "----------- -----------",
3223 " Data More data",
3224 "",
3225 "---------------------------------------------",
3226 "",
3227 ];
3228
3229 let mut builder = GreenNodeBuilder::new();
3230 let prefix = ContainerPrefix::default();
3231 let window = StrippedLines::new(&input, 0, &prefix);
3232 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3233
3234 assert!(result.is_some());
3235 assert_eq!(result.unwrap(), 6);
3236 }
3237
3238 #[test]
3239 fn test_headerless_multiline_table_does_not_close_on_full_width_rule() {
3240 let input = vec![
3241 "- - - - -",
3242 "Third section with underscores.",
3243 "",
3244 "_____",
3245 "",
3246 "> Quote before rule",
3247 ">",
3248 "> ***",
3249 ">",
3250 "> Quote after rule",
3251 "",
3252 "Final paragraph.",
3253 "",
3254 "Here's a horizontal rule:",
3255 "",
3256 "---",
3257 "Text directly after the horizontal rule.",
3258 "",
3259 ];
3260
3261 let mut builder = GreenNodeBuilder::new();
3262 let prefix = ContainerPrefix::default();
3263 let window = StrippedLines::new(&input, 0, &prefix);
3264 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3265
3266 assert!(result.is_none());
3267 }
3268
3269 #[test]
3270 fn test_not_multiline_table() {
3271 let input = vec![
3273 " Right Left Center Default",
3274 "------- ------ ---------- -------",
3275 " 12 12 12 12",
3276 "",
3277 ];
3278
3279 let mut builder = GreenNodeBuilder::new();
3280 let prefix = ContainerPrefix::default();
3281 let window = StrippedLines::new(&input, 0, &prefix);
3282 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3283
3284 assert!(result.is_none());
3286 }
3287
3288 #[test]
3290 fn test_emit_table_cell_plain_text() {
3291 let mut builder = GreenNodeBuilder::new();
3292 emit_table_cell(&mut builder, "Cell", &ParserOptions::default());
3293 let green = builder.finish();
3294 let node = SyntaxNode::new_root(green);
3295
3296 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3297 assert_eq!(node.text(), "Cell");
3298
3299 let children: Vec<_> = node.children_with_tokens().collect();
3301 assert_eq!(children.len(), 1);
3302 assert_eq!(children[0].kind(), SyntaxKind::TEXT);
3303 }
3304
3305 #[test]
3306 fn test_emit_table_cell_with_emphasis() {
3307 let mut builder = GreenNodeBuilder::new();
3308 emit_table_cell(&mut builder, "*italic*", &ParserOptions::default());
3309 let green = builder.finish();
3310 let node = SyntaxNode::new_root(green);
3311
3312 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3313 assert_eq!(node.text(), "*italic*");
3314
3315 let children: Vec<_> = node.children().collect();
3317 assert_eq!(children.len(), 1);
3318 assert_eq!(children[0].kind(), SyntaxKind::EMPHASIS);
3319 }
3320
3321 #[test]
3322 fn test_emit_table_cell_with_code() {
3323 let mut builder = GreenNodeBuilder::new();
3324 emit_table_cell(&mut builder, "`code`", &ParserOptions::default());
3325 let green = builder.finish();
3326 let node = SyntaxNode::new_root(green);
3327
3328 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3329 assert_eq!(node.text(), "`code`");
3330
3331 let children: Vec<_> = node.children().collect();
3333 assert_eq!(children.len(), 1);
3334 assert_eq!(children[0].kind(), SyntaxKind::INLINE_CODE);
3335 }
3336
3337 #[test]
3338 fn test_emit_table_cell_with_link() {
3339 let mut builder = GreenNodeBuilder::new();
3340 emit_table_cell(&mut builder, "[text](url)", &ParserOptions::default());
3341 let green = builder.finish();
3342 let node = SyntaxNode::new_root(green);
3343
3344 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3345 assert_eq!(node.text(), "[text](url)");
3346
3347 let children: Vec<_> = node.children().collect();
3349 assert_eq!(children.len(), 1);
3350 assert_eq!(children[0].kind(), SyntaxKind::LINK);
3351 }
3352
3353 #[test]
3354 fn test_emit_table_cell_with_strong() {
3355 let mut builder = GreenNodeBuilder::new();
3356 emit_table_cell(&mut builder, "**bold**", &ParserOptions::default());
3357 let green = builder.finish();
3358 let node = SyntaxNode::new_root(green);
3359
3360 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3361 assert_eq!(node.text(), "**bold**");
3362
3363 let children: Vec<_> = node.children().collect();
3365 assert_eq!(children.len(), 1);
3366 assert_eq!(children[0].kind(), SyntaxKind::STRONG);
3367 }
3368
3369 #[test]
3370 fn test_emit_table_cell_mixed_inline() {
3371 let mut builder = GreenNodeBuilder::new();
3372 emit_table_cell(
3373 &mut builder,
3374 "Text **bold** and `code`",
3375 &ParserOptions::default(),
3376 );
3377 let green = builder.finish();
3378 let node = SyntaxNode::new_root(green);
3379
3380 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3381 assert_eq!(node.text(), "Text **bold** and `code`");
3382
3383 let children: Vec<_> = node.children_with_tokens().collect();
3385 assert!(children.len() >= 4);
3386
3387 assert_eq!(children[0].kind(), SyntaxKind::TEXT);
3389 assert_eq!(children[1].kind(), SyntaxKind::STRONG);
3390 }
3391
3392 #[test]
3393 fn test_emit_table_cell_empty() {
3394 let mut builder = GreenNodeBuilder::new();
3395 emit_table_cell(&mut builder, "", &ParserOptions::default());
3396 let green = builder.finish();
3397 let node = SyntaxNode::new_root(green);
3398
3399 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3400 assert_eq!(node.text(), "");
3401
3402 let children: Vec<_> = node.children_with_tokens().collect();
3404 assert_eq!(children.len(), 0);
3405 }
3406
3407 #[test]
3408 fn test_emit_table_cell_escaped_pipe() {
3409 let mut builder = GreenNodeBuilder::new();
3410 emit_table_cell(&mut builder, r"A \| B", &ParserOptions::default());
3411 let green = builder.finish();
3412 let node = SyntaxNode::new_root(green);
3413
3414 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3415 assert_eq!(node.text(), r"A \| B");
3417 }
3418}